From c2e136973863c5c838986db08cd5a63fb8306da1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Wo=C5=BAniak?= Date: Wed, 27 Feb 2019 17:59:57 +0100 Subject: [PATCH] Gh 3/open file (#15) * Remove unused imports Start FileSystem UI Fix formating Add textures for dir and file, implement prepare_ui, render, update and open directory Display choose file Display files and directories Format code Expand and collapse directories, open file Format code Fix calculating size of directory and displaying children Refactor render open file modal Format code Scroll file tree Format code Refactor open file modal Fix CI Fix some tests, add more tests, fix formatting Fix CI test run Fix CI test run * Add more tests * Add more tests * More tests * More tests * Fix caret position * Add simple string matching * Fixing add characters * Fix themes images * Simplify * Simplify * Simplify * Fix some problems * Fix race conditions in tests * Format code * Format code --- .circleci/config.yml | 19 +- Cargo.lock | 78 +++- Cargo.toml | 28 +- assets/gear-64x64.png | Bin 15812 -> 0 bytes assets/gear-64x64.raw | Bin 15343 -> 0 bytes assets/gear.jpg | Bin 51938 -> 0 bytes assets/theme.txt | 53 --- rider-config/Cargo.toml | 23 + rider-config/src/config.rs | 242 ++++++++++ rider-config/src/directories.rs | 183 ++++++++ rider-config/src/editor_config.rs | 93 ++++ src/config/mod.rs => rider-config/src/lib.rs | 12 +- .../src}/scroll_config.rs | 12 +- rider-editor/Cargo.toml | 22 + .../assets/images}/gear-64x64.bmp | Bin rider-editor/src/app/app_state.rs | 185 ++++++++ rider-editor/src/app/application.rs | 324 +++++++++++++ rider-editor/src/app/caret_manager.rs | 67 +++ .../src}/app/file_content_manager.rs | 67 ++- {src => rider-editor/src}/app/mod.rs | 0 rider-editor/src/main.rs | 57 +++ .../src}/renderer/managers.rs | 48 +- {src => rider-editor/src}/renderer/mod.rs | 0 rider-editor/src/renderer/renderer.rs | 83 ++++ rider-editor/src/tests.rs | 11 + {src => rider-editor/src}/ui/caret/caret.rs | 35 +- .../src}/ui/caret/caret_color.rs | 1 - .../src}/ui/caret/caret_position.rs | 0 {src => rider-editor/src}/ui/caret/mod.rs | 0 .../src}/ui/file/editor_file.rs | 15 +- .../src}/ui/file/editor_file_section.rs | 12 +- .../src}/ui/file/editor_file_token.rs | 47 +- {src => rider-editor/src}/ui/file/mod.rs | 0 .../src}/ui/file_editor/file_editor.rs | 41 +- .../src}/ui/file_editor/mod.rs | 7 +- rider-editor/src/ui/filesystem/directory.rs | 432 ++++++++++++++++++ rider-editor/src/ui/filesystem/file.rs | 204 +++++++++ rider-editor/src/ui/filesystem/mod.rs | 7 + {src => rider-editor/src}/ui/menu_bar.rs | 54 ++- {src => rider-editor/src}/ui/mod.rs | 52 ++- rider-editor/src/ui/modal/mod.rs | 3 + rider-editor/src/ui/modal/open_file.rs | 235 ++++++++++ .../src}/ui/project_tree/mod.rs | 0 .../ui/scroll_bar/horizontal_scroll_bar.rs | 31 +- .../src}/ui/scroll_bar/mod.rs | 6 +- .../src}/ui/scroll_bar/vertical_scroll_bar.rs | 34 +- .../src}/ui/text_character.rs | 191 ++++---- rider-generator/Cargo.toml | 17 + .../assets}/fonts/Beyond Wonderland.ttf | Bin .../assets}/fonts/DejaVuSansMono.ttf | Bin .../assets}/fonts/ElaineSans-Medium.ttf | Bin .../themes/default/images/directory-48x48.png | Bin 0 -> 2004 bytes .../default/images/directory-512x512.png | Bin 0 -> 16260 bytes .../themes/default/images/directory-64x64.png | Bin 0 -> 2322 bytes .../themes/default/images/file-48x48.png | Bin 0 -> 866 bytes .../themes/default/images/file-512x512.png | Bin 0 -> 3217 bytes .../themes/default/images/file-64x64.png | Bin 0 -> 929 bytes .../railscasts/images/directory-48x48.png | Bin 0 -> 2004 bytes .../railscasts/images/directory-512x512.png | Bin 0 -> 16260 bytes .../railscasts/images/directory-64x64.png | Bin 0 -> 2322 bytes .../themes/railscasts/images/file-48x48.png | Bin 0 -> 866 bytes .../themes/railscasts/images/file-512x512.png | Bin 0 -> 3217 bytes .../themes/railscasts/images/file-64x64.png | Bin 0 -> 2761 bytes rider-generator/src/config.rs | 102 +++++ rider-generator/src/images.rs | 218 +++++++++ rider-generator/src/main.rs | 120 +++++ rider-generator/src/themes.rs | 81 ++++ rider-generator/src/write_bytes_to.rs | 34 ++ rider-lexers/Cargo.toml | 10 + src/lexer/mod.rs => rider-lexers/src/lib.rs | 28 +- {src/lexer => rider-lexers/src}/plain.rs | 49 +- {src/lexer => rider-lexers/src}/rust_lang.rs | 62 +-- rider-themes/Cargo.toml | 15 + .../src}/caret_color.rs | 6 +- rider-themes/src/code_highlighting_color.rs | 273 +++++++++++ .../themes => rider-themes/src}/diff_color.rs | 10 +- rider-themes/src/images.rs | 51 +++ rider-themes/src/lib.rs | 21 + .../src}/predef/default.rs | 2 +- .../themes => rider-themes/src}/predef/mod.rs | 0 .../src}/predef/railscasts.rs | 18 +- rider-themes/src/serde_color.rs | 49 ++ rider-themes/src/theme.rs | 141 ++++++ rider-themes/src/theme_config.rs | 59 +++ scripts/test.sh | 7 + src/app/app_state.rs | 113 ----- src/app/application.rs | 260 ----------- src/app/caret_manager.rs | 64 --- src/config/config.rs | 138 ------ src/config/creator.rs | 51 --- src/config/directories.rs | 36 -- src/config/editor_config.rs | 44 -- src/main.rs | 106 ++--- src/renderer/renderer.rs | 45 -- src/tests.rs | 28 -- src/themes/code_highlighting_color.rs | 123 ----- src/themes/config_creator.rs | 26 -- src/themes/mod.rs | 23 - src/themes/serde_color.rs | 26 -- src/themes/theme.rs | 91 ---- src/themes/theme_config.rs | 30 -- {assets/examples => test_files}/example.txt | 0 {assets/examples => test_files}/test.rs | 0 103 files changed, 4009 insertions(+), 1582 deletions(-) delete mode 100644 assets/gear-64x64.png delete mode 100644 assets/gear-64x64.raw delete mode 100644 assets/gear.jpg delete mode 100644 assets/theme.txt create mode 100644 rider-config/Cargo.toml create mode 100644 rider-config/src/config.rs create mode 100644 rider-config/src/directories.rs create mode 100644 rider-config/src/editor_config.rs rename src/config/mod.rs => rider-config/src/lib.rs (56%) rename {src/config => rider-config/src}/scroll_config.rs (91%) create mode 100644 rider-editor/Cargo.toml rename {assets => rider-editor/assets/images}/gear-64x64.bmp (100%) create mode 100644 rider-editor/src/app/app_state.rs create mode 100644 rider-editor/src/app/application.rs create mode 100644 rider-editor/src/app/caret_manager.rs rename {src => rider-editor/src}/app/file_content_manager.rs (70%) rename {src => rider-editor/src}/app/mod.rs (100%) create mode 100644 rider-editor/src/main.rs rename {src => rider-editor/src}/renderer/managers.rs (75%) rename {src => rider-editor/src}/renderer/mod.rs (100%) create mode 100644 rider-editor/src/renderer/renderer.rs create mode 100644 rider-editor/src/tests.rs rename {src => rider-editor/src}/ui/caret/caret.rs (93%) rename {src => rider-editor/src}/ui/caret/caret_color.rs (97%) rename {src => rider-editor/src}/ui/caret/caret_position.rs (100%) rename {src => rider-editor/src}/ui/caret/mod.rs (100%) rename {src => rider-editor/src}/ui/file/editor_file.rs (96%) rename {src => rider-editor/src}/ui/file/editor_file_section.rs (96%) rename {src => rider-editor/src}/ui/file/editor_file_token.rs (92%) rename {src => rider-editor/src}/ui/file/mod.rs (100%) rename {src => rider-editor/src}/ui/file_editor/file_editor.rs (95%) rename {src => rider-editor/src}/ui/file_editor/mod.rs (91%) create mode 100644 rider-editor/src/ui/filesystem/directory.rs create mode 100644 rider-editor/src/ui/filesystem/file.rs create mode 100644 rider-editor/src/ui/filesystem/mod.rs rename {src => rider-editor/src}/ui/menu_bar.rs (85%) rename {src => rider-editor/src}/ui/mod.rs (69%) create mode 100644 rider-editor/src/ui/modal/mod.rs create mode 100644 rider-editor/src/ui/modal/open_file.rs rename {src => rider-editor/src}/ui/project_tree/mod.rs (100%) rename {src => rider-editor/src}/ui/scroll_bar/horizontal_scroll_bar.rs (89%) rename {src => rider-editor/src}/ui/scroll_bar/mod.rs (68%) rename {src => rider-editor/src}/ui/scroll_bar/vertical_scroll_bar.rs (88%) rename {src => rider-editor/src}/ui/text_character.rs (80%) create mode 100644 rider-generator/Cargo.toml rename {assets => rider-generator/assets}/fonts/Beyond Wonderland.ttf (100%) rename {assets => rider-generator/assets}/fonts/DejaVuSansMono.ttf (100%) rename {assets => rider-generator/assets}/fonts/ElaineSans-Medium.ttf (100%) create mode 100644 rider-generator/assets/themes/default/images/directory-48x48.png create mode 100644 rider-generator/assets/themes/default/images/directory-512x512.png create mode 100644 rider-generator/assets/themes/default/images/directory-64x64.png create mode 100644 rider-generator/assets/themes/default/images/file-48x48.png create mode 100644 rider-generator/assets/themes/default/images/file-512x512.png create mode 100644 rider-generator/assets/themes/default/images/file-64x64.png create mode 100644 rider-generator/assets/themes/railscasts/images/directory-48x48.png create mode 100644 rider-generator/assets/themes/railscasts/images/directory-512x512.png create mode 100644 rider-generator/assets/themes/railscasts/images/directory-64x64.png create mode 100644 rider-generator/assets/themes/railscasts/images/file-48x48.png create mode 100644 rider-generator/assets/themes/railscasts/images/file-512x512.png create mode 100644 rider-generator/assets/themes/railscasts/images/file-64x64.png create mode 100644 rider-generator/src/config.rs create mode 100644 rider-generator/src/images.rs create mode 100644 rider-generator/src/main.rs create mode 100644 rider-generator/src/themes.rs create mode 100644 rider-generator/src/write_bytes_to.rs create mode 100644 rider-lexers/Cargo.toml rename src/lexer/mod.rs => rider-lexers/src/lib.rs (90%) rename {src/lexer => rider-lexers/src}/plain.rs (83%) rename {src/lexer => rider-lexers/src}/rust_lang.rs (91%) create mode 100644 rider-themes/Cargo.toml rename {src/themes => rider-themes/src}/caret_color.rs (79%) create mode 100644 rider-themes/src/code_highlighting_color.rs rename {src/themes => rider-themes/src}/diff_color.rs (67%) create mode 100644 rider-themes/src/images.rs create mode 100644 rider-themes/src/lib.rs rename {src/themes => rider-themes/src}/predef/default.rs (68%) rename {src/themes => rider-themes/src}/predef/mod.rs (100%) rename {src/themes => rider-themes/src}/predef/railscasts.rs (85%) create mode 100644 rider-themes/src/serde_color.rs create mode 100644 rider-themes/src/theme.rs create mode 100644 rider-themes/src/theme_config.rs create mode 100755 scripts/test.sh delete mode 100644 src/app/app_state.rs delete mode 100644 src/app/application.rs delete mode 100644 src/app/caret_manager.rs delete mode 100644 src/config/config.rs delete mode 100644 src/config/creator.rs delete mode 100644 src/config/directories.rs delete mode 100644 src/config/editor_config.rs delete mode 100644 src/renderer/renderer.rs delete mode 100644 src/tests.rs delete mode 100644 src/themes/code_highlighting_color.rs delete mode 100644 src/themes/config_creator.rs delete mode 100644 src/themes/mod.rs delete mode 100644 src/themes/serde_color.rs delete mode 100644 src/themes/theme.rs delete mode 100644 src/themes/theme_config.rs rename {assets/examples => test_files}/example.txt (100%) rename {assets/examples => test_files}/test.rs (100%) diff --git a/.circleci/config.yml b/.circleci/config.yml index 39d48c1..c9d759f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -9,6 +9,7 @@ jobs: environment: CODECOV_TOKEN: "e58da505-19f2-481c-8068-e845cb36fbe4" TZ: "/usr/share/zoneinfo/Europe/Paris" + rider-config: "1" steps: - checkout @@ -31,15 +32,23 @@ jobs: command: | rustup run nightly rustc --version --verbose rustup run nightly cargo --version --verbose - rustup run nightly cargo build + rustup run nightly cargo build --all + mkdir -p ~/.local/bin + cp $(pwd)/target/debug/rider-* $HOME/.local/bin + export XDG_BIN_HOME=$HOME/.local/bin + - run: + name: Run rider-generator + command: | + export XDG_RUNTIME_DIR=$(pwd) + export XDG_BIN_HOME=$HOME/.local/bin + rustup run nightly cargo run -p rider-generator - run: name: Test and code coverage command: | - RUSTFLAGS="--cfg procmacro2_semver_exempt" cargo install cargo-tarpaulin - Xvfb :5 -screen 0 800x600x24 +extension GLX +render -noreset & - export DISPLAY=:5 + RUSTFLAGS="--cfg procmacro2_semver_exempt" cargo install cargo-tarpaulin || echo 0 export XDG_RUNTIME_DIR=$(pwd) - rustup run nightly cargo tarpaulin --ciserver circle-ci --out Xml + export XDG_BIN_HOME=$HOME/.local/bin + rustup run nightly cargo tarpaulin --all --ciserver circle-ci --out Xml - run: name: Upload Coverage command: | diff --git a/Cargo.lock b/Cargo.lock index 1f75337..d4a8fc4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,3 +1,5 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. [[package]] name = "aho-corasick" version = "0.6.9" @@ -413,13 +415,24 @@ dependencies = [ [[package]] name = "rider" version = "0.1.0" +dependencies = [ + "rider-config 0.1.0", + "sdl2 0.31.0 (registry+https://github.com/rust-lang/crates.io-index)", + "uuid 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rider-config" +version = "0.1.0" dependencies = [ "dirs 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "plex 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", + "rider-lexers 0.1.0", + "rider-themes 0.1.0", "sdl2 0.31.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.84 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.84 (registry+https://github.com/rust-lang/crates.io-index)", @@ -427,6 +440,60 @@ dependencies = [ "simplelog 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "rider-editor" +version = "0.1.0" +dependencies = [ + "dirs 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", + "rider-config 0.1.0", + "rider-lexers 0.1.0", + "rider-themes 0.1.0", + "sdl2 0.31.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.84 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.84 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.34 (registry+https://github.com/rust-lang/crates.io-index)", + "simplelog 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rider-generator" +version = "0.1.0" +dependencies = [ + "dirs 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", + "rider-config 0.1.0", + "rider-themes 0.1.0", + "serde 1.0.84 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.84 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.34 (registry+https://github.com/rust-lang/crates.io-index)", + "simplelog 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", + "uuid 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rider-lexers" +version = "0.1.0" +dependencies = [ + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "plex 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "simplelog 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rider-themes" +version = "0.1.0" +dependencies = [ + "dirs 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "sdl2 0.31.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.84 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.84 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.34 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "rustc-demangle" version = "0.1.13" @@ -590,6 +657,14 @@ name = "utf8-ranges" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "uuid" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "vec_map" version = "0.6.0" @@ -710,6 +785,7 @@ dependencies = [ "checksum ucd-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "535c204ee4d8434478593480b8f86ab45ec9aae0e83c568ca81abf0fd0e88f86" "checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" "checksum utf8-ranges 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "796f7e48bef87609f7ade7e06495a87d5cd06c7866e6a5cbfceffc558a243737" +"checksum uuid 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "dab5c5526c5caa3d106653401a267fed923e7046f35895ffcb5ca42db64942e6" "checksum vec_map 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cac5efe5cb0fa14ec2f84f83c701c562ee63f6dcc680861b21d65c682adfb05f" "checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" "checksum winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "92c1eb33641e276cfa214a0522acad57be5c56b10cb348b3c5117db75f3ac4b0" diff --git a/Cargo.toml b/Cargo.toml index 6fd482d..24dafda 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,17 +4,25 @@ version = "0.1.0" authors = ["Adrian Wozniak "] edition = "2018" +[workspace] +members = [ + "rider-generator", + "rider-config", + "rider-themes", + "rider-lexers", + "rider-editor" +] +default-members = [ + "rider-generator", + "rider-config", + "rider-themes", + "rider-lexers", + "rider-editor" +] + [dependencies] -rand = "0.5" -plex = "*" -dirs = "*" -serde = "*" -serde_json = "*" -serde_derive = "*" -log = "*" -env_logger = "*" -simplelog = "*" -lazy_static = "*" +rider-config = { version = "*", path = "./rider-config" } +uuid = { version = "0.7", features = ["v4"] } [dependencies.sdl2] version = "0.31.0" diff --git a/assets/gear-64x64.png b/assets/gear-64x64.png deleted file mode 100644 index 0911c7a9c2d4dfc75608958cc4b492da1c21c667..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15812 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 diff --git a/assets/gear-64x64.raw b/assets/gear-64x64.raw deleted file mode 100644 index 21b478d2088c91f18fdf06bed975aa133fddfa6a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15343 zcmeHubzD?i*YFv-C8a|`x`v@!q@_C*hM{v9TBQU8=@g|y8l}@f8YHDlQW^z86hz(w zSMR;g``+jGeed`0_netMv(DaYt-aRTYscAVuk)$%MSxgMNmU7eKp=n$_yNwBSOgS( z9c%$WRh0|C0RR9Kpn+fl5D-!W6YVcp7lb(==$H9uAS?($0|>x>Q!qh6m>m2EnE?J^ z0@+^T{r*>0)zD^w@(Bv?i2+anL2(HIK?wmE6BH^TBqRZaUepS~xTLuFf&2|d18YPF zxh;YT{a5W+AdK}lnBo_Ffd@hRgU?40M*9WdU93ELbXfH{_hkE`G3;vPsRSyYX%@dhk%WTP6EgR-+xANda8X2zL5VkOtRDYUQ zAJokn2Xp^kp37plyS>>GHN#T!UUH_hG^bHBT!5nX4=c6W=2gh)(g+M=PC(no^ zY?Na=jXgR`MLQ>6Y|(0g<9(XhFj=Q}BvoVN#0SGDm~%Rj+8o0hQM~TBr`U<>)D*b| z^YR1-W($?4__f4yxwb&%;+r1@j43H*pF1J&xV-WY)9OXH2kc~|dY(FPk3?8YKWt;@ zD${1li{b$CqDF=fot_hKi_BQ0N7`m&XW7j>EX5ZRJjBBl>s<-bB8Pa&eLgg;%iKL% z$fwUtNm8objqVjn7j+?dCg-Y`QN74;#zmV=xFy&tI<+E^(kc*H&{P$(8be0SR-N-q zz_XDkeKW+L(D7grQJ?aRi!bv|3(2_1(?|jIcXL_B(FdAXz#_!NeSo)hWPl)!uIdE} zWle2N7lN~)h+Z|QW8t;7R{z4{$jKm}i0##^D+V&rhprLbIo)KXn`n5}v7h}|c_r9wnlP4MEzqVHY9e8n zFA`U7yyd-3J7DKQ-{Hi>rp|%|1j%ti{HpOfb#}Bp=anBwqwBpLp4r;lY0$A zDevmaVPRgd@y*X?NpOid?m_W}q%WJAxA9pE{AfNt@%P|N6gqh(YpTY9PaOD`y8`-= zTQNJt!wrVaAZq0%Guxk)Um7Cpc3BIb@B*$kIj~qvc>HLC>-7?B%|1i-(!rn*WQ+dTNlku(UF&!=| zgel$5!Liqy&twkn@hzMKdi8hGu70Kn*XT5%@6qU!#}FXsAQ=ue4@0w3B>(Mu|K0AJ zJcm>~74Vl6P>avQQ=#DrzLFWC$+umL=p$f|?7|HnnyB>o(TRckNAITb4=r~V+V?x1 z9eo7N=Wbf{s<xut&?|zmwyRX_RwOyTHF` zamY!_qd>x0`6i!vbLzg(9(_#xtbo+J9#db;*SsXMGG<=Jyc~E$&0ZVYpE}7Xpg7k# znf(~4O4V~rH_0o14de{@Gf}V-Yj}B3ylmBi9W<(}#Hb5Zi2;ya*8M0THfm?pNt)~W z=%jj0CIzwi)8h6XTga`ah#S=UVq79GXqE3K+QrFkGRq{HR4} zI^W7&wy?mwLN;q06c>Mm0bb_uK3>A#(>#S7Z{5O;J-o7Vl&oxFMrq!zxr&xD#E+y! zv;jB>AaeA24{LckUSG8@lOiPJUAy4Y{26zxKvCy?)9RuTg(AeWuJQB4b2s~jY-_*r zSxLT%o+sDa+S+rOzI0pGr5I26G?;pgT9}v*xVWcJ^FYbHdfM9PqC=EdEM&4*#9TG% zo=2j3_$v(FB<^7xJf{!|TMEWoz4s}Dv|HRNBglti#7uVDP@-~EO7!kzXwp~CF=g-k zj~nUCn&eeeX6V%2YTXE~PGjOXiy`9V1Zo_4L5$?p@?8C7)hZEOeeUulpAN?-R}%NS z*fdnhTfgqd@?rbPpY5#$95JLAW?!JHlO-qMAP{0q!z>wj%@*EWyf>!no^rfZ&pw<* znPW&7Xk(#etyFzwp&LiTCXHyiwo#o|y$J zL$d0IWFxx7^1?G&Z}~0A+sfvHcN<>~XVklTQ9)XMjdvo=(TG>EXesdwY9gTUP&sn@ zyeK?8P~FQ^SF|}PkvWv$928$TKOItFFV6GM*8n|HgYRk4K&fS6IW2j-$!bDO;fe_e4b3sdrO~E#IOxa=U~Mo&aTvoS3Q@#L5^nY5;d@ zT&KTV6#Ss|7HPjeWZLT?^IH}FQgw2tBihNcjOT`xL{Y%j^TX3jk@cP?Ib7|HzP3a~ zmTM^y1O-T(<(OlHmN|x9qnIulqTb#ce;#>Mn<&5`~2plmh@O6k*cu zj`?5t6{Nhhc*Ho7mlq_`DwaJ%I={i|&h*GIV&`6>)FG{CH zk+~<@U@Qb@N09**EZd+|NVj0^ID-ErwV$O$AYK%mvDN6!`Wknyc+Rbor53v+qu4J! zAp!TKD%LuDFr0nI$6T1!y_XFl$yf8jxzPueby*J`?s`m{qd8UMD4?I z@#xp7?(BAZ$l)%l_q(9Wp92rP`;4EB@qIpN4G~x{94(LM_P!Q=8@*U3i`pq7FA5h= z8M%w)wPBJT6ES_(X5Cpg6YmEN_=dp zLs{WOP!;q~h8AdedngTa-!zqo_L=0k>qRaPM%i5&rzj$Wr(Uqf=Lv=&XZ>B!!@(ve z#j7&JAamq@ojEGk!`TBuHdkYag=e~AIJJvtRo#`*OBM~KF)_ha!)0=? zARoJn@9BCfb;h^knrr8c-539GRS`WzoysgLSW7zA#bJYT#Mnuo7a6WDE<`wH74<2s zYtlScNk1zYXN5aZdEe!0CWh&~1usHsGv1=XX>A$j?=HQO)g5adNzvhShM43ydqpF= zl{=I=!=J2QMf8!Hn{nU&IlSb9r3*n|hI|ilVLvE0jd+XnV_>b%X7y9&$+)uD)qSGh zvHCGM2V4?R8Dvup(LXh{_x4Gqq(YvFXkpTS$n~nvp(hkh~#!Zbxt{}fV_$2T zdOvy=Xm7q3e}7rV>@0uN;z?;9rZ!q|5Vj+xr_AKdsyc0{1eQR5`6O^ZKmb5Xjly7I zZcafE6ougQ2&enxn3zZB`SEWvq90VKaiBjKgSEtnzlq0EIJWSoDYSe96P&v_qL!Nw zjw9T@^(4Sy(iV!0=@Tu^u`e{43429#>rvb3)XqI8rKileEH=_q@FF#!EiWZGpTa7H z2&W)N*=TX|i14O`uQllxc|W=|5`}~ZPgtRDIw43MY8tb{cEi-yn8vjUQD5(ZL36elfH=y3m-Zt1L2-*8RhKt@g=m&?({@B7&ZdCaw92!EY(JX z4>hj4%uF{m#WVNRd%xz}?QQbu@zq8)&EQfWQRU$9)NVgI=wfwkpDaiyQ-^D>DWm{8+;Bh)K}6naJ&>iDEw%efk# zUVMaj^XsjV*~~_!>eW5TaAZfu$_v11nEPV!8*>g2NXj!*2z3AO`)=X)y+Hm)jk@~M zZoBG)3bunva`wINj6I{;FmfG5bm7_>54T+3*q_sHVG@DKX7;i?)!k%7_jKm$iifC# zJ0>l7JHEcc9mn9UbPjI&4Ch5v-N`+|N3byU<@~e`iJiPvd)1IcA^0_QV9B=OMqY?qJeKMkibr0{ zX2v$0q_C;2H;X#UW<}wh)(UaLFS@6USmZ_~XMAHPmgN%5ZfM=;X8BT)Rrm2|%GZ0d z61E(R91$7TDslv-%3u1AxS}QlS0dK+esntON<=6b0wq*T$K*I)qBsXzERwxh_3Sx% zUu&c}`$B?Txhm`(@7S`$`ceNVlL@)eqMRpAFg)O-*=!}Jvo8MF~^YW$jfW)tEcCV*uFgn zy5o{P#!?cxWrG=0L^79|i4yXD^r^%mX6NpI&fb93pS*r1@PXMg5%S~p848kb zi290OVw&iVQI`GAz)>4~>T08l!joo*dJac6r0QtOM*1;Cyfn^9qV4dG^RTYY>3y&o zyt&YXcNkk2=e0B|8NnBp3m;?G>I=@e=WWSQ8{i^|3JOnYxbv01xc2i=W^IVlybzJn z&(O~mj*u|yAp7z&*A3H6WX6wC!beB4XU@Yck@4XP4>wd{GaNbjvU;GLx8n}WPwI+g zw#(sr#f+0DMgAb3+j+JnI;;{6XRK zT3FwXJ?b2Q@h$qMny~40mrBriQLjINP{VW-F)8YZ+UXK}lFVkU%6yJicRVYqc@hQF9NsyYBiUKbeXp{0g>|0vEQJLn6Ov z{OW?pL|@t&5jH_Z^2S#l&Fz}4UA zKG-JJKAgE8yGkbhBoX&+`sghi^r6xGykL4rUdfcXU&42d<6)0(9U~5o>0x1cJHdwZ zDuRfRz_8bEn@_&@&x{^@=NY$Z#8OcP;#%`hqA*4#b+pYaXDur4q%%%2C@<*^gOf># zPl>4s`nno=pgXera}NHkz9^H2cvz~^WlfQe!#K0?;fTTiuO#=JT4)Y2se8Agd37GX)YTaX% zR6Y8ahhj79)R0NmuBoBE`SBOV1QQf!yqjs+(+yn`z>khRlc4*rt z3!<`TbM_~76Pc(MyZV%Tk3B-9*VGV?e_90UrM&se4$iOKFK)yX%8nCbD+;=3t6)6- zP>GvkKX|c303Ut9RWpo-rNK4$5+sYiep->`GtMJbLKzXXI?OaDH%~Wf#uGJMJdVoA_8An-)d2ocDw(vyN-s_1WAz=4L0~3C;cbVrim9A^#@vo$2{Z>d7IWUN+X_*cn>!az*^ei{y z#+`=&XeeGu_s@W2r0_kg*#)AsU2laP?7+Oh+H zM3VjwRpf!=fywvJT5d(DH4TY#gmE-2^lzf~=1iW7%Lc>>0M~7dl$SVX{P0lEVYe8>S***WiR3YmIMN#=qw-dM zs8ilu_LQ1vWor0|LBp^x4!a8fReWj-h0@#R-L7g$(MwZwHVpS-;0h~@WV4l1Qp#l= z(xfK_KQ)YNiPr5(?5iDKbo0;Fp4PciOF!fCKgNfjOpUxN9FQYI+_BX3LO9+nh;L?1(b>)G(`Ff0i;Q6Ny4$MF zLNkq+$=%En;i}rvP>C#RmBmoGSk0)NBiSp)PG$6yVHK^9G9NdNnmWhdIQwQYXij(U zUFw_?l!!7qM_yOagGc;eE-1i@f}R|5&r1Y-{UO!@;}YtU2TGQPh-S_nI-Pt&jE+BSs3WXb2YV2G$DO$Jv#96d zT#lyECxzSdT2-nLU6^(W4J1qv{o2HI8ch51SuvekT#@1^A6bFxq<6GdwY;xf z7(JN?FiY9aUG%OEd7?a=CZ^)x-vFo-?tM&H)h)V+vYT zyN*Rz=3pd%w*K}jCCk{HdF~lo_ojBc-BM=$V9j_xJTt8-IJ=7r6P}XSf^YwW*U2|2 zllkWX+x*jjvPF_=9BRS52J9@HO|8^Z6-vPYyQLV3xvsV`shW4VbO#JK%B3GN9p~^M zy}gb6V@6G^;_bBvSjsVyyGF~herihlGVHu4oS|DBfa7(1eSEjo-hoEfNkyrFy z{Uf1taTB+QK4T*?|5VFG9>tU|a=g5ivD|U@BWG&@YzoI~W|4%a z>GsQndO8N|cm7{jwrgsm&A%z%s(oMhW%bLmCm*NOo+&=wKj=P=`mPe;j$zXvbqBBo zmj{2SV{pc=MOm%Evu9mh7g^Mt3}2Il4_n5oao!jx`+ZQ$#okdm(m-wa;NO#j;^bm0~#H=&mY3tf7aaibPn+BO?hng zz%aW;6t++9e-+%w;oM+Yi)4%^&;FdGO#P1Lde%!K#1~fCvX#ReK3)n}+;gD5_E{qd z$IBYVO*ELk;lm8>U2UN4nil=!MCl(dL@FcNCc1Se5F;UP21-pe92m2pk65l9Onvr< zz(ZmwLB%WmYkx@Ig?%7(Xed~e_oPkAaHp~j66@po*-%8tczX=hCYr&sSJ*(hJNp_= zF&~{_N8P0XzQzRCP5=0gt%NDd$?Rbz@0zb&WP3NOvq3^|4^yl>cE%V=8*7z;HPxYQ z!rDcNM`WF#YVIn#Wb~{mJWIxX&W6BUDJ8S;)e9Zi)565fp4(OCBy>%ZO2kR^TIuyc zL|;O@A?7iEjIA21LP{h3wB#sv1#a-IpEQni$(?ZZ^py~DesJYL;h*S>R$}Ixq#dhr zfBM>3#nOXz`ntn7R{O_q@WU|$gJTL%-M2ztyDi6DQK`=Qq+B;7NmN_|q2;Wk}ZX=`@>P<;EgWA%5l30@gSKIR`w45}h8HlPMX zkQMf*ciG)EuT#iP?Gp039Rxgl&`@}k5@B@u-WV6!FBe+5a>cS4|;tPyq&uF}k(T3$0VIoL=u z8wqO&Xt*mN>>X76kqAA1O?|k(6I|SeSyqNg%2&eI#oYygvSRXead!2T@Rer1WG(^1 z7uo#GOqVDqCuwG5@IZ}%8xp|;52TH)gzinHUmU1)%&;C@+Y?>*?o;vhwA1^Zpj&yKGIk>qpU2wFrcJo3> zGlTVC3I>+I6@9^sU#u_gFByBHZ1`2d8yKhvh`> z(S9y2e`0&06utkd=O3{>_5Iut{JIEFH!ma{q3DfpMX~&@%-z|`^Y3+ec_J>dE@gGL zvE>KFylk=G_!sAt{=~m9n4N=*`z7`T=5H(;_@6lLUP$N5avL~5!WrQLR_+OM6#Nqx zr27;3AH~053R*p}&VK$-&F>xzFF&I?nHy>43Pn4A_9C5)1?pD9)wU7>=%UU`AU7ia*HXt7{D-c!GR)7}<7ZBlvS;574#o?kL zA3-5oK>>s)+!hLFX1Xwbto)OEdp@;_SkufBCD#EjPH;g->5%B5Zzjmm5}g{2<8x@3H=SiJrZi zkL&+|`ZvyRT6rYO#|`PEh19ZkM8Hx1hWdYE{ifFiM-opI(ogl@iTy7KfB2LNNa2R` z`&E}7!s8d@?7(!{u_UbE7v_*=_O$Xw*f9Ufgxg!W+9ANf3UuN>&>a53r^Kv;;i9&p z!n{J(a40VfCT7Dc4z&h<#KnZ*A|e7}wnC!6={?w#mid^M~9QeHH8`f2nlQktI|wd_d33-QC#%f&5F)OWJ=# z_$%|5yZl?CzsfIZ6x`hXz^-AB((-Zr=c@h(#wFb?2ROpj)9oLH`&$|4*YH35_b-9| zY%Li>&|}~f|7PacYhiHeI$@n0Co*#X47gnpUoUzlAM{B1!0jSJ2a7vP_h zjttX(uK!rzKNk3p1^#1!|5)HZ7Wn_q0)IWWB3!}cpAYy@dKnmjj)r#e1uqQni-~g) zVt|QNJL6ZNJK(}k57D+n1qa+f`WqJ3MJK5aw<}C3i8Xq2y~DJ z0}~e$6PKJ2pAg)e`d?m`ff0ZlAcqbCV*-8$M$m#$9v2Y=zk(vbKO9_iENn1Z0*o$z z5(Aep5twLLU@*kR^+#|42>^yokX|Dbl-IE$XJ&y4-S7ye(7kHC57JOxMk8Dz|Na4q zzycZ)uokeY-?8 z#457Q;aQ@^UtF-J#u!fa{qCWwVnFc19YJs11P`;>bZC`PLC@VWhc8cWJX3Kw%I7X;+J&D1h>>Q%~`hln(rJ#tUAM!Dz;nQ8L{$_^j*1yG|du20p z3P4ulP(jo%lUPsvEgOo6X`|OHUmW&7+bQGa6&W)=)8uDNc=p8ETTOl^Uef<0lU{6L z=f3gjy6;;@^HKw@35Sj&Hj2I=^{MYJ1&zA*DMI)2Tb$!7WRl*cf~MiuIW`t5OPHl9SRcDrYqZjc-QHD^3xtTne?rmaStu%|Eg^U!|F*I}u&Yq7!6#@vYRF`>Ci z#F6l6mKB7Dom!8V<5&Sah(|vWkdX+8&&D4&OD&kin0Q~Eo7yaCcTwahG0HY$+uwV* z8=E8DoP4EO=~1am(Z}7ZUWmuE35Y0|@>SC23c{mTsX|+g+w}Kj*0^-Ost%9Idq}oZ zBz0*{3R>p8h8S^%vkvpwKj7pqh%KnyidaY7w>Ztoh$fb2ZM@E^+UWi9;5KJ54@jrMYMFU5oRrwd@04 zHQh+oYZgR4vNbnrmL7dE$Q`Czs`E-8vrjB5*I()^8q09@-M_00UA)nKmlIc;Q##f& zItXQpqCj?6K67p@C(Zosg+&Br)J>TMAV#7Mn&|{2ye%Hcv`use-89cOWI+M z*I}t&uMsYw!^Pjn*BO2OAi1$T;4oW0Uq<1kZ?be=z3!Ldw{2?l_iDYQPK$;aUAxhl z0GMWeCfcKx)aHV2<(0Eu+Z!XJU-;r?o_nk%B;ttE(B(Z(R@%|O{^q5~LO_{68%)%w zC|6kq@IW*X#+@8J;AYq|IU2c#9cyKX=$!W2?w`W`89ZVX;cd?Zqib<60PDee-iNn*OZVCigYDBmUyh?;iOg#TmW6 zZ(|zy$G>`~yQ)MDkd}vOm5vzT9!eG7rx{$XP9Zfb zL6n|J7_i}+yxG@M7>L`c)|F8iaifh8b*U|>v8?jBPOFB@n#Idk^WtO`*l>SIS2o36 zGLnr7-h$cncp_%5@6O`h==I>@G>fipm81v7EP5p%KO)oNiO{J}9M`**6~fMZ>}6#*@^I&*s{1?&czT@fj95>N8Hmu4qsW2Y&Rp ztJK<9UfS_+%hEAe^O36QFj7jJ{7dKwKSp*u%F*2kSyrKKzEshCldD73tBcE%O`mh1 zQOfrf%VW0+247v$M2c0<=v9(@(enN{S4_9e#Sb_>-t(i(1GW_qoHD+f6ZzND3F05{ z9MNUf_bJa@**yL-oQC;5z$DG>N=v~RO=-#qU(0zg z_`G@;w@QeDbfr&pvaO{KMM!?A?Q_E2P3lN0X1%4bD#;a2obFFW2Bqp^k$x$m9~}j^ zj=VC06IC&iSj)^RZap_BGH^_sFzg|50VO18U64vU)|N1>q|t1O zu#6l(getcReJ{GwfuQq({;c6ZHElsplmXqP;R08lfPr+Aldhbtb*$7*(`;YxU w%?vO!b8J*QjI7oOmWFlY^+)77c(9SYPt%I;)|UG|Qf4&<3C@2yk9|JFy4d0|-htNF$Qcok}-S(j{FAB7z_WzkL9` z_ded|^S;0L-}hOt_pJG@HEU+pteL&fK6}pj%=t2as~{sU13(}UKo0x^&R6KUrM+z| z0YF}!6+i_500|(6AOR4NQveT&D_#TSSs>6090BBcAP4{^nCgKCFUS*uDQE=n1rN~d zh2F1JR$f_+mY0)nC)<>nFP=BMT573Jd-<>iIf3PHRm0Y5?h#v_0=LP2ZG z-~qj=9U0`2|Hczt@o+r|!lj)}kVm-UZ_$AnkWerF&j0QMcr(Eo&*y>L04g#v3NjKZ z3JMAu8Y(&_Ar>YE1|}Ik0S+M*ISn-xIVB}611B>r9XmZGC5zB?c5WVietsHe5iw!j zo1A?7yl^8BG&D3!3`|liEK=TUl-GFwuj#xEz(a+%2@0VD5bz*SJji($Km)cD2^=x_ zpj{CJ0g8x(jDm`WjsYsv;sT(jpa_UiBqT&cQ2QY$2N3a)@UQVmArq*ZpwPJx@&?2{ zN2R}2*-oS}cEG@A>KcfKPE0~dM$X8@%yONTUqDbuSVZ*p9cdX^Ie7)myZ5xTb#(R2 z%q=XftZi)F+&w(KynTFw9tVd!c^VoP7oU)rl>96uHRnZcUVcGg(aWmpn%cVhhQ_9j z&aUpB-oE~U@wXF`Q`0lEb1SQB>l>R}+dI3T4nH3qe>pk*dIs+oyq~}5vSa_zFFdec z2#APKL=livQ1OE!+ zCwSK`diMXWV}bvldiJ|xfAwn)z=T4;!Gq!f62RA=VJz7Q|2W1*YqkXp^=>GpCD*}v z8KU`}zm`0ww_5Sye6%k*t&BAsklRVV)5L1H$f;;#>)+DPRuh2wW5y^K8BA z;%~*aBOiL?5R%0o<7h5~iEyig#06QV0~+D1>axq`$c1g~YuG(tNB(@mGaI)oY8_#5 z@Mi20b%2W7gjurf+?V3A1)sCL?kH6 zS>7izM`X&WWyc6Fn=G-sP1YYJ==KaHZ3vxurxnI;pMtHhK;0&U-W#2U61PQ0^gMS_ z3iD8Zsq7m@4er8oOQ3AI?RzdwVxs;*H^eMDr{vwNV!{14R^m7N(rnnqo|uX~ZKv)j zR-?@cV*zr)#zv3q%5mQd&KahJTBc`ZTFpIuiNVKngpMZMzXnq!gt$o_9O>0&?EP4J zL6wmhFH^M_(a)D68P1rwM_kkl_Cpkm#1H7W=%A)g*!C2u`+T!l7Nh{Dm$Os ztpPh_C(swm_Hf#wE-{~#GsCzUZ&ENVlp8vs9(F=(+7seArPTC-49=g=YKYmvu~l~e_wme)2kOd@uG^mT{B2mRO^Lc zbcxQc=X;7bRxVUscC?I&^vD2AszJhGMe5@}@2K3zV950$-%Ry&xgN)NmM@{Fz=DD6-@#VOyU8Y< z73ku`@0yO?!bYI~X$>0l>`-2W305)3k`oXNHkNz}Z*bM{*0fw>l*rQlrocBT>Ce+KEsBv{4r^G6 zz4if7Oie$;4vD7L?O!Ex>SA215$|Gm|b|I0S3VcCennzV(&lwzQcY* z%3f;rYKqtJV00!ZB-Z;zdQifB#{#M+{E)1?Z7=e$l*RF>H;;~oW-*S8J}h;9>bAG_ z;xSmbW703@unOZb%==!0No5%ae$r7~By zS=h46#3-?_9$P_H2e(3Xf1X{PwnBiC^8#{RrqY@)3JcH3r$dyYZV8pKtlPuSU(P2s z$)nHd%RNt=F(=QQ>@B-=x4vljdE)%4DN7+x*E~}z zN!u!RSohg_{GF=)ne|D}9=cFhNixqi z${5j;mv2u8zunLx@HQQ$AT=?iOI;<(v_VEDJK4W?6CVK*IWuS`@W#k!(UwHj!_Q6N zP3PK!YB|GsD3syQ8K4P9Xq*;I$=6@3ef2T)s8H9Xp?h#k1Cg9hGa# zL*4thO7FGB?IRwR6A1>d1fZ`!{Fsj4D`Juk^I{p(mzdQOE!(*%^k6zD{>$|VSmWgu+!NLTXQ`r(N0ZZQar-@t z%JPIQUp_@~qIgOD*x&FwrcTt#f~zVLB*daxSP0XO(x>M%8G80`+?uUTW9d-LI+}Mk zKorYWLq^D4tNcPwIf;PIQaW;a>$?h5)b*PZMUv*m0c9MS42bEjLxB3e9P|}nGNzwe z{`gpoLP`Hyr&YzsI`3^S%CZP;*ErnR5J=_70p%K7x+uWr)v2b4d1xo4J@B?~ZplQ8 zpsGGWn=-B>?}vn^)Gnl>tm$xQ`-|3`Vo$$2Ox35sIMe_EcO9961pQw16JE0*NkZ$K zFm!aVyB9}o!Opa3#z?F^EGBPpHZa#(gnh{S9yCswGp*pwOQXCJ3c?uO_1L;xYh>M- zOavGj!Ddx?cc9q!xDd-x7hfnw?FNb-odvWVmg09#9R zx35!}*mG4ge8I3036mTuRjd!!|OlgMNNPh;M2=L;YER1;Nf zEE?ZV%0WMB86u$XyDJ-bC(AwQoJ)viDsXr_&g{@ILlhb?Q^6dQ!xQK_@9O{!0f!tP zy;2Ji*I8;$UZ1=r)^;js?zlt~bsq@aS&zgOnCpqWu2w)H?<@-~TGkRnLIUp^E)E+* z!ifhMZjRq>{@`43d~W55G7$50T^bswNUEP1peh#SV6#m!renw5?;4^g!iP0s6815; zXWAf2=5A&J>KdEB>?emW8Hjohmprh@^f?NKXVt{*zdH1XR&{N-#7BfsY9SG#?iY;h zm3<&lAN^?h`pE#kfj--#pQ9^Y$Qlp}q`6m83p?SLlRfT6NRFVBoF7H_3$$^32eHelZpT+MKXB z)bBgv;?AnVM;|4_=N`VE{QS(-TTRwU@0BbR20*i^3bV6IIfP^yCdB=ZozH;>QLUbL76hX938Z*s>l#Gl_;9_afgYABus|PA@Ha<_w8bFbJ~8wq&oc_==UKQjrtmyA z1XZAOH`UK(+LG5bazLms+d5BoF8DR6Qh58fnGX-`WYXx+=*`7Q%?cEN_MF6o7epq3 z*r>VLvf9f#$5?j^y-o2yOZiYHAmX_Xik@%arF{YO=*Toc3tU6O+qavDf?@5j&Uf!yW z2OKFgIn2#AHpbBP)p@pZ?)5i%^?9qgHqN1u9g}9GveKkT6m$2S{F)yC_lR)JWV zA{)OBSPG%WNdy&)psvfRI~(N)ws5!GD;?tD=io6l&Vty4t`Pp1bu4>_Nrb2?1$^tqfH`m)N+=U?rknp^@lRdkI-*Ms>q(WEH#p&nP}I{Oy>IF+=bFa*+yF1 zWH6osuygRS&uC6qrSbD)3=4YNf$X2Afl<>+HN;wYw*y+q{EOaeZRZ3^#vseL5ruoK z>g$+a$LF8fZCh4f)h`I?HoYA!P|-W1O)oh%J?9-YwJI4`d`tCKFa77z%-YT48E?;> zGXB*l*D=9S)dE{!rsS3Hn4|l&|Jsu+&F|f|8lq2Rw16T~+7m+5&tcbxn++2@88od~ z`dgKg?Y$u|N7hnnTVqT5C?B%##o~dtnw8}i46HM$UpGG76U^EOnIqW<)1^~y`%w5o zOhsg~q3QjL#?1HadOE3w@9jKXoY0rCLZ7bpQa!SvTt7R5NYX_Ts!K_!d{Rt#K4#f* z4)jJRxJ)F*_DTfMBnoD%(qYHud>@dDvY20ZbddF4GG%y7nh=#GC$GLPb>Aa!i8)E@ zTVW8xi9`WEq3e3`aQ2}jKlvx# zIe@3z6@9ItN@gxh=jXonLgo!5Eru_^G=&s9+ij(DAgT4qamiV0KKDC1w>Zf6)*tSW z7h3LLFpBizoDqw&hVI#_fuFkCDZ|jksKd(5TB`BB|jXu^>$p-zmH>uA4~kOAEghC35k8WEzdv4lKnzL6Kv=G=%bRe z+CuU7C1(4DG}C93dT8kHU({z#w~O9SZ`Rn|UEQRXT6h+9^T7M-3zwjsZE075dE3C% zcef8Vf(JfWyPpI6oXg%xx{R8=FGVRm$hJ};Wc=#VNJ!MQRzC;ECbk^=b;8}+*nKD6r+(vU7#C<&RH8;!0pAiCj- zhNLcp+%F=*DR@Yp-#oD@?k;NhSkxln$unb1OOx&4uIh(qy|ql?KH{Y-7^RGfxeknx zN7z2I7*)BUao!Yjg6Jq(G4XG#MlU8Q{MJ$6JC7Su>r)4$xsTNuk*@}v*j`_U$dZxL zhuoZ|@v{=o@OLqD&ou0=Da9;m5S-SKA|peI@gb_#^sbxJZ#0=+t&7oH>?I#z)lUfJ z1Xu?foQKQnxb`qwW3rgU3EnMb6uoz|C5x0z zMmdq%cwL(;IgT!4{B+otg<&-?fl3pMZ0A)`-%xtm3c;`3<7OGLpp0zZN~CcZFS45 zNzUcC`&D}Nv(B)BIhqaSS(I$fHBylXyz!jNDFv7>dlWv45^yh$lopZ2y5JJARL3C^sHcEE3(h5uIqF={}D8K0! zGK~|tBu%PUN5XSkWRPiwo|%z>#mNdC%qb!Sj-BMJ*|UsSW7_VK?Q#4u&tpY-9Y2uC zqXe~oG{i2>U$8!_oyu@;wyH~f;j)kA>M=9I?wkC?{gsr#tFG%`*eY&C=1ENAqDb>N zsL3Hddsl{*Z9NRXMF2ni0$NLvlzQEX(@z|8(;0iLs^!-2BJY`bJmoQgmE1=uaFjOH zDCQORFfA#d4eH~EjHny7K0P`W_-8Df>WY)g)jhDKQg?w$C+B)pT^Ul3`wHC!^fiuTDx~iGkXO=o>szCg*dNoMh!K~Lsyz=& zF9M&`l;r36Q421<=`~#sE0zXy0S%p{Y`SyKot*dyNoZN0 z-PeUkR@~h-$SZ0qjRFdN3K0}ng13k;`C`@`J&Wik^sLG$Hf6K=H_8k+KTSl(u=XYj z-{4!SEYeL`Xy&mri<9qXiVF+`r#Aw4U^ICayXDLETqBHmeRJMt4PTlC5todD_7Ji) zp0Y|se^s`6c z;SnZTG6R5U{N3;Ju7|dVx?c}eoeGkw>*E%%qRE@7+9LKBbkjs6{Gzx4CUb4smFsgp zT>LV#8}E8wiw%(wbdkO2k&&luF_9wU#mCu`gS}-{WL;%b(aSYo^R3pAbk>kzmDdf{Ef1tqK4jy4X)T(w zGwnpxXIa^JpLGSYH{1J7!H4GQM?G!NQXAJXKDH<_D(rg`hom&OM6&VVch-ZxjxWR$ zee2a67!b-S3e96tJL#`47m*>7-^V+3od3YwMUGHyqts@gPrS(Z_{ph$V?Afh6KJoFHRZapRV;G+1W;2co$|Mc-^ zt<{($InA8^QSUiGa}K=MG@r;w#_Pd7i%)$a4T#I?V!CKNwj=eRnpC>BCkwk#dkgeUeGIiq zB3m0%t=)Z*AogU$> z;rpE7$GERA!AFB*Lde)4Va&iMg^0LotOK2hcylRQB_4#+FBJ9r5q855JDs<}V`nMP z0spaRqbKPEMsiDJqSG-;_H3J&QhfXQ}!O#E#-JStq#sa z8fS}LLUPDsgL`zUiMJwGD)=I_7zu%u*4|h_s)2(;8M=$U*+5+-SA`)9a~iv>ba} z8TCVKRGLJD%1(8|;Edr=0AZ`+3km(ig2D42jJ-Q*omMLuFNUip2hB2)D+986Sdq*U zbDA-%zjN4m$7j&}JY-x<^DAD)t3oB?$*D)lMBPzM`X)!h^Tui=QgorGed1>I5E|tn z^{o=Ir?e;8?5>`k+P;zFdb4_O+?>C%5#l9#wlOSYs{qW~Y>#kI2Q4^S=Dv^V&Xa_$ z*UL|qnTIwyynG%gOqDwyC`;v%=e8PYx%tLE_`Q^QTAkipVF)AQI_xR19_LS>bA;V9 zXD8Tn2ghz@{Js>i8j6A3;qWD2Ix(^YKNG}iKa?8wo3>#3~P zvEw4j&CzQDRA%3(uNBzDtu71O6(`U>p>-ku!ipyHp?Aj3NP>W&3R-BRRUO&Dfo$yR zD)RM_!SPS(7RsHUa+*!S5WMFuui!5lwR(BcGdVm*I>ROPCAJxcU#j$o=OXZ~4a z&C4QseGDt@m>~$>eq_bK^ER_rlB_reFz+)K1o^Gh-BtbYP$QcS*F$-FVk=WudFD9z zEdi>(Kgb4oL&l_9?A%bB;yx{x;IPx>L;xTLzuutnD?FhRY1+AoWhO8ZYWh0zdqYj4 zXu`NLRWTlt3>w+OHCKs)s*qOHZ_Fc zCnn(E4kFX>VZUCBP?-sejK#nlu40~L^m2XTcUEa_KN&+TnB?M?RuM=exKq@Qb_M&q z#d{00F^l(XIfNe8h4ZC|=sG*zPcvD;8a$r=55~R_0RdKn==ae($78}_MU`& zm3!ihXkLHQ7_bCy4=%fN?+2GENtH7DzG+o$XhBng*@o1NIZQW-NJd5{;I04{iII`f z8^#jisiepNqWZYFrxTcvB!l>XIPZRjWp|*AP@_I{C8b{V`HWTcF_YXmpxmWV{*=G- zXZ5X*=K%ZujLS|RKT^-w?e}MozVK{kU*D$Q2&IW3%sPmdB^x4V%6x@w@tHxQcuEaMo*K}8O_geT>gDAZB4tn7r+PJ}EXD%c-n`UP zwxP-74X3|$ICJ3g1l<)`hF7G_x90ne2mJ5wZR&HErQ9gu)165zVZsBfKkEyyXzowA zw+p4S@8{Ly@6ETG5iLffTaooBgI{BU_f7x$9a|A?rk(!NGL8*ztI*C~2K#%l0e!Sl zQYh&Y?rO;E)C@^B?Ng=>GVFp|n3W5}%r_$D<;^n1ofph8on;a;@?KY{^QYy-?eyKR zG{B>5ydi@dU#FT<2gCjx=m{~1yj)wA8Te$z1}O;Loo};&-};H6iWM1$D5k6h5^{le z4n)3j-Uvnd-tlTta*t-OO_Z)Y?xd)xvr)atYvi6d8)`BAoZ zLR&kPDdd{#uEvPxvbnsb-Hp8-@jG)Wt}cPt>qgfn=H;r>It0xN;+_3#wq%8y5^t^Y zq6ZQSVe`b1t~8^s73Kt7Tou5ToBTD^q<`gcA#;*^aC}AB7hxEazN^ep`|>(A-zdzR z7O%e4=83`troyJ8jk;A`nvFU;rw~0%@@AlD*xLJw(T^)rHF7m<^Tl>{#e&0G{2Qhb zJwtC8oUYMgt86g;c%Ie7f@t;jMdU`TStQwF6wzJt_z!G)OKa^-&hHBE-?y#$sz1dc zPQyv}eT7q-QP*{%Fmz~ao)`%+MImDVctzIlX?fq*Ghr+ z+3BVIb~Z7yvv8+1 zwXm{r6r=mt+)77lV=hLgEuhS;?0nn8+D6XT)k4!(<*u2potcO^orE~;O>a?e2WJNh zcN1D~2YW|1QExH23*(|74~MzvXfIUU?ZoJGz!z$6JGolW@^kWYa&v$$@Objlfi_&t zEk!l%$Xr%M;o|1y;^pN4H8|XS9NkU4IUL>Se_6O= z;b!J)73P+}(S4FXrD;rR>0Ztmt>@?eG^>vboRyV-m;6`%IXL{*c5|2Z{HvaS)pon<<7~mDVd3WF;c8|f?P=lY zPXDViXL}E~zt`pAW&wvT+G=lZ$ptp^V#I#w!{1Z-tq*sYm5qb*g*IICmzKHNZyje3 zSNjXOxfz#*y@dlOtarrx9bnse5&7I6_%tbF) z0TEtHQ&STj4jvH`GY)L5m^WaH@JeTCk&aj?*IH-Wo|M}S{YkWWaEn@@;Wn4eGJN<+)S)eQ_nxGDI{ zV|29UW}-5n9TPApY#dCiEVyi)EvzouE_&P5!o=Ol^{$hXy%^o^H_qT8eHE{wN={}b z@R+{Pv@pM#F1JjqxImWczia)cMAO>I%klrA^e>%XWu;u*y_{U_R9#g~Z7s~)|4ZpV zwSJY?09O(>cUK?z|JLk(C|t&r99Y81)#s`%O$(PR&fbRhVq%G!n8Dp4M(1YYX<<%x z1)5o#I9geNixn8emufcu5>vt^0%k&%LINCore?ex{QSb^93s4?;7LT7&rDE|TiB9M z=vR3+CrfuP6ITlr&T;%z`$mWyb({!7ox+5&708~o)!Zn}#xfiFIn?S{`) zaGLzpCHO=Zm50ZGriZh$y^V$IU;Vr&`>zUrfmh-3Ukm*uzbJFt$=L^-8rJTrUXK4> z)gKxc<&d6N#>2+I zz$M1TBOoLqBErNWAtfdx#U~^pyx1541Ck0nAB2?J6X6G;9fvjPk$gZ&jN#s8I}=ziUtBK<3W z#UV9YI{pCy0O3P3i$7oyn(T-V#(%&7!b^69?Ei$37w+djg8e570JhRHI-ex}6Gz+Q zaQ#mV9M`xO4#)ene+V!VhF<>x0=C_^{>3tIBYL#X?N4nwx)-(Kf5OOjT1xhf{}ab% zbkQ4c`A-zgBg@r)82nEhTKJL;@n41jfGp@uSsDL_y3fGliaFR72yGZ5t7gcIUs>l- z4C1l+t-7wqWPcHUzY)+m+3bSZmw8Ag!$fSnpJ3+_=tYlbFI4y$BjpxdmM^8ySR4HI zD@_ogvf|qYgTJe&80pF+S)7%8iqKlkg*wX9h^NY?7KgBl95lsY+)46+1y7K>Ny54! z4C}E<&j5t_QcJZ90i=PLjLH4WFC}hs+zRxSwu>x|lzU42B@5{mjH!bj$P347 z`iq%MYKC0Rg_&^fp$SjeMH%n}CDyJjS6sWuLXx>OZ%BeHO2RQ5&_*hG2>>`g*O&qu z+TZ}N$9z;(^!LechoqrXBncNmUMGiUsR26zYl{4}$dEN~4!hyS7^c!DtHbt?7!%3Y*m z3ieY~zY(lSt~hX5uo>~}WPX7Vp}Z-*%JU#NO-O67ltOnnF`-82>_&r^g4x+$p{a?4U}(=65>phF9%LwJiHcmp#h#C zPGo74z^{c81h8UWMQlpK#gpi}6$7Ui?7EXhu%(3v9EDI3MwSc1IXKD7dyPV#N{FX^ zy37NR8NJ*2oJz%q)s$IP*4IoHy28qDH z92Q|tsmCu=kp2iMb1l1M&?vd&3e&H%Tn1TZR@e654Fr%!4SPJ*Ko~uy!WIUma3WDo z(Lvy6y`gs%KtlLm`4&JNy{UFZ5=^1vZsOFM%OD5L5B3ZjfGK&ul-_4J04E0X7Hgn8 zQP)KJ^dgV2SSN3?WRpAR>1p2QqHtDTre1Z8LfoK5hVhLFQfmks7Y zg3Kw0I^qK2xU+XtyDktu*=;uQY|w%Ri<^-o_(s60l+u|u$lyq=^Ly5U5`>|=7%&fc zo}gR~11<<(LC<^%2xL~!Fw5s_hKLlhLEz4S>7^eHSV;! z9tHq3=(J6|U<#7GMb>3-KuZe4(F;KY;h3)#1^{??N^{^${gUSEBMG7a;E4w{1dVd! z4TE`zA$rFs9EH4RXVu9)fx%g&DLS~n1qqJuGTXp)&jQH~3<<0;p}SZ9gNEV>8~~8M zR}mkLLED*Xo)?!DoZP0QV8Lv#D1g%W`MA^^WDuMb-Cxh$Mg(5Cexmq8?|p z!v=spn&%PUB*B4yP|*YfOI;($xYWR6cpeGY;|jmg2DPh1 zV9gN>0FLnpjA#KQn4$)l!81|N14}_`eh^xLnjj%y=p|%>slDtrpM)d;NEY8E1WZ11bs71=Ii_{Xnp~yqG}@lgip3HGpEkejD6G{lf#iN+3QRb_U;bdK#MzP(9i16dwf*yM4|prv=L(EnCWi z30O=gsS(~3`~gGwyvPeDECkT5@qq?W8_Pie>y04jS21&zQ2=sAI}Anviu%`?DfoJY zu*b&?15*Gfu`@^j6Ex!{b{GI?bcfvq5ED{BT}1OKZqUQ~X$oN7RfN`{4{)t`L5~)G ze{V+(7J+!q#)Cyb(yA>5l>o$4!yZX+`L62R1)z_VLqV0ivixiSNFeJ2>+Ybn22f1S z*hgBxOv3jayCJX)#5*On4NSqsFtIocOpp)nBTE7RYigSU0JRnhkpv){^vEsC09LF} z7bxa2V+0_v64JVxU{i1(dDVg-fN)w38iiBPKm_P3fUNU~1MYdYxyd{Lvdg6P00eou z9!9_M}@Np6L~O6 zaC%u*;=_P5rJ*A@2ohwQK6p9-K*|1rX~hcWHHktA!36C(=NVjj@D#mestn{n3&x6Z zFt9IZ%H-gha_f%O@C6L|WssBwfUq-;NrLk;2%Mkr#vz_1!F_?8dM`K}j3PK$M33e_ zqy@t=dStV<4FF=}h0`TL9IU4A>D&$gU{nodgE`qeQh5I%j%08##4zkN+%F&@eBfh& z0RS%DBoSCaW{Nvl1|-NfooD4T0HG%d$TSAy+eq(2t0aiCyXu}g`g%g3T zKHQ<(EOvU{V1yqF=5E7bXzV6EQ;RX!kd}x(V=%ttzy6$RfJ+cllNJTlz`9dP?qtIO z0Pu?*)pBhEMa~+;O<-yj`87)y-b5VTLe7wEu##~-xO?%`6)wU8;0=ChX)6u`5EV3$ zVW0pc;sADd$fFO}HcEnC>Ps_8P?42G;cMw7-`%lm14WA%L-0rk2%noyf(b||aZ~v^ z0A%#hBLUb2001M&$M9HzOlev?g?kzDQILMu*}&wnI9$PEaC~?iOi_mYu5`%ND1$i*&Y&CFpa!D2=H&$iCo^fjdr&e%xbwH1Uj zAiYU)NoiXL8OT9lINGnipapZ@0%rpB^K6Opg&F|#B|pz-gYzzQu1&R67bx9aD{k{%>sCo{Ed)PH7qMwC9e>QYyPwQ zAdj5ji%8=u@1W$+7q=@Io$M z$m}`Fw#ye8hOXMs2G|u2AJ)*YlB>@Z2=|nqMI>tq!+;wYrx!tfp+5kZyC z(XS;6jG^0XyxrlKH6m{t$+$9i8qiBe$c=e?US>jUWcgJK1e@^F<$4F8RHSJ>EhdmS4krX#g7j~u;MG2oZ- zjKQY)U;ev5Y0wvH^RDU!fithn>XuWV=xe_xCg3-iG?aN|;a7t57@^j}x9wN(y^{wO|Fv3v8{GC47A2^E zjs0*h?cbqGUdy6%_YV|JigVx(5d3MJf!?q`V9ZZv_#?D`z0H7}!cXU`lby)uFTrO*ky1(ORb%;n2D8g`{+HzMDfR8Y zCXqCwy{)UIIj`~G75*DfgOmLa5f1o#of{YY-6rsNhrnNpLO{8A(vAc^dPjx;DBOg! zl6d$8L~1;^B*fPk_yh!v>G;7X@2H>_1PS50kLHUGsmO5}jJw5$BjCQZQ41Xvj3p$L zvB;?uW=;74hF7>s>?=xmo^4!6R;Ox#KzTJT#O4Io2 zwm^x=#Lte;=}g_eT$~CEnqixB-?+3%IvQuy4T-MMoPIYbUhQ877LJ zYU?hT)i*Jbt*x-_Z0=;oQOPL3d{cnu ziva$nCG~U1b|yCp&y|L;s*e=(%i7uNvAN7^vO79-#{q89N?bjU@t(PBY$}rB-8VP0;Kkqt=541=4njH<#E>F+a>o3-bef_2& zvB7+%dNYKg*6|U=M3IxS-;rROm#pCrf;wT3(<#4_d4jB*K_8#dgSUXywto3oJ8h^4 zlKIb=orj3K8~IdGLR;5$R5+W%Ka5Wm5S<0}$&oQ-`I@%9+Z5d;b^O?X!B9SJGg+}g z_cKuEd%?RqmSrc&Sk3F#5*za+WBrxKv2Nr#jCI?6vv$*+*L*hq2tEq~_wQ03Zui~aRuU6ZUZYCK!sEddO)l`sAC~)ien!9TWJYJ*v>cBby zJFu{|TtZAKn!nRgbK3#)R@d`GkT>hj&GQG4A@-)?f(-eT6mu`{_nVI9%w)I&#%r#0dr&VT1lCFd!-oGaTz zxGTF_OW?X1gMGhZ2G?2jwVAIpk9Z~M>%By28l(zVLhbu|%D;SvhSU~ypF-M{g^#`L zO`%Ka5++3NdRZTjobZB zP&boX-T0EBzP&cHTDB}@Z@Ob6>StnK#5R$G>tC$c9>0)x)Tavf!P>p|%z#62VR`JB z%WEuBagq1qu3<~dqM*zzhG5#boKti0rzKP@)zU37FvhHS?cCetLZ_@MMB$F}0umal zVn{z9Ha0RL=k4Rlr6xDW#1{G0i@i~7rAR44+!z?~d`tS()OkO4__1ufMy`CNFv}g| zF@O9w8lmPW$LR6LwN`b56;w)kQ(GiKc4 zo_m1AhSn92YhvH^n2$tsi!4!pNtU&brW0H zhXFU9ptU`o*-qCT$uIjBzNe9xxYbu|kgt_NsK@i{ffxCiJ!H{Fsc}uae_blXP;t-Z z3m#*d+Zu%P6{PS8A3%zGMQ&*Cf#}75Hw4Ni8-~c7%BIVFS+>4{$G+ z^^4mT*^8dmXw*bXtWjwexfRpItBdhmDLh6-q9Wwm@1|VRU8mSpwffQ7D2F)NNZl#% zt_64d_vGvH-H)GZisdj2s|6j?5yom-SyVJUD+*D`?5*8K<(QD{T!U-`}AwR^Omp*SCZ-{ zNEC#2QbnGY$5~EZPHh%UkW~O?CGE3$_>OhHML?NBs88UP--BruX^|}b-AncZW z9bdH438St;k=3K`Bv>_GbK0Myl@86wwCwym_ZBdSqPrVqT zZ|i;}lwio`&>483$e>ar_U*3K3$BjbZ}H|q$GyVsh~8h(H{Z3PM6b0moTx%JzL$O4 zhac=y zkY53J`nUM@=1*dyH*#8^RaprpZ8$4G4lNyc{6X4N3cJ24VFhiuR^%~TQ#=U)$4wy- zDgnE=rs)oxwuR17N`jub*Fs3G6H&_xs3KYSJ_a+FVs)mQ=y>1sa)0@gABMH2Bxx!{ z=y|Y~SFoN!y_fssFm+|&>wP4vC{c0Q75ddDLzbP01cJ)_&$Jb^?qwsy(TB?T;hPvl5`M+dj@@v{jS`^@#Wqy)g)xHZJE%div8xmTJy5_8l7I2 z$T+Xx_T0Z8HB{Z+w$aIZadLC~ zzWKoDnM+$hGym@wANE1HuE}9>a*^-xIJ*9vgF5!Id*p&{ttUZ>t;alON(_q&}-X_g&V&pfTarQ7QVcT^wCkoOLMLK*QkALtHE zBVAHjLvLTKC)JlJln$~w-6BTzNd_E5)t}v7!?H?taU><9`TA+wL_Dswz3KVct=Alu zT|b;*RrVyoLq-VnRDLf%$eCUv&+V;Eq1EPLo5IHjt%vdNaP!kzM*o4 zCzNiZqI+y4rG4wox`dTtNeun9g4V3cE!8HZsmraQR}uR@*hw#Wk4KpK=l1MO8Q#Q2 zFU#XTax%BwXjuBqDHA&jn<7z>*>MhohG6XBmo!CcZ&H)}Grob-hJ z^Aed2%r6IhT)Kyf@02IVCNQY`Kg;ozrH9vl%$iipi^`Im_ltMM$2d=4m>ZcMS}$H$ ztdtGpgRr$z4w^^nhFDG})OmYtCK0g@f{iMva4~z$K9z-s7O&P13n-vib{wvfJd#K+ zaY)dKqyAu`B0@Op7_A$lUZ22z9bJrgAbr3?T5($4+%aP|U8MQ8de_E>?4TlKh5ZTE z_f~$HIAN^5Ap#g!a*wp$7NT(VOV5P^GXdnw?C(t+viiiz%Ssf)FY$)@Jd*2j^ezOT z(Xfh5-2YsY^{H042X4l~=tEfRK%`%Q7I?)R>^^Z`-N19lwXh)BGEFrI7pvQ; zXO;*J0cWUh!em+7gx=SFiF28;>quH=qv)%gc9%MTk7tJ&;$6_hmaAsGK#2hS+qL}V zrTz21SijMk^kE@a{&$=avU{?#gcJ>utLC=eVS8R)zAx$!Pb~y9PPk}SfvMC7RXbM9 zg>-u$GWT>W)5FLVQCwreuIEV|@sqr%%#aPtJ}F!#`FV(2q1rnBs5K47&VJk5aL%ejz8hzE5zkT$V>$OLjPJAlfN_PW_n=Du9t0tHs=>-gk%O^jZXj&7L z>AuXJuhFk+-n@^p>cUK)IE`m(c;XZwGdZ+q#GmzEs&LzC9L{;AJ7o6)=e@H=UweF} z*UFjb*-PHjjlLVsv_}PCI%dMFvhyj0;o(Ga)#DP)Dn``D;c~?j1eG$^RU2Bb+Z4X? z&ZQabAY)QbzAqBzvgTP|u(TnSJUB!E|C0Nv^CCvkrx~jS`)JqI(v5eDv3_7JcD&4X z7kEHJ$UnYa#&Ip5%jB4%%R49{d!ADStb`pq{&k$R;uqSxqzN4n)pLnmoAz7;Eon2p zGrHsOfz8mqSL|fJ;GWFn)_T>56g3C&^Dk}LPPUc5z0_@#SyoZ`*~9&D%Hw6t4gq5t4ojlpqCF!wF4|0EmMk)cXbXbghq|iA9omPkctLjh z5h!t_7XA_`<2oWTE`7(j-U!-yp;j7&TEV|T8rAdZE=-8Wdz>6|`ip`3NsWGAs`mU8 zO>D$X!1E4P zZ&q|<#Ha7Jx*_%Q1XWXpSZt^yuBSK_!ncU@>?;LhHB9;D2)6S}-xnfr4&YD8WLDYk z;?|ViCius{JXa|bEY~O4e?)4@!Wp>DO5G#?fLah;&sWCj&(jX&WgG0fgK%yA2*9cI* z+3x%vt*!+pJfw&Vy!K=pV~&)inI8NNl8rM}auA+U&d5E6q2vsr`QZD_OWKjDe}Qmm z5gAjO$$;Xt80UpJ5zZ^$OOe%s-yqkCrJq+C5iyg3Q>gCax?<4<;42|tkzOs-`-4pw_#dThr;h~SpV@z#{7 zB{N=rI+z_5yvVC0^}4cvq6XnxtA49`hn;(dkKP`JnX;B{KFA>p_B) z$C+^aMNzK+(XsmvGN1!+F#opvz6VBcTm1mqYRUuvp$uKE{Q(Rj0x~i@;sXR&;L0*^ z*%T9mMZwA@Dy9ODO{r?+h(k_A%`X1TKb}KE&DaT8-h2#$(!GzLEZgTRRoa-r^0$i)bb}W4^k=<>^!f&lw~z zN|fp>cn8a}zKcA+>Dvff>kC<4AuQzU+j@j}EvQqRN4d|-Acmawem~Xl=nGTXq(XPk zPi@_yZ4P(t5gPgI>_UAyEQJQ*l@>Gn!r^z%chtuQdvy%KsF9Q;+x9mw1Myu?u^cqW zx{8PjI(1&D%AB|^e;DU{`zgi`Gc}5_`6$)v>E-TQs>~!6CnaPxYs(|@>)#-|7c}yp zPV7^!s%s823Fx-+f3Xd#s#6c1C^snwsps024m+qNl z;r7Q8D1FKZpU75B`%tL8+ICP=2fKlVBK^+O)TYGQNy(2L@`fjhQErg{bYePv(3O|a zX(7VHV&DE4VRi1L?B# z$6b@E12kcm!16~LB$`jE--J26Q0)9j`*zma*5cW_%}*RxHKX})XPM?}N54F7fb|&K z$-e#uovjv$z8;FnA}&~pfhgn*#xB+hkM<^12K@$c^J5jblVPU?fB0e3y&c0#osrOP zR1h-7_gHu$b3ZC{-aB)gZq~--Ng-9BMQ=(3LvJ@?b}@vHxrO&*gBtM<=SQ$c1tp~Q z0p}Woeogf|6g$!mGWGg-A(m=9z6}H(Q5v7V#Kl_nKa+~uud17C&?6n$0~PS`)qoK- z+4gjFV&0oxiN}y>v25U-7HcRJJasYA?$T$&VY`I&t!HHH8^T%(T-swlWR)cB@1Afn z05=c}SGf^tuUVN2%v?#zy=ItyGAFH*hM-j*sNJc$FvPN%a&!uj=BjCA$bBK}_ev_a zr2Zt6hlS894HZHv&{_g7Et-&FI7{aP+cnD`D@W-ZYXMR8>mGA5v6wvaNoDF~h^c*D zUN<3Y67L>`L|e~Q@MY%}=Z7df=UNztccbC^=&QwoLO6@ zH<&nj2_Z#=#)&UKcwH$=5wwH|X5kE7rw)mc@Av*_+_&)#@YNR<6R9G5Z_r#bo}2PD zmO-BVte9Uk=2tFz2IdDNdOoo|meTLSO|?2;d`1>0B|=D2#8}y|*A-93ct4$;k`1gt zuR=R^EMF}+5>%D3Q!w+gD7N)Tp>z;QzhrL=@dEqB9x3`th1_o~)G(V#Q`fRoN0e$Lw1L!g*y zO@$(%IaMJxX0j7q$%s$QSpSQb@wE*MiAfS^#YP8iVg>!8$xx(Vw8GLyd^C7TC)myp zZfgT{BS)1yZZ;;J%YsYe+(yBW<`0ZYRTcikm~Uix=w}ZZ%f?maSjU}A8*gxOD*4`* z5>m}q7=w+CAq?-R2(XAWrTOK1o2*JJ62_wK!>y@#Jn?G|p5jCkTo%vZm2V{4r3f=? z%Uq9TqVujBRM6V!GpLH4DDQHPa_!LIT~WoLunm}GCR87}RHv-Whv4a~ep7ROt>RZx zDn%4BWl|Pwrq+wbL(}iO-2Ocf=b+vRZK%3e16F(V+98RJ_@?$JzbK(P-1g&fvHtj` zqVLq#_2iv`@t9w1o_}V1d>)9UKFO^f;|2;Vn%Ra$RvGY75;DeobG9s{jP)_lDf&aN z8U~&7Ja4!BaOG(rN7T9yZiGJu=DNXS|X815n_?3ncj>e=;bnTvRBp#S`Iu*P7T zuxH*IR}#T;eLkXTLQI9%n^cypbb7F{b|yqWta@gd9Kn6qz<)D5N$p@%K)4j^hWGIK zM@!+~AmeODCpyxo0tCXuq3w5*^Dj83Yq23;dqL4MqToq36;_^bbRIH?InByuIn`+J zbXPA{kkn_;sesyK4~bGiH=a?7!eUU%3xNbrDgw0JL@FK6>vW%2a?{gAxdh`JJBEyr zFz;T`Y;9u*+D7loEe7Qze_=*x5;t<)ALXQ<4#~(BINL+feco7TAf;U8SI$)6Fk`0} zY6p{DiSPd0r#&ZnZ{;OWdqy6@j}&t*O7izEd@Q)zA@j)2^1@p0K$1mw;)>TsH?%4G*2gV4O~j zi0r{`*C(Jwcp$66$>F%F$K2h$ibPYalY?nWNdCq}`kl_Pd~ehBrfO{F9vG#|hHrCK z3vP5_cqLeTswfS!t4BQP;5-2*``AWj7`CkiA&nBUmC9OPS|xBGg=HCLl3)S0!#5I7WlzSdh(bg+5F z3g=2I+dDKkj{p3n;BfBx$Y!P3sm<$&V?_F3ad~s%?=iTp?>d&@C94Av)2EI!Y<61|Q6~)An z61rJ9%!;X-TGr@McwN6VmsmDV##nGL2aEcVC`wJ;peVt7|A2h0Z(hI?EW(_5m?h_e zz2?8#D`yz+g)7J!k==cHM2jK532pFe9iq5Kh=Vj5!3N07R zo}gfcXpq{{$#GoR;u2?X7$}O^K$`y@{j>Q+u~R}+9mb3vDKu2NXjG$zx~D>-*9ge8 zYx@GS8iIQiFF(*7cBzl|k#b+vL!|7Pvgk+b2zl_#xT!3P%Lr<9MclBYhTpJ=m`G86 zr$WJSI1x-_PtXp^>4qnftDbr^@TImYx%A8H!2+gU8tGbxNx|&zRyfCNWPHCtM8+dZ zzsU4C%0lppv)ab()e3PBnDcVR8`tNsxOtLua=pcuopE&wRjS&FzbZ(gjoS$jW9atB zB%B$UsFd6d?B2RekQ$?Ad*UvPM`Gs<>YIM!zrZ6d#0yS~mM z$SrdG<`gM1`GQmuvp!V2aaX^%7N5h+lrGWa=a8=tKTef^!FXu`&vB#=uURxzFnFNe zF}Op$B1_#4i`eH%<)*B6zkzH!ldBK1(whk^P#*(IkKs7UKeRnFzpC+I%uah>SNZI` zYAdP?gHx+XfM7s5CWnn#^>CUm>ZnR6Tx6!t^((Mg(aMAKL3v9zvA_1_Q zmA^qGC54B$zNd|-xGMMyuP?p4G%k8-etdMut<7JH#z}gbbt0W|97AfA`60#*JQ`+| zZ&f5*9)(b0kD5NcUSLn#y4U{opd*A+&dc%WS>)5}t7C0h8(Wj~oRq1Bu1wbll_Be2-O`DaW%>r+b9`hEPyJ~Fz_t($n@%Ge`oVJF+e zwpQeja`FYj*&agKKy_8uj$wi0iD}l&9(JvEN$_U`q6{Z1ip?Go?7R(Y;tAE0frWPC z)wJT+&gI42g{C=y<-!ybXcoLeuF|rEUrX~AHGjAdP6dYv>qNI8_Mw9 zm@(->Sz2m&?ul_RIWrM)!w&(Bm}IO41Bb35ZR_%wB}wA}CSEiGEttd~yuoE^8CI3r z2ydC$b%s}&TZY5S3hepIHe)_5x=_Ba#{~NXc}4Zs)in_)8iH!e*B|ntg*qL^kvzWi z={{+3{|09) z>{N;^_Z=U?kE^Qfpp&<|;Yu6A*VCk~l`cA}s8E@qzDaC+lLf1xY0z0P8K^hZebS@$ zT$jCcvcPZVu)!$;b?CU;|8l10v$un^F&k>{ceVa^Z3p=kA=5UIRX)r`)x!?8llsXS zP2;@Cib!Z}5^uKlBWCSi^|?UQ9wsLDPiF1VKkG+iBbj z$yX}J4)v*_19M4Vn2ymr%%M}@_*IaLG2*K&=WLK~v{a z_OwwoOQtuVNQgd@n<5KsN(z-zp7jg60a)6dt$(H>$Ct;J8Oxp_DbK>hOvO2(Jo%y@ z`CvNxU2&K*B)-G$Y`eQP;5*TbH4*=Suugm(u*R|PnZauAbMS}6p*ZIsU-Jt}x7(B; zF^bykmB2$)B<`?aLf2GOG5nKfXlOQmN#^>tvMVk~uf5voeL6l1U;UbUV!(V^Ktd!f zz;-zDgZIK@erlC6;-+)#?oF^&j z!xqpEl&+tVguv~{7v{xA6-db|koIV^XoZpTO9uSd#X&WtcrOVjC-#m)s%n3Fe>ByI z@0a;O?dZ%Zy}8j_7gkN&reK*wWjsXco5yG8$FG@^AP0>Rts)^V^~^mK)uUBY{BoF` zRr&cPjiylzS0mRRo4?4Y-!jjK5wu-dDzqmq@Evm_46G3`lhS+sG5GT{N%i+0n}#SuZ!h;Qbj(ff$HL^&DetbBcEK$*YBwf6{Brg_-C_$a@vlS1)L#Pv``AI0 zmNdZ2-501|&qn(ebmksu{ss*Ix1SZ^(Jzp1zM`BN7hLyTyCg3D3h{60fn?NU=3DR= zhRa?H`lv=_C&pDOpHv3Z62?kys_^XaKT;XE=wnAbsfwxiIFEDSN~HmKoKx<|u+j^b z`C2{Nj>sSLKPZ=(tX|c`NhZP@dEC`5w<8gfy6R^F0dL>SgO#+` z^b5T0=42U7oKr4M>XEVUHf?58t!-R-?5e-4gl6Ps@?^rR81h(JF^E2n5q1n)G!Xfg zuzKi4G$X8OH-zi#`)E#w`WwcKzMleh*#IJq^StcajwPJ&lLADE=lK_4D<=4TTX?D+ z4|`biv#J#z!Y7!HQ5`)#l^hB(UaLh^q`Nblv_5o_utcWAtJe!G`@B3vNzJQjJLkr+QxpC<5r(_XA<>0(6?NdYSMTBZrXYDtlEn8{ArTR& zB;WhSRb+Zp{5XrX*Ph4On}h^HfGdVkOUbAWSoO6eyDlf{g&Wa_Az?#=YrU$?KEQ3z z9%D9Kr%Rds;iu(F!oPe;GN>_H2HC1rK7O0Sw07B)Ih6+1_vbZv>~0euVXg563w}({ zeduBExau$YHpm8Rx0K?B21$AddwDU9yR;By^^we2mecD?D*lCjIj19srCe9%^@*r( zndR5L@W`4=t~_vdv);HO9%Z_HVr>eZVI&&{-D1QGi|XpXxCI~WVfW1jM6Q(kdeVT~buc&EJKh+L$(`3I(p}qKy||oFOqlOQEZqG0 zgx?(biixP7=+^y&c9uE$kC|&qRZ{m0B;e5s4mUUZZZOqjHy$SUO-kd&^nwAN@A+j$ zTYc=0<(W40YAv@eq!3&-Mcf9l5+xbt%EdZcUO9~Q zHM4L9R)_C^Xdnk_`#y-(MkR^IG2cob!HVYfW=KpVG}XbVo0fhCjj27OIOex6icGMc z6h$mxSzYqm>`dIIl2Vgf;!P$whtT1f^3j=kNeeyn&uEt%>&qJp4&zPFN<5PMF1es3 zLBee3sxUTbFV=B@kG3Vf7!|qM|u+;7m+9UD&K><|d7gMnV9fbQWKscKm)BT{re;*=dfPqboj z>EX+ZT@)eC5IzLF?DwM@jwia}hBKhM(N#WQE>)#j(bd5y0n2qEdloS4Xsp~eGQ ze#NYU7fI0nb%oWb?G<51eVl^A#Z%EEJcB;NyJg86@hUI2 z>94C3hroHq+S1{yE@a&fVi9r8Yq>}e2gZ)X<>lx=O?>3cqgK5aE-#nn#Pc;OXlt@* zK)zQRJjuF4q@VpLU^g#ut$I?LeC_fJ$M?*{cxhkXXbok%WF^(J<+3Rbo4Co3HU|v{ zXzJ46LL56b|5&zuFK(XS=GX=-?pZ8huOqQp$!?@qmNAjI!>^1O{9)buYqv#4VxfN2 z3#6-S&#p)*p2Qa^owW8`nfoV7PYWo5I~N>Iyb~XMR$PQFJu}Kr=Z=t?dc)-An(km# zT9p+x{B=goTD>pYhYrlF(9@6KMq8dRC4|{wGf0S1M+6gLt+K`VH5PAawa6Hym2c$K z%}DCmqASLb?{h2f6~VM7h3vRa?;PZ!uS~C6PQ}ouX*I7x+fh^o;55`xzX?hGasc7M zx}|hj3U(0_Ei3&$&$9j(KTLf#mfTEa;9>SL;)>07Uvyiwe1yjW!Z9R$zQlzhVjQR z<~B(@VGxMjJ$weHo7F>2r&alMWavC0g}xNa`^J4&#m47{nTXyJsay>8550%ySfh$t zFwONVe8`aSSTKLDHx)b#BfYSI82yd^h2p=p+$;|a@%~%jpdr!lBt{t zA)1kKm1Uc`ic~IyV)1DtwRKi^PkOSEtK-j)^>rxhWHH&(?5a&gczF5gqT4?ECh|EN#OE1=lVEgcGt6BKx#5TLvP-NCC^@SqJW8+PkrMf{y{_j+gMq5QBWGe5X zgH^)IiY~eNTWY*KnQ71GpU+o18|C{&lvkE-<8Lq)QQL`?+OW$%!M93bwWE7nR0N_G-Z~Z6e4@?~beaB7(7A6Ritys{tn!%jNyw*iTG4Wn%**I=QYL6MN~i zHjL%0T?fpUx{;MJw!B6@o}Zeb3OIf*U2WHvia{v6GV&o4Mrj*f359fDs z-?JUyWcV)aNk!Vo%i3P zhX`qoM5>DscZUu6k4y}ryLtPZQVA04k}5Ug-PpL8OI*(!hxQU4R1tjj;oX{)ZrzK? za-q7YNq6P}K1Q*91PS9qj18y*B1&4*3x4PJinbSh zfH*Vlq#xkWd1?4t%6A$|gClTt%4 zIhH2tmdK+Kb0K#+dhL@B5^9D8WtxPYr|%~wWRiP-^yJZ~d8xkQ9Z|M?iEz_M_%QXe zD$DBLv$#qHs`12ad`z_x`Kh=a;ZX*5)4}Zl1jm3nT6O(51uCcMlNy2WbZC3Q0oiQC zFW93S3 zb4t%nvrL=bp~VR;PGpme%bdTGt8}uBC+KFIQar^eiDiH3w#>HvrRWtk{-HEOuTdkv z3zmDOx2K$^TI(Z@_0ePX>~FDjU=Cl39&7f%oeZO`axGe^8Oz~yn^-F(#3BLRuhoT) zQ6yOp1+iaNvy8TnfFZlIyYdkIX#7Y1gBZW#}A zqwnb?{RYt}uh!rix$=c|m&fuX6cJoE5)ydy2o*o?UOvjnjN~TRx;`Z<+wx{rnJhpv z3a*bR)JYdSrOwOoP1%1xzg$l`%2MSM_i6Xb_)clx%xEGeUs zgZ1oN;o5BT@g)NXH>w8fho_xAA^aX%j56H#axTM{U*BaVYKgar`^wE{$S24v9G=c| z!yX1*&s_4JEIZ>2rcB#x%p>rZVwno08WpbfHx~njVq@5?x6;|Br%IMqf&XJCOOg{^ zU{0imxR?q!qI&&5){jRD+S)#nv>fMwf81fE)k(QZ+a3a|gr=8c035B1qfPW53cXX6 zd{p9$$}z4VVPCO%&8oe;k$3sLE*3*wq0I`W`LY*A{W;n^?|0e9g=s}ikA!)DdTI9W zG#dF`=cy$7#0Z*Bs8PRe9JzVGGgfM0K93x@mHqP@hT*~ejcXVYieoop!tQ5dd+5y)9LJ=-$F$;T znh{9OPt~r3mkydj)ej=hSzeH~%E{IK_{GO#GgO~WKs{K@ zCd5|GtPren>NST)~vL&k066!pS$IvnLp^b3@HFiQ#!P5UAoD(Y}sVag5 z5nE-8lJ$s3xh#G%y&UyZtwFaoIgiH&;o1;tR8<|P*;EsLo8a`pSkJn61B+g1@yE_h zg+c2dmh})|yVSAg%E)|9h{r^=^A{^DF_G6!9&oEavP2J0BkC>h z{gfscTdMT2ll0SO6b2k(InyGZw>s}>w=dn~Q)f=g%T%7I$_H=E*JblL3CxA_13UHk zz2ZH@w||4+DLd>rg!fhZ>2f&HKlOj`iX4WJU+CCfaU}R}JF#?Is=(5e`zj-JB)#Nn zw#!tR&sTY}UEV~kcW(9e0d5M_1aRdv_aVm07D={{(F|NBm9!)Q&!whMl1Ks$j1pU( ztiev162ZDP!uNqlf*2Z0pQ)hHpLxYfU9l;>oH$(zieOJwNU(>@vp5n;@n^I|(eDJO zeBx}dU|Zf07n*clWi!r`F`3VoO?kTRAM+_G4z}hb1to3Jc-!mgBW8aI-3EPgESTh{?!vjJTP^XK+!OMzAk~U5cpRNxyD!3B_)xKN8>I zy4n*<*cijQ4txLjr4C-kM!&F>;?e1qCDG7PfdN`($Ecd$4cv+8Gn>eEnZ^1*(`cbL zpPpJrIgZ0F*4A<=PtCSiT#?{+s5ys}NZRN6Y%k$cy6jB2zVoa|`68H2Klxb2-6DUA zk%f@SqpHD`S@LP?XxYgtcY}h&nd1aocu$G&iCGWJ+AFr)FOgyGKRGDOWc1Tes>Fr2 zTkMX+S?wv$Y^?{LBfwVFsJ<=RoxCx7eqAObK3JY;yI$g79hQ4J*P_#jHyg{>Kg+~l z&yjVKF^;i_1~ssFII1CZqU=2@Cf-jU3a|E7!4>ClEJ_x8ani+(Rv}t~{63}YbUP-V zr}o0ig?*lG_n6w8Cce~-Yl194aZmITVW8DSgPgivPA>L`znWo7!(_**m`2q4m1=X{ z$%`JmA0rQ^A~8SnU48#Twfx>7`T_kS%HeCAW%MGM^x(BOZyiqTsu~XC8IXx#HDI1- zj#3&okb80P)0=WM<+_Ngg`lSqHrBpR_7qb|qbw zk|4qMJe2!(y4Afq+Kp^EdtSP2?s6boP;AtSiR34vWJ#`RSZi}M*%p7bM=UD2c9~n+ z_~VVLq!nS667yc_(e7J51oB=`=5s#!OnvZ+r7s3))pN%!+8*qM(tX-Re03+Y;t!8b z3p~5XqX?B;`bw?gvua;`iGE-tU&zU>bN=lDk@i3iPM;ZW+pZlXaCliI>qb1id3C(i z#fxSh&C|@&gP=xRI1nQkS|i(K?HIJ6&4~5(H>fAPrwH^Qx8j=Lv%pv(KE5Yd(p|9Z zfX^NWu~B zNiQ9x0!naRT8bbuV2o#bI2Sfqr2C|My1TRwZ+6Mx(kw&MUrx>SY4F|z;;^Bo&ciOu z9(?%cXjYYc?)^7rg-lzj8rv_zmMC{Evzg|(YAsg~g7j31=t40ICK`VVpJef|+IIg2 z!D?pq%hK~#7i@aq(8*hRywzjxMxjU)#eJN}g4=s&?bXoWI&G7azPxtB5$sr!pR zax8MTCr4C)^3zS?fP207FM=Wl;htO^kP#EsOhJlYwNOjODvW(U(r-|I{BYTGsaoT~ zVnf|i!e?NSq|)7QaWpp*Nqo{{&A=x$N{mF73MVqf22!V}QLY&9A2P*?tC%PZhCcQF z*`Gxbo_JNiglze|v-+#kBOU*y6wu7Tas;kFj%k30nytR|%GnF8N!q8aX6h2%AE!@% z`zMkSk8*az#^NmxEV~O&m7CvBMb%)P_o-G<4y$qQA%l)Bh*%iMU*HX?S6>c@~w8)vF{`pjZ1iv_7SXMm!hmwN$9@ZMYQ2s zYnmdHI`ND>eDS!QGj9AWce2-7MkdYpM_CC;o2;z_$VX*Q%-4pFS~#p1)jU%NWx?I1 zQtb&LF!ga!Ve&%MyW3IVuEY=`beO}4@=xUl+=+m6L}(@zsPnl zvcq9R2x&cJW`=2^l$^~Lb&!fOfBaDIO}${bGje=iYpP2^y#?$W58@{YAfk3wtR!7n z)5l#1lHthmXFref8NC;pJS)iE{oMm%Fm~aiI2bX4qQ$RFM`+3A#eaRX4-$)_Bm&+h z4q%m}4W2NZKD1;*Vk9FU<;SGeaYJ8^>Godzj2LX|4_DUq?AmBOHFb6{SAz;Jq)86` zs`Ljj25{iXt6pEx8HDNDgK_BK>=epNAur|KHuJB4{gbL|&Ng_I%hD-WcbRfI4Z(k` zm=WaT9L71$kd(m;v108C@hk-cNIoyKd zeY8oh@I9Er9W%^|F;s$77dd>;GvP3_b`b1dL_aTx#o2eY+a9PG4kDq)QVgXXy`W`x z^Y^Da;;_9!Tty0(6<5{kAm$N12X?^Q(lH7l0XH0wUtn&^%#|x+^q0exhiM(VMnuV$ zf=ihDerD_3~7m0jkVz)dhR^?h?w%dCkygWDgpJkk_- zpeGxpgb^ySpG|in$C_G;=ALtM;-8%^0Y?ziMcm|z&w)*UljHpWe8L`hcDbPNjJdN- zB5+9TK+jl&H!B}KJH%V03S$`C*Vkg&c>D z+5^QP_cm#C)S|R!m82${!+wY&mpZX9xrB!6_{9CFS1)>ZI42kT7!L8ZND0t+LmV~v zxdmNN;2MkJB=Y%Jcs)~U!=#lXIukL13KQ_Lbke^Yn!XqnFw%qJAkLjp+%9L=)F6o=iC zEZP#Ihul_H7Sm2?QD>_s@MQBM9w+dzog_X_1yu>MDyEL9k~ugOu<^g5G)M6#&dk8+ z`0VErbr^op70P`Li><%^di`B>l**yg$BNBbrA3@7tRuGL4#YfyNd4);I)waFuPeeqme%QJk(F+)2OF*rqKEiNh8TtyS)wmi-W!QW_Rx@p z(*Oo1YSY-~nzrjjYuVU%i((D!n;>^R&d;4ZNkTL>$$YAVEUzCoVGy)3VTqryDyn|m zom}>RRdLOV&WQ6u8FH0<_#5=;5pLaBCphB)xeu;>J#0WIp|p+0RpbXuL#0L153} z(daC`)d+_|K8k_>SNU9vg8Q&QpO+pMsTjkvp6$8q$L5@eyoyiayB@76q2UU`KT?8x z_MBRv^DtSHJ6Bw-DMaRqTdj&P1W1UPjd)wZB2McnXH|g&7zDyrn5mv)`$3 z5dyFB1sBm49AR@HN}=(QD%a{+Rm^(QX>vx|+KovXBZFIiZp@4V-v>U_vE((DO^AoX z#M7DohC?sbv}F{VK11009zLWwO_1{?yQJj->jG|{)e4SBl8B8VzA4>q=38Q> zS%XcKf%3HT3p69OA?gu&XAK99!CZrq;sm|T`^Pj#ut|I{S{o0LgnsnGs#PNzoHOt5 zONufjm{AdSwkGaDv>3nPIVmKZaiPBu#?z=3V|Nw0#8i*s42g{LwS|3_Wzs&)Z$W1s zW*N6puv)0dUho5lB+3NoVNQo#vR~8y5_~-zr0EIZhO^% zEt7`PR|eM9@|H3tH09FM_#$eM{H>!N`}a-kFSL8WqV%k8QMiVhix z(p|C}g$nEV&MQIhd|8cpZipz=Y``Il3C$xKPsVEN%7Jf69&_z4uVUa7m0DDe54>H> zAb+%8#nX9q36WEVi@C`D#hA$p_cnhN!_dIV3s5Mu4{tVw>U|z z_I)d>Jp8M?HoaH`v(5tVf2+d57Hi;h+9(tDRQ85~o=dJr4}T7fe4 zZ%~A+(dA5tF5EaPMe3?g+oOnh4%ocrIG8&5pO}p3@MUNViGng?RMO$@n%~eDGl#GR zm4i()sf`wc@Yh&LkkbXR-4*yQEV=o^`MTL0e2K~B#IfS=MUtK=if6m)w6~UP+Pq4Y zFXa-7D}{fel}mwJC&qz9(>_neMjmTi7(#xWP>HWGCl)hYQ!`2W&}{d`vtz+@1Y*lx zB;+Bfe!_u^(ym61?MG#SEz-$0ygiD<4isD)EOH_;4nsDVf((RL--~@R2+?9n*h5Yi z77d!@vzjHi7*M$#7ZSfF%@r8c`>&4SJgg>caPa>88`Mg_l#pFK{atV8nHP2uC$gq= zeD*<9qZj-Hf3)t!n2wpSeoZ(b%`c#I{I-rxjSy){WrOfqxU4<^IR zpV=2Xu0=mb0A)@N4f%N%X_25W6ekeqTRVpFU@sI=%PNZO>Y4E|ZTfhX#Zka^E@pYT z!fe(SxR~en*(Qs)3Ye%B>%**9KT}!a6Kp`MsPY^lqvM3MVpnM#lgu$G&_4O)l!DsH ziT*Q?G0Uv3v<)BKj4nQgDHc&H={rh-bGkGfQ>?U0EOk&R{BICwv~}V)XuG6l33J77 z0CTcemZCf&0{e#Y&HJ&cJ=v&}Ho}pD(0G^>W>Hv-eF0)Gei$t2c)dgWfOy%sbeF;b zCs|7&Bqkqg9<9o4)SkELu_0VIc-{zY7OLTtc0G@M%!i}6iP?0N6By- zM=GBOOoWum3dLkrcO}CVoVHTqQ#%uCG|&l4@jpPNDp6&T*BYkR>X!MDpJHxamAN!) zKxOhp2CjF(dK(t8VapQ9P9;MQc$4y#@`Iq)FDT4sYQ7dAVO{w2mcV{jKykF1nxx9H z*HA)jthuo4QCJktC6A;`PiAjS79lOkv_^_q!f`r;Ibj%bBCg%r=CiVzbYs~+S>EFmk_0|mj9$)N_Qm_)GWtjOek;KR@(H^)@^k~&!+^t)(%g$&6+$5RV%0eE z4JW53e`uNs3YT#I5cr{A!plefO$cnLV{g2zz3oJl5u;V6Hw61xr|zNrh`1|-xfX2$ zUV@e-+G@p@mjS7!v>Ty26Wqee#J7i7gbRS&Uug-)kCkuk$`iSW2VW^ z7dm*<3%}a@$cGWyOyFPp_q1I*6}Hw^-PAUi476RR54J>1$p{jjcUp$+hF-sa_x$P2 zi-)21&0`CG*FsX!#NQ$A3^<79Yo80FJcD>`_$hXoqUC;mHGTPQ6qV)$vgDbN>;vkU zXwOPQPj+kEP|y0znG=m=?lF;9^d1m@Tym5i*kA7wLWB1yRz7>?UlS&ox_ z8?T$#l#id0B2I-)y!d`JAv>`vR#s)Qk%=wO$EFrkBq>eu5??G4cAD8~-Egv8W99T$ zHUlmh)cx2ii@cN$m7h4U5+%+3k{n~-PWnD=Uhm0G6(uYNU8sNf4O$izm))GybP_x? zounjL?{dCW7l50I)(lIXPmg_u@Wri&)k<*sCc^6L*wAG>#*JSd9p3|ypC1j`vCSvG z=)$mClu4PtUHspInJ#jEtuLb z5CkRv%Hkz&C3hOswO-gD~tRGrtp`2OThi%gQ0Qz zSLXkheMkIN2iJnB3;)3KZ)=Q`Qv{e$-ap|a3IYF7xrLGB2eksG)`n*P48VA5xH~@Nwjx<(s2E5D{{!O&|A_%;w@}|tED$i~E%sgo0)~KY z%K!oy$}0r6+r$q2{bK=Q7@+zuzSuntAfST}*nRg}rg&Jl^lgo|`%JMBD!Pp>DD*a^ zQ77IJ&;Wos&p8T$gG!;(&fjKIg75b%6e|Zrh$t}8KL`K_s2R9B3s9B2Sd|1AP_uXM z8LNV+(*W4L|K#BSO#^Jy^FYP`Y<{;eI0%>)Dgr71%t`{;9Su|sf$o$$TOfc7jU~V) z;A%gRQQuCv&=_UK=C#X0hl-xS_@!u6U~}45l80@(Q6)Xu1nFD1F;ZxIcVA{qH{<{GVrEk?~?kat7D}=klKoo1ifBlDc5fB>>fS*WH zC@pIRl%WY4Gq*7bEz;le$U~4R0&a!>)Hn;k;N!YrZdD}jRR84U_eTK;7^)=;y6g74 z-uQ!|hR7pBb)m`h{+IB!c-u>VVL^YXv3q#H9S7QJf08W2AL@4a-10GQ#n3_h2NrPO z24FzI^iGB?_qz`|6Rdu%2cAMM+@^na26uk!#t4(m@vf0a}JSLs%c^H=(p z=>IQ_;lC_||Cqmz#=mx;#Q)J1w@07tiEaz`fel0{P*lYFH{gy|gZ}(ESG2l?{U05BdoK5fe;Z4z$p3W=I1t?( zhHgcwz)`>wTJ3)miq+p;3{0(9|EDQHO9FZ*zfI(D|9jH=hkq*-`&$Jn`+w{DFXtcp zt)epUs2UZpQVVpg8W6A`&E1+UFqonsR<>st

5mh7OKKw~wenm#e|prf{UXl=N7; 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 QW@"] +edition = "2018" + +[dependencies] +rider-lexers = { path = "../rider-lexers", version = "0.1.0" } +rider-themes = { path = "../rider-themes", version = "0.1.0" } +rand = "0.5" +plex = "*" +dirs = "*" +serde = "*" +serde_json = "*" +serde_derive = "*" +log = "*" +env_logger = "*" +simplelog = "*" +lazy_static = "*" + +[dependencies.sdl2] +version = "0.31.0" +features = ["gfx", "image", "mixer", "ttf"] diff --git a/rider-config/src/config.rs b/rider-config/src/config.rs new file mode 100644 index 0000000..ba417e9 --- /dev/null +++ b/rider-config/src/config.rs @@ -0,0 +1,242 @@ +use crate::directories::*; +use crate::EditorConfig; +use crate::ScrollConfig; +use rider_lexers::Language; +use rider_themes::Theme; +use std::collections::HashMap; +use std::fs; + +pub type LanguageMapping = HashMap; + +#[derive(Debug, Clone)] +pub struct Config { + width: u32, + height: u32, + menu_height: u16, + editor_config: EditorConfig, + theme: Theme, + extensions_mapping: LanguageMapping, + scroll: ScrollConfig, + directories: Directories, +} + +impl Config { + pub fn new() -> Self { + let directories = Directories::new(None, None); + let editor_config = EditorConfig::new(&directories); + let mut extensions_mapping = HashMap::new(); + extensions_mapping.insert(".".to_string(), Language::PlainText); + extensions_mapping.insert("txt".to_string(), Language::PlainText); + extensions_mapping.insert("rs".to_string(), Language::Rust); + + Self { + width: 1024, + height: 860, + menu_height: 60, + theme: Theme::default(), + editor_config, + extensions_mapping, + scroll: ScrollConfig::new(), + directories, + } + } + + pub fn width(&self) -> u32 { + self.width + } + + pub fn set_width(&mut self, w: u32) { + self.width = w; + } + + pub fn height(&self) -> u32 { + self.height + } + + pub fn set_height(&mut self, h: u32) { + self.height = h; + } + + pub fn editor_config(&self) -> &EditorConfig { + &self.editor_config + } + + pub fn theme(&self) -> &Theme { + &self.theme + } + + pub fn menu_height(&self) -> u16 { + self.menu_height + } + + pub fn editor_top_margin(&self) -> i32 { + i32::from(self.menu_height()) + i32::from(self.editor_config().margin_top()) + } + + pub fn editor_left_margin(&self) -> i32 { + i32::from(self.editor_config().margin_left()) + } + + pub fn extensions_mapping(&self) -> &LanguageMapping { + &self.extensions_mapping + } + + pub fn scroll(&self) -> &ScrollConfig { + &self.scroll + } + + pub fn scroll_mut(&mut self) -> &mut ScrollConfig { + &mut self.scroll + } + + pub fn directories(&self) -> &Directories { + &self.directories + } + + pub fn set_theme(&mut self, theme: String) { + self.theme = self.load_theme(theme); + } +} + +impl Config { + pub fn load_theme(&self, theme_name: String) -> Theme { + let home_dir = dirs::config_dir().unwrap(); + let mut config_dir = home_dir.clone(); + config_dir.push("rider"); + fs::create_dir_all(&config_dir) + .unwrap_or_else(|_| panic!("Cannot create config directory")); + self.load_theme_content(format!("{}.json", theme_name).as_str()) + } + + fn load_theme_content(&self, file_name: &str) -> Theme { + let mut config_file = self.directories.themes_dir.clone(); + config_file.push(file_name); + let contents = match fs::read_to_string(&config_file) { + Ok(s) => s, + Err(_) => fs::read_to_string(&config_file).unwrap_or_else(|_| "".to_owned()), + }; + serde_json::from_str(&contents).unwrap_or_default() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn must_return_language_mapping() { + let config = Config::new(); + + let mapping = config.extensions_mapping(); + { + let mut keys: Vec = mapping.keys().map(|s| s.to_string()).collect(); + let mut expected: Vec = + vec![".".to_string(), "txt".to_string(), "rs".to_string()]; + keys.sort(); + expected.sort(); + assert_eq!(keys, expected); + } + { + let mut keys: Vec = mapping.values().map(|s| s.clone()).collect(); + let mut expected: Vec = + vec![Language::PlainText, Language::PlainText, Language::Rust]; + keys.sort(); + expected.sort(); + assert_eq!(keys, expected); + } + } + + #[test] + fn assert_scroll() { + let config = Config::new(); + let result = config.scroll(); + let expected = ScrollConfig::new(); + assert_eq!(result.clone(), expected); + } + + #[test] + fn assert_scroll_mut() { + let mut config = Config::new(); + let result = config.scroll_mut(); + result.set_margin_right(1236); + let mut expected = ScrollConfig::new(); + expected.set_margin_right(1236); + assert_eq!(result.clone(), expected); + } +} + +#[cfg(test)] +mod test_getters { + use super::*; + + #[test] + fn assert_width() { + let config = Config::new(); + let result = config.width(); + let expected = 1024; + assert_eq!(result, expected); + } + + #[test] + fn assert_height() { + let config = Config::new(); + let result = config.height(); + let expected = 860; + assert_eq!(result, expected); + } + + // #[test] + // fn assert_editor_config() { + // let config = Config::new(); + // let result = config.editor_config(); + // let expected = 1; + // assert_eq!(result, expected); + // } + + // #[test] + // fn assert_theme() { + // let config = Config::new(); + // let result = config.theme(); + // let expected = 1; + // assert_eq!(result, expected); + // } + + #[test] + fn assert_menu_height() { + let config = Config::new(); + let result = config.menu_height(); + let expected = 60; + assert_eq!(result, expected); + } + + #[test] + fn assert_editor_top_margin() { + let config = Config::new(); + let result = config.editor_top_margin(); + let expected = config.menu_height() as i32 + config.editor_config().margin_top() as i32; + assert_eq!(result, expected); + } + + #[test] + fn assert_editor_left_margin() { + let config = Config::new(); + let result = config.editor_left_margin(); + let expected = 10; + assert_eq!(result, expected); + } + + #[test] + fn assert_extensions_mapping() { + let config = Config::new(); + let mut result: Vec = config + .extensions_mapping() + .keys() + .map(|s| s.to_owned()) + .collect(); + result.sort(); + let mut expected: Vec = vec!["rs".to_string(), "txt".to_string(), ".".to_string()]; + expected.sort(); + assert_eq!(result, expected); + } + +} diff --git a/rider-config/src/directories.rs b/rider-config/src/directories.rs new file mode 100644 index 0000000..7091c2a --- /dev/null +++ b/rider-config/src/directories.rs @@ -0,0 +1,183 @@ +use dirs; +use std::env; +use std::path::PathBuf; + +#[derive(Debug, Clone)] +pub struct Directories { + pub log_dir: PathBuf, + pub themes_dir: PathBuf, + pub fonts_dir: PathBuf, + pub config_dir: PathBuf, + pub project_dir: PathBuf, +} + +impl Directories { + pub fn new(config_dir: Option, project_dir: Option) -> Self { + let path = match config_dir { + Some(s) => s, + None => dirs::config_dir().unwrap().to_str().unwrap().to_owned(), + }; + let mut config_dir = PathBuf::new(); + config_dir.push(path); + config_dir.push("rider"); + + let path = match project_dir { + Some(s) => s, + None => dirs::runtime_dir().unwrap().to_str().unwrap().to_owned(), + }; + let mut project_dir = PathBuf::new(); + project_dir.push(path); + project_dir.push(".rider"); + + Self { + log_dir: log_dir(&config_dir), + themes_dir: themes_dir(&config_dir), + fonts_dir: fonts_dir(&config_dir), + config_dir, + project_dir, + } + } +} + +pub fn log_dir(config_dir: &PathBuf) -> PathBuf { + let path = config_dir.to_str().unwrap().to_owned(); + let mut path_buf = PathBuf::new(); + path_buf.push(path); + path_buf.push("log"); + path_buf +} + +pub fn themes_dir(config_dir: &PathBuf) -> PathBuf { + let path = config_dir.to_str().unwrap().to_owned(); + let mut path_buf = PathBuf::new(); + path_buf.push(path); + path_buf.push("themes"); + path_buf +} + +pub fn fonts_dir(config_dir: &PathBuf) -> PathBuf { + let path = config_dir.to_str().unwrap().to_owned(); + let mut path_buf = PathBuf::new(); + path_buf.push(path); + path_buf.push("fonts"); + path_buf +} + +pub fn project_dir() -> PathBuf { + let path = dirs::runtime_dir().unwrap().to_str().unwrap().to_owned(); + let mut path_buf = PathBuf::new(); + path_buf.push(path); + path_buf.push(".rider"); + path_buf +} + +#[cfg_attr(tarpaulin, skip)] +pub fn binaries_directory() -> Result { + let mut exec_dir = PathBuf::new(); + exec_dir.push(dirs::executable_dir().unwrap().clone()); + let mut rider_editor = exec_dir.clone(); + rider_editor.push("rider-editor"); + if rider_editor.exists() { + return Ok(exec_dir); + } + + let path = dirs::runtime_dir().unwrap().to_str().unwrap().to_owned(); + let mut path_buf = PathBuf::new(); + path_buf.push(path.clone()); + path_buf.push("rider-editor"); + if path_buf.exists() { + let mut path_buf = PathBuf::new(); + path_buf.push(path); + return Ok(path_buf); + } + + let mut current_dir = env::current_dir().unwrap(); + current_dir.push("target"); + current_dir.push("debug"); + let mut rider_editor = current_dir.clone(); + rider_editor.push("rider-editor"); + if rider_editor.exists() { + return Ok(current_dir); + } + + let executable = dirs::executable_dir().unwrap(); + let mut rider_editor = executable.clone(); + rider_editor.push("rider-editor"); + if rider_editor.exists() { + return Ok(executable); + } + + Err("Cannot find binaries!".to_string()) +} + +pub fn get_binary_path(name: &str) -> Result { + if cfg!(test) { + use std::fs; + println!("#[cfg(test)]"); + + let mut current_dir = env::current_dir().unwrap(); + current_dir.push("target"); + current_dir.push("debug"); + let name = name.to_string().to_lowercase().replace("-", "_"); + println!(" name {:?}", name); + current_dir.push(vec![name.clone(), "*".to_string()].join("-")); + for entry in fs::read_dir(current_dir.to_str().unwrap()).unwrap() { + if let Ok(entry) = entry { + if let Ok(meta) = entry.metadata() { + if meta.is_file() && !entry.path().ends_with(".d") { + return Ok(entry.path().to_str().unwrap().to_string()); + } + } + } + } + Err(format!("Cannot find {:?}", name)) + } else { + println!("#[cfg(not(test))]"); + let r = binaries_directory(); + let mut binaries: PathBuf = r.unwrap_or_else(|e| panic!(e)); + binaries.push(name.to_string()); + println!(" name {}", name); + match binaries.to_str() { + Some(s) => Ok(s.to_owned()), + _ => Err(format!("Cannot find {:?}", name)), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::{Path, PathBuf}; + + #[test] + fn assert_log_dir() { + let directories = Directories::new(Some("/tmp".to_owned()), None); + let path = directories.log_dir.clone(); + let expected: PathBuf = Path::new("/tmp/rider/log").into(); + assert_eq!(path, expected); + } + + #[test] + fn assert_themes_dir() { + let directories = Directories::new(Some("/tmp".to_owned()), None); + let path = directories.themes_dir.clone(); + let expected: PathBuf = Path::new("/tmp/rider/themes").into(); + assert_eq!(path, expected); + } + + #[test] + fn assert_fonts_dir() { + let directories = Directories::new(Some("/tmp".to_owned()), None); + let path = directories.fonts_dir.clone(); + let expected: PathBuf = Path::new("/tmp/rider/fonts").into(); + assert_eq!(path, expected); + } + + #[test] + fn assert_config_dir() { + let directories = Directories::new(Some("/tmp".to_owned()), None); + let path = directories.config_dir.clone(); + let expected: PathBuf = Path::new("/tmp/rider").into(); + assert_eq!(path, expected); + } +} diff --git a/rider-config/src/editor_config.rs b/rider-config/src/editor_config.rs new file mode 100644 index 0000000..5903cd1 --- /dev/null +++ b/rider-config/src/editor_config.rs @@ -0,0 +1,93 @@ +use crate::directories::Directories; + +#[derive(Debug, Clone)] +pub struct EditorConfig { + character_size: u16, + font_path: String, + current_theme: String, + margin_left: u16, + margin_top: u16, +} + +impl EditorConfig { + pub fn new(directories: &Directories) -> Self { + let mut default_font_path = directories.fonts_dir.clone(); + default_font_path.push("DejaVuSansMono.ttf"); + Self { + character_size: 14, + font_path: default_font_path.to_str().unwrap().to_string(), + current_theme: "railscasts".to_string(), + margin_left: 10, + margin_top: 10, + } + } + + pub fn character_size(&self) -> u16 { + self.character_size + } + + pub fn font_path(&self) -> &String { + &self.font_path + } + + pub fn current_theme(&self) -> &String { + &self.current_theme + } + + pub fn margin_left(&self) -> u16 { + self.margin_left + } + + pub fn margin_top(&self) -> u16 { + self.margin_top + } +} + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn assert_font_path() { + let directories = Directories::new(Some("/tmp".to_owned()), None); + let config = EditorConfig::new(&directories); + let path = config.font_path().to_owned(); + let expected: String = "/tmp/rider/fonts/DejaVuSansMono.ttf".to_owned(); + assert_eq!(path, expected); + } + + #[test] + fn assert_character_size() { + let directories = Directories::new(Some("/tmp".to_owned()), None); + let config = EditorConfig::new(&directories); + let result = config.character_size(); + let expected: u16 = 14; + assert_eq!(result, expected); + } + + #[test] + fn assert_current_theme() { + let directories = Directories::new(Some("/tmp".to_owned()), None); + let config = EditorConfig::new(&directories); + let result = config.current_theme().to_owned(); + let expected = "railscasts".to_owned(); + assert_eq!(result, expected); + } + + #[test] + fn assert_margin_left() { + let directories = Directories::new(Some("/tmp".to_owned()), None); + let config = EditorConfig::new(&directories); + let result = config.margin_left(); + let expected: u16 = 10; + assert_eq!(result, expected); + } + + #[test] + fn assert_margin_top() { + let directories = Directories::new(Some("/tmp".to_owned()), None); + let config = EditorConfig::new(&directories); + let result = config.margin_top(); + let expected: u16 = 10; + assert_eq!(result, expected); + } +} diff --git a/src/config/mod.rs b/rider-config/src/lib.rs similarity index 56% rename from src/config/mod.rs rename to rider-config/src/lib.rs index 35a7e4a..05bc7a2 100644 --- a/src/config/mod.rs +++ b/rider-config/src/lib.rs @@ -1,15 +1,17 @@ +extern crate rider_lexers; +extern crate rider_themes; + use std::sync::{Arc, RwLock}; pub mod config; -pub(crate) mod creator; pub mod directories; pub mod editor_config; pub mod scroll_config; -pub use crate::config::config::*; -pub use crate::config::directories::*; -pub use crate::config::editor_config::*; -pub use crate::config::scroll_config::*; +pub use crate::config::*; +pub use crate::directories::*; +pub use crate::editor_config::*; +pub use crate::scroll_config::*; pub type ConfigAccess = Arc>; diff --git a/src/config/scroll_config.rs b/rider-config/src/scroll_config.rs similarity index 91% rename from src/config/scroll_config.rs rename to rider-config/src/scroll_config.rs index 9669b4a..3f4149b 100644 --- a/src/config/scroll_config.rs +++ b/rider-config/src/scroll_config.rs @@ -39,6 +39,17 @@ impl ScrollConfig { } } +impl Default for ScrollConfig { + fn default() -> Self { + Self { + width: 4, + margin_right: 5, + speed: 10, + } + } +} + +#[cfg(test)] mod tests { use super::*; @@ -92,5 +103,4 @@ mod tests { let expected = 98; assert_eq!(result, expected); } - } diff --git a/rider-editor/Cargo.toml b/rider-editor/Cargo.toml new file mode 100644 index 0000000..f5541e1 --- /dev/null +++ b/rider-editor/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "rider-editor" +version = "0.1.0" +authors = ["Adrian Wozniak "] +edition = "2018" + +[dependencies] +rider-config = { path = "../rider-config", version = "0.1.0" } +rider-lexers = { path = "../rider-lexers", version = "0.1.0" } +rider-themes = { path = "../rider-themes", version = "0.1.0" } +rand = "0.5" +dirs = "*" +serde = "*" +serde_json = "*" +serde_derive = "*" +log = "*" +simplelog = "*" +lazy_static = "*" + +[dependencies.sdl2] +version = "0.31.0" +features = ["gfx", "image", "mixer", "ttf"] diff --git a/assets/gear-64x64.bmp b/rider-editor/assets/images/gear-64x64.bmp similarity index 100% rename from assets/gear-64x64.bmp rename to rider-editor/assets/images/gear-64x64.bmp diff --git a/rider-editor/src/app/app_state.rs b/rider-editor/src/app/app_state.rs new file mode 100644 index 0000000..351d429 --- /dev/null +++ b/rider-editor/src/app/app_state.rs @@ -0,0 +1,185 @@ +use crate::app::{UpdateResult, WindowCanvas as WC}; +use crate::renderer::Renderer; +use crate::ui::*; +use rider_config::*; +use sdl2::rect::Point; +use sdl2::VideoSubsystem as VS; +use std::fs::read_to_string; +use std::sync::*; + +pub struct AppState { + menu_bar: MenuBar, + files: Vec, + config: Arc>, + file_editor: FileEditor, + open_file_modal: Option, +} + +impl AppState { + pub fn new(config: Arc>) -> Self { + Self { + menu_bar: MenuBar::new(Arc::clone(&config)), + files: vec![], + file_editor: FileEditor::new(Arc::clone(&config)), + open_file_modal: None, + config, + } + } + + #[cfg_attr(tarpaulin, skip)] + pub fn open_file(&mut self, file_path: String, renderer: &mut Renderer) { + if let Ok(buffer) = read_to_string(&file_path) { + let mut file = EditorFile::new(file_path.clone(), buffer, self.config.clone()); + file.prepare_ui(renderer); + match self.file_editor.open_file(file) { + Some(old) => self.files.push(old), + _ => (), + } + } else { + eprintln!("Failed to open file: {}", file_path); + }; + } + + #[cfg_attr(tarpaulin, skip)] + pub fn open_directory(&mut self, dir_path: String, renderer: &mut Renderer) { + match self.open_file_modal.as_mut() { + Some(modal) => modal.open_directory(dir_path, renderer), + _ => (), + }; + } + + pub fn file_editor(&self) -> &FileEditor { + &self.file_editor + } + + pub fn file_editor_mut(&mut self) -> &mut FileEditor { + &mut self.file_editor + } + + pub fn set_open_file_modal(&mut self, modal: Option) { + self.open_file_modal = modal; + } + + pub fn scroll_by(&mut self, x: i32, y: i32) { + if let Some(modal) = self.open_file_modal.as_mut() { + modal.scroll_by(x, y); + } else { + self.file_editor_mut().scroll_by(x, y); + } + } + + pub fn open_file_modal(&self) -> Option<&OpenFile> { + self.open_file_modal.as_ref() + } +} + +#[cfg_attr(tarpaulin, skip)] +impl Render for AppState { + fn render(&self, canvas: &mut WC, renderer: &mut Renderer, _context: &RenderContext) { + self.file_editor + .render(canvas, renderer, &RenderContext::Nothing); + self.menu_bar + .render(canvas, renderer, &RenderContext::Nothing); + match self.open_file_modal.as_ref() { + Some(modal) => modal.render(canvas, renderer, &RenderContext::Nothing), + _ => (), + }; + } + + fn prepare_ui(&mut self, renderer: &mut Renderer) { + self.menu_bar.prepare_ui(renderer); + self.file_editor.prepare_ui(renderer); + } +} + +impl Update for AppState { + fn update(&mut self, ticks: i32, context: &UpdateContext) -> UpdateResult { + let res = match self.open_file_modal.as_mut() { + Some(modal) => modal.update(ticks, &UpdateContext::Nothing), + _ => UpdateResult::NoOp, + }; + if res != UpdateResult::NoOp { + return res; + } + + self.menu_bar.update(ticks, context); + self.file_editor.update(ticks, context); + UpdateResult::NoOp + } +} + +impl AppState { + #[cfg_attr(tarpaulin, skip)] + pub fn on_left_click(&mut self, point: &Point, video_subsystem: &mut VS) -> UpdateResult { + match self.open_file_modal.as_mut() { + Some(modal) => return modal.on_left_click(point, &UpdateContext::Nothing), + _ => (), + }; + if self + .menu_bar + .is_left_click_target(point, &UpdateContext::Nothing) + { + video_subsystem.text_input().stop(); + return self.menu_bar.on_left_click(point, &UpdateContext::Nothing); + } else if !self + .file_editor + .is_left_click_target(point, &UpdateContext::Nothing) + { + return UpdateResult::NoOp; + } else { + video_subsystem.text_input().start(); + self.file_editor + .on_left_click(point, &UpdateContext::Nothing); + } + UpdateResult::NoOp + } + + pub fn is_left_click_target(&self, _point: &Point) -> bool { + true + } +} + +impl ConfigHolder for AppState { + fn config(&self) -> &ConfigAccess { + &self.config + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::support; + // use crate::ui::modal::open_file; + use std::sync::Arc; + + #[test] + fn must_return_none_for_default_file() { + let config = support::build_config(); + let state = AppState::new(Arc::clone(&config)); + let file = state.file_editor().file(); + assert_eq!(file.is_none(), true); + } + + #[test] + fn must_scroll_file_when_no_modal() { + let config = support::build_config(); + let mut state = AppState::new(Arc::clone(&config)); + let old_scroll = state.file_editor().scroll(); + state.set_open_file_modal(None); + state.scroll_by(10, 10); + assert_ne!(state.file_editor().scroll(), old_scroll); + } + + #[test] + fn must_scroll_modal_when_modal_was_set() { + let config = support::build_config(); + let mut state = AppState::new(Arc::clone(&config)); + let modal = OpenFile::new("/".to_owned(), 100, 100, Arc::clone(&config)); + let file_scroll = state.file_editor().scroll(); + let old_scroll = state.file_editor().scroll(); + state.set_open_file_modal(Some(modal)); + state.scroll_by(10, 10); + assert_eq!(state.file_editor().scroll(), file_scroll); + assert_ne!(state.open_file_modal().unwrap().scroll(), old_scroll); + } +} diff --git a/rider-editor/src/app/application.rs b/rider-editor/src/app/application.rs new file mode 100644 index 0000000..e2e34f1 --- /dev/null +++ b/rider-editor/src/app/application.rs @@ -0,0 +1,324 @@ +pub use crate::app::app_state::AppState; +pub use crate::renderer::Renderer; +use crate::ui::caret::{CaretPosition, MoveDirection}; +use crate::ui::*; +pub use rider_config::{Config, ConfigAccess, ConfigHolder}; +use sdl2::event::*; +use sdl2::hint; +use sdl2::keyboard::Keycode; +use sdl2::keyboard::Scancode; +use sdl2::mouse::*; +use sdl2::pixels::Color; +use sdl2::rect::{Point, Rect}; +use sdl2::render::Canvas; +use sdl2::rwops::RWops; +use sdl2::surface::Surface; +use sdl2::video::Window; +use sdl2::EventPump; +use sdl2::{Sdl, TimerSubsystem, VideoSubsystem}; +use std::process::Command; +use std::sync::{Arc, RwLock}; +use std::thread::sleep; +use std::time::Duration; + +pub type WindowCanvas = Canvas; + +#[derive(PartialEq, Clone, Debug)] +pub enum UpdateResult { + NoOp, + Stop, + RefreshPositions, + MouseLeftClicked(Point), + MoveCaret(Rect, CaretPosition), + DeleteFront, + DeleteBack, + Input(String), + InsertNewLine, + MoveCaretLeft, + MoveCaretRight, + MoveCaretUp, + MoveCaretDown, + Scroll { x: i32, y: i32 }, + WindowResize { width: i32, height: i32 }, + RefreshFsTree, + OpenFile(String), + OpenDirectory(String), + OpenFileModal, +} + +#[cfg_attr(tarpaulin, skip)] +pub struct Application { + config: Arc>, + clear_color: Color, + sdl_context: Sdl, + canvas: WindowCanvas, + video_subsystem: VideoSubsystem, + tasks: Vec, +} + +#[cfg_attr(tarpaulin, skip)] +impl Application { + pub fn new() -> Self { + let generator_path = rider_config::directories::get_binary_path("rider-generator") + .unwrap_or_else(|e| panic!(e)); + Command::new(generator_path).status().unwrap(); + + let mut config = Config::new(); + config.set_theme(config.editor_config().current_theme().clone()); + let config = Arc::new(RwLock::new(config)); + let sdl_context = sdl2::init().unwrap(); + + hint::set("SDL_GL_MULTISAMPLEBUFFERS", "1"); + hint::set("SDL_GL_MULTISAMPLESAMPLES", "8"); + hint::set("SDL_GL_ACCELERATED_VISUAL", "1"); + hint::set("SDL_HINT_RENDER_SCALE_QUALITY", "2"); + hint::set("SDL_HINT_VIDEO_ALLOW_SCREENSAVER", "1"); + + let video_subsystem = sdl_context.video().unwrap(); + + let mut window: Window = { + let c = config.read().unwrap(); + video_subsystem + .window("Rider", c.width(), c.height()) + .position_centered() + .resizable() + .opengl() + .build() + .unwrap() + }; + let icon_bytes = include_bytes!("../../assets/images/gear-64x64.bmp").clone(); + let mut rw = RWops::from_bytes(&icon_bytes).unwrap(); + let mut icon = Surface::load_bmp_rw(&mut rw).unwrap(); + window.set_icon(&mut icon); + + let canvas = window.into_canvas().accelerated().build().unwrap(); + let clear_color: Color = { config.read().unwrap().theme().background().into() }; + + Self { + sdl_context, + video_subsystem, + canvas, + tasks: vec![], + clear_color, + config, + } + } + + pub fn init(&mut self) { + self.clear(); + } + + pub fn run(&mut self) { + let mut timer: TimerSubsystem = self.sdl_context.timer().unwrap(); + let mut event_pump = self.sdl_context.event_pump().unwrap(); + let font_context = sdl2::ttf::init().unwrap(); + let texture_creator = self.canvas.texture_creator(); + let sleep_time = Duration::new(0, 1_000_000_000u32 / 60); + let mut app_state = AppState::new(Arc::clone(&self.config)); + let mut renderer = Renderer::new(Arc::clone(&self.config), &font_context, &texture_creator); + app_state.prepare_ui(&mut renderer); + + 'running: loop { + self.handle_events(&mut event_pump); + let mut new_tasks: Vec = vec![]; + for task in self.tasks.iter() { + match task { + UpdateResult::Stop => break 'running, + UpdateResult::RefreshPositions => (), + UpdateResult::NoOp => (), + UpdateResult::MoveCaret(_, _pos) => (), + UpdateResult::MouseLeftClicked(point) => { + let res = app_state.on_left_click(&point, &mut self.video_subsystem); + match res { + UpdateResult::OpenDirectory(_) => new_tasks.push(res), + UpdateResult::OpenFile(_) => { + new_tasks.push(res); + app_state.set_open_file_modal(None); + } + _ => {} + } + } + UpdateResult::DeleteFront => { + app_state.file_editor_mut().delete_front(&mut renderer); + } + UpdateResult::DeleteBack => { + app_state.file_editor_mut().delete_back(&mut renderer); + } + UpdateResult::Input(text) => { + app_state + .file_editor_mut() + .insert_text(text.clone(), &mut renderer); + } + UpdateResult::InsertNewLine => { + app_state.file_editor_mut().insert_new_line(&mut renderer); + } + UpdateResult::MoveCaretLeft => { + app_state.file_editor_mut().move_caret(MoveDirection::Left); + } + UpdateResult::MoveCaretRight => { + app_state.file_editor_mut().move_caret(MoveDirection::Right); + } + UpdateResult::MoveCaretUp => { + app_state.file_editor_mut().move_caret(MoveDirection::Up); + } + UpdateResult::MoveCaretDown => { + app_state.file_editor_mut().move_caret(MoveDirection::Down); + } + UpdateResult::Scroll { x, y } => { + app_state.scroll_by(-x.clone(), -y.clone()); + } + UpdateResult::WindowResize { width, height } => { + let mut c = app_state.config().write().unwrap(); + let w = width.clone(); + let h = height.clone(); + if w > 0 { + c.set_width(w as u32); + } + if h > 0 { + c.set_height(h as u32); + } + } + UpdateResult::RefreshFsTree => unimplemented!(), + UpdateResult::OpenFile(file_path) => { + app_state.open_file(file_path.clone(), &mut renderer); + } + UpdateResult::OpenDirectory(dir_path) => { + app_state.open_directory(dir_path.clone(), &mut renderer); + } + UpdateResult::OpenFileModal => { + use std::env; + let pwd = env::current_dir().unwrap().to_str().unwrap().to_string(); + let mut modal = + OpenFile::new(pwd.clone(), 400, 800, Arc::clone(&self.config)); + modal.prepare_ui(&mut renderer); + modal.open_directory(pwd.clone(), &mut renderer); + app_state.set_open_file_modal(Some(modal)); + } + } + } + self.tasks = new_tasks; + + self.clear(); + + app_state.update(timer.ticks() as i32, &UpdateContext::Nothing); + app_state.render(&mut self.canvas, &mut renderer, &RenderContext::Nothing); + + self.present(); + + if !cfg!(test) { + sleep(sleep_time); + } + } + } + + pub fn open_file(&mut self, file_path: String) { + self.tasks.push(UpdateResult::OpenFile(file_path)); + } + + fn present(&mut self) { + self.canvas.present(); + } + + fn clear(&mut self) { + self.canvas.set_draw_color(self.clear_color.clone()); + self.canvas.clear(); + } + + fn handle_events(&mut self, event_pump: &mut EventPump) { + let left_control_pressed = event_pump + .keyboard_state() + .is_scancode_pressed(Scancode::LCtrl); + let shift_pressed = event_pump + .keyboard_state() + .is_scancode_pressed(Scancode::LShift) + || event_pump + .keyboard_state() + .is_scancode_pressed(Scancode::RShift); + + for event in event_pump.poll_iter() { + match event { + Event::Quit { .. } => self.tasks.push(UpdateResult::Stop), + Event::MouseButtonUp { + mouse_btn, x, y, .. + } => match mouse_btn { + MouseButton::Left => self + .tasks + .push(UpdateResult::MouseLeftClicked(Point::new(x, y))), + _ => (), + }, + Event::KeyDown { keycode, .. } => { + let keycode = if keycode.is_some() { + keycode.unwrap() + } else { + continue; + }; + + match keycode { + Keycode::Backspace => { + self.tasks.push(UpdateResult::DeleteFront); + } + Keycode::Delete => { + self.tasks.push(UpdateResult::DeleteBack); + } + Keycode::KpEnter | Keycode::Return => { + self.tasks.push(UpdateResult::InsertNewLine); + } + Keycode::Left => { + self.tasks.push(UpdateResult::MoveCaretLeft); + } + Keycode::Right => { + self.tasks.push(UpdateResult::MoveCaretRight); + } + Keycode::Up => { + self.tasks.push(UpdateResult::MoveCaretUp); + } + Keycode::Down => { + self.tasks.push(UpdateResult::MoveCaretDown); + } + Keycode::O => { + if left_control_pressed && !shift_pressed { + self.tasks.push(UpdateResult::OpenFileModal); + } + } + _ => {} + }; + } + Event::TextInput { text, .. } => { + self.tasks.push(UpdateResult::Input(text)); + } + Event::MouseWheel { + direction, x, y, .. + } => { + match direction { + MouseWheelDirection::Normal => { + self.tasks.push(UpdateResult::Scroll { x, y }); + } + MouseWheelDirection::Flipped => { + self.tasks.push(UpdateResult::Scroll { x, y: -y }); + } + _ => { + // ignore + } + }; + } + Event::Window { + win_event: WindowEvent::Resized(w, h), + .. + } => { + self.tasks.push(UpdateResult::WindowResize { + width: w, + height: h, + }); + } + _ => {} + } + } + } +} + +#[cfg_attr(tarpaulin, skip)] +impl ConfigHolder for Application { + fn config(&self) -> &ConfigAccess { + &self.config + } +} diff --git a/rider-editor/src/app/caret_manager.rs b/rider-editor/src/app/caret_manager.rs new file mode 100644 index 0000000..090cbc2 --- /dev/null +++ b/rider-editor/src/app/caret_manager.rs @@ -0,0 +1,67 @@ +use crate::ui::*; +use sdl2::rect::Point; + +pub fn move_caret_right(file_editor: &mut FileEditor) { + let file: &EditorFile = match file_editor.file() { + None => return, + Some(f) => f, + }; + let c: TextCharacter = match file.get_character_at(file_editor.caret().text_position() + 1) { + Some(text_character) => text_character, + None => return, // EOF + }; + let pos = file_editor.caret().position(); + let d = c.dest().clone(); + let p = pos.moved(1, 0, 0); + file_editor + .caret_mut() + .move_caret(p, Point::new(d.x(), d.y())); +} + +pub fn move_caret_left(file_editor: &mut FileEditor) { + let file: &EditorFile = match file_editor.file() { + None => return, + Some(f) => f, + }; + if file_editor.caret().text_position() == 0 { + return; + } + let c: TextCharacter = match file.get_character_at(file_editor.caret().text_position() - 1) { + Some(text_character) => text_character, + None => return, // EOF + }; + let pos = file_editor.caret().position(); + let d = c.dest().clone(); + let p = pos.moved(-1, 0, 0); + file_editor + .caret_mut() + .move_caret(p, Point::new(d.x(), d.y())); +} + +#[cfg(test)] +mod test_move_right { + use super::*; + use crate::tests::support; + + #[test] + fn must_do_nothing() { + let config = support::build_config(); + let mut editor = FileEditor::new(config); + + assert_eq!(move_caret_right(&mut editor), ()); + } +} + +#[cfg(test)] +mod test_move_left { + use super::*; + use crate::tests::support; + + #[test] + fn must_do_nothing() { + let config = support::build_config(); + let mut editor = FileEditor::new(config); + + assert_eq!(move_caret_left(&mut editor), ()); + } +} diff --git a/src/app/file_content_manager.rs b/rider-editor/src/app/file_content_manager.rs similarity index 70% rename from src/app/file_content_manager.rs rename to rider-editor/src/app/file_content_manager.rs index 3f5cac7..acab0ff 100644 --- a/src/app/file_content_manager.rs +++ b/rider-editor/src/app/file_content_manager.rs @@ -1,15 +1,16 @@ use crate::app::*; use crate::renderer::Renderer; use crate::ui::*; -use sdl2::rect::*; +use sdl2::rect::{Point, Rect}; use std::sync::*; -fn current_file_path(file_editor: &mut FileEditor) -> String { +pub fn current_file_path(file_editor: &mut FileEditor) -> String { file_editor .file() .map_or_else(|| String::new(), |f| f.path()) } +#[cfg_attr(tarpaulin, skip)] pub fn delete_front(file_editor: &mut FileEditor, renderer: &mut Renderer) { let mut buffer: String = if let Some(file) = file_editor.file() { file @@ -35,7 +36,7 @@ pub fn delete_front(file_editor: &mut FileEditor, renderer: &mut Renderer) { .file() .and_then(|f| f.get_character_at(file_editor.caret().text_position())) .and_then(|character| { - let dest: &Rect = character.dest(); + let dest: Rect = character.dest(); Some((position, Point::new(dest.x(), dest.y()))) }); match move_to { @@ -51,6 +52,7 @@ pub fn delete_front(file_editor: &mut FileEditor, renderer: &mut Renderer) { file_editor.replace_current_file(new_file); } +#[cfg_attr(tarpaulin, skip)] pub fn delete_back(file_editor: &mut FileEditor, renderer: &mut Renderer) { let file: &EditorFile = if let Some(file) = file_editor.file() { file @@ -68,6 +70,7 @@ pub fn delete_back(file_editor: &mut FileEditor, renderer: &mut Renderer) { file_editor.replace_current_file(new_file); } +#[cfg_attr(tarpaulin, skip)] pub fn insert_text(file_editor: &mut FileEditor, text: String, renderer: &mut Renderer) { let mut buffer: String = file_editor.file().map_or(String::new(), |f| f.buffer()); if buffer.is_empty() { @@ -81,15 +84,19 @@ pub fn insert_text(file_editor: &mut FileEditor, text: String, renderer: &mut Re Some(c) => c, _ => return, }; - let mut pos = Point::new(current.dest().x(), current.dest().y()); + let mut pos = if current.is_new_line() { + current.dest().top_left() + + Point::new(0, renderer.load_character_size('\n').height() as i32) + } else { + current.dest().top_left() + }; let mut position: CaretPosition = file_editor.caret().position().clone(); for character in text.chars() { buffer.insert(position.text_position(), character); - if let Some(rect) = get_text_character_rect(character, renderer) { - pos = pos + Point::new(rect.width() as i32, 0); - position = position.moved(1, 0, 0); - file_editor.caret_mut().move_caret(position, pos.clone()); - } + let rect = renderer.load_character_size(character); + pos = pos + Point::new(rect.width() as i32, 0); + position = position.moved(1, 0, 0); + file_editor.caret_mut().move_caret(position, pos.clone()); } let mut new_file = EditorFile::new( @@ -101,6 +108,7 @@ pub fn insert_text(file_editor: &mut FileEditor, text: String, renderer: &mut Re file_editor.replace_current_file(new_file); } +#[cfg_attr(tarpaulin, skip)] pub fn insert_new_line(file_editor: &mut FileEditor, renderer: &mut Renderer) { let mut buffer: String = if let Some(file) = file_editor.file() { file @@ -115,17 +123,14 @@ pub fn insert_new_line(file_editor: &mut FileEditor, renderer: &mut Renderer) { Some(c) => c, _ => return, }; + let mut pos = Point::new(current.dest().x(), current.dest().y()); let mut position: CaretPosition = file_editor.caret().position().clone(); buffer.insert(position.text_position(), '\n'); - if let Some(rect) = get_text_character_rect('\n', renderer) { - pos = Point::new( - file_editor.config().read().unwrap().editor_left_margin(), - pos.y() + rect.height() as i32, - ); - position = position.moved(0, 1, 0); - file_editor.caret_mut().move_caret(position, pos.clone()); - } + let rect = renderer.load_character_size('\n'); + pos = Point::new(0, pos.y() + rect.height() as i32); + position = position.moved(0, 1, 0); + file_editor.caret_mut().move_caret(position, pos.clone()); let mut new_file = EditorFile::new( current_file_path(file_editor), @@ -135,3 +140,31 @@ pub fn insert_new_line(file_editor: &mut FileEditor, renderer: &mut Renderer) { new_file.prepare_ui(renderer); file_editor.replace_current_file(new_file); } + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::support; + + #[test] + fn must_return_empty_string_when_no_file() { + let config = support::build_config(); + let mut editor = FileEditor::new(config); + let result = current_file_path(&mut editor); + assert_eq!(result, String::new()); + } + + #[test] + fn must_return_path_string_when_file_was_set() { + let config = support::build_config(); + let mut editor = FileEditor::new(Arc::clone(&config)); + let file = EditorFile::new( + "/foo/bar".to_owned(), + "hello world".to_owned(), + Arc::clone(&config), + ); + editor.open_file(file); + let result = current_file_path(&mut editor); + assert_eq!(result, "/foo/bar".to_owned()); + } +} diff --git a/src/app/mod.rs b/rider-editor/src/app/mod.rs similarity index 100% rename from src/app/mod.rs rename to rider-editor/src/app/mod.rs diff --git a/rider-editor/src/main.rs b/rider-editor/src/main.rs new file mode 100644 index 0000000..f864617 --- /dev/null +++ b/rider-editor/src/main.rs @@ -0,0 +1,57 @@ +extern crate dirs; +#[macro_use] +extern crate log; +extern crate rand; +extern crate rider_config; +extern crate rider_lexers; +extern crate rider_themes; +extern crate sdl2; +extern crate serde; +extern crate serde_derive; +extern crate serde_json; +extern crate simplelog; + +use crate::app::Application; +use rider_config::directories::Directories; +use simplelog::*; +use std::fs::File; + +pub mod app; +pub mod renderer; +#[cfg(test)] +pub mod tests; +pub mod ui; + +#[cfg_attr(tarpaulin, skip)] +fn init_logger(directories: &Directories) { + use simplelog::SharedLogger; + + let mut log_file_path = directories.log_dir.clone(); + log_file_path.push("rider.log"); + + let mut outputs: Vec> = vec![WriteLogger::new( + LevelFilter::Info, + Config::default(), + File::create(log_file_path).unwrap(), + )]; + let terminal_level = if cfg!(release) { + LevelFilter::Trace + } else { + LevelFilter::Debug + }; + if let Some(term) = TermLogger::new(terminal_level, Config::default()) { + outputs.push(term); + } + + CombinedLogger::init(outputs).unwrap(); +} + +#[cfg_attr(tarpaulin, skip)] +fn main() { + let directories = Directories::new(None, None); + let mut app = Application::new(); + app.init(); + init_logger(&directories); + app.open_file("./test_files/test.rs".to_string()); + app.run(); +} diff --git a/src/renderer/managers.rs b/rider-editor/src/renderer/managers.rs similarity index 75% rename from src/renderer/managers.rs rename to rider-editor/src/renderer/managers.rs index 673643a..ad6afdd 100644 --- a/src/renderer/managers.rs +++ b/rider-editor/src/renderer/managers.rs @@ -2,7 +2,8 @@ use sdl2::image::LoadTexture; use sdl2::pixels::Color; use sdl2::render::{Texture, TextureCreator}; use sdl2::ttf::{Font, Sdl2TtfContext}; -use sdl2::video::WindowContext as WinCtxt; +//use sdl2::video::WindowContext as WinCtxt; +use rider_config::editor_config::EditorConfig; use std::borrow::Borrow; use std::collections::HashMap; #[allow(unused_imports)] @@ -10,22 +11,41 @@ use std::env; use std::hash::Hash; use std::rc::Rc; +#[cfg_attr(tarpaulin, skip)] //noinspection RsWrongLifetimeParametersNumber pub type RcTex<'l> = Rc>; +#[cfg_attr(tarpaulin, skip)] pub type RcFont<'l> = Rc>; +#[cfg_attr(tarpaulin, skip)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct TextCharacterDetails { + pub c: char, + pub font_path: String, + pub font_size: u16, +} + +#[cfg_attr(tarpaulin, skip)] pub trait ResourceLoader<'l, R> { type Args: ?Sized; fn load(&'l self, data: &Self::Args) -> Result; } +#[cfg_attr(tarpaulin, skip)] #[derive(Debug, Hash, Eq, PartialEq, Clone)] pub struct FontDetails { pub path: String, pub size: u16, } +impl From<&EditorConfig> for FontDetails { + fn from(config: &EditorConfig) -> Self { + FontDetails::new(config.font_path().as_str(), config.character_size().clone()) + } +} + +#[cfg_attr(tarpaulin, skip)] #[derive(Debug, Hash, Eq, PartialEq, Clone)] pub struct TextDetails { pub text: String, @@ -33,6 +53,7 @@ pub struct TextDetails { pub font: FontDetails, } +#[cfg_attr(tarpaulin, skip)] impl TextDetails { pub fn get_cache_key(&self) -> String { format!( @@ -43,6 +64,7 @@ impl TextDetails { } } +#[cfg_attr(tarpaulin, skip)] impl<'a> From<&'a TextDetails> for TextDetails { fn from(details: &'a Self) -> Self { Self { @@ -53,6 +75,7 @@ impl<'a> From<&'a TextDetails> for TextDetails { } } +#[cfg_attr(tarpaulin, skip)] impl FontDetails { pub fn new(path: &str, size: u16) -> FontDetails { Self { @@ -62,6 +85,7 @@ impl FontDetails { } } +#[cfg_attr(tarpaulin, skip)] impl<'a> From<&'a FontDetails> for FontDetails { fn from(details: &'a FontDetails) -> Self { Self { @@ -71,16 +95,21 @@ impl<'a> From<&'a FontDetails> for FontDetails { } } +#[cfg_attr(tarpaulin, skip)] //noinspection RsWrongLifetimeParametersNumber -pub type TextureManager<'l, T> = ResourceManager<'l, String, Texture<'l>, TextureCreator>; +pub type TextureManager<'l> = + ResourceManager<'l, String, Texture<'l>, TextureCreator>; +#[cfg_attr(tarpaulin, skip)] pub type FontManager<'l> = ResourceManager<'l, FontDetails, Font<'l, 'static>, Sdl2TtfContext>; +#[cfg_attr(tarpaulin, skip)] pub trait ManagersHolder<'l> { fn font_manager(&mut self) -> &mut FontManager<'l>; - fn texture_manager(&mut self) -> &mut TextureManager<'l, WinCtxt>; + fn texture_manager(&mut self) -> &mut TextureManager<'l>; } +#[cfg_attr(tarpaulin, skip)] #[derive(Clone)] pub struct ResourceManager<'l, K, R, L> where @@ -91,6 +120,7 @@ where cache: HashMap>, } +#[cfg_attr(tarpaulin, skip)] impl<'l, K, R, L> ResourceManager<'l, K, R, L> where K: Hash + Eq, @@ -124,6 +154,7 @@ where } } +#[cfg_attr(tarpaulin, skip)] //noinspection RsWrongLifetimeParametersNumber impl<'l, T> ResourceLoader<'l, Texture<'l>> for TextureCreator { type Args = str; @@ -134,6 +165,7 @@ impl<'l, T> ResourceLoader<'l, Texture<'l>> for TextureCreator { } } +#[cfg_attr(tarpaulin, skip)] impl<'l> ResourceLoader<'l, Font<'l, 'static>> for Sdl2TtfContext { type Args = FontDetails; @@ -143,6 +175,7 @@ impl<'l> ResourceLoader<'l, Font<'l, 'static>> for Sdl2TtfContext { } } +#[cfg_attr(tarpaulin, skip)] pub trait TextTextureManager<'l> { //noinspection RsWrongLifetimeParametersNumber fn load_text( @@ -152,7 +185,8 @@ pub trait TextTextureManager<'l> { ) -> Result>, String>; } -impl<'l, T> TextTextureManager<'l> for TextureManager<'l, T> { +#[cfg_attr(tarpaulin, skip)] +impl<'l> TextTextureManager<'l> for TextureManager<'l> { //noinspection RsWrongLifetimeParametersNumber fn load_text( &mut self, @@ -169,9 +203,9 @@ impl<'l, T> TextTextureManager<'l> for TextureManager<'l, T> { let texture = self.loader.create_texture_from_surface(&surface).unwrap(); let resource = Rc::new(texture); self.cache.insert(key, resource.clone()); - for c in details.text.chars() { - info!("texture for '{:?}' created", c); - } + // for c in details.text.chars() { + // info!("texture for '{:?}' created", c); + // } Ok(resource) }, Ok, diff --git a/src/renderer/mod.rs b/rider-editor/src/renderer/mod.rs similarity index 100% rename from src/renderer/mod.rs rename to rider-editor/src/renderer/mod.rs diff --git a/rider-editor/src/renderer/renderer.rs b/rider-editor/src/renderer/renderer.rs new file mode 100644 index 0000000..cb1aa7b --- /dev/null +++ b/rider-editor/src/renderer/renderer.rs @@ -0,0 +1,83 @@ +use crate::renderer::managers::*; +use crate::ui::get_text_character_rect; +use crate::ui::text_character::CharacterSizeManager; +use rider_config::{ConfigAccess, ConfigHolder}; +use sdl2::rect::Rect; +use sdl2::render::TextureCreator; +use sdl2::ttf::Sdl2TtfContext; +use std::collections::HashMap; + +#[cfg_attr(tarpaulin, skip)] +pub struct Renderer<'l> { + config: ConfigAccess, + font_manager: FontManager<'l>, + texture_manager: TextureManager<'l>, + character_sizes: HashMap, +} + +#[cfg_attr(tarpaulin, skip)] +impl<'l> Renderer<'l> { + pub fn new( + config: ConfigAccess, + font_context: &'l Sdl2TtfContext, + texture_creator: &'l TextureCreator, + ) -> Self { + let texture_manager = TextureManager::new(&texture_creator); + let font_manager = FontManager::new(&font_context); + Self { + config, + font_manager, + texture_manager, + character_sizes: HashMap::new(), + } + } + + pub fn character_sizes_mut(&mut self) -> &mut HashMap { + &mut self.character_sizes + } +} + +impl<'l> CharacterSizeManager for Renderer<'l> { + fn load_character_size(&mut self, c: char) -> Rect { + let (font_path, font_size) = { + let config = self.config().read().unwrap(); + ( + config.editor_config().font_path().clone(), + config.editor_config().character_size().clone(), + ) + }; + let details = TextCharacterDetails { + c: c.clone(), + font_path, + font_size, + }; + self.character_sizes + .get(&details) + .cloned() + .or_else(|| { + let size = get_text_character_rect(c, self).unwrap(); + self.character_sizes.insert(details.clone(), size.clone()); + Some(size) + }) + .unwrap() + .clone() + } +} + +#[cfg_attr(tarpaulin, skip)] +impl<'l> ManagersHolder<'l> for Renderer<'l> { + fn font_manager(&mut self) -> &mut FontManager<'l> { + &mut self.font_manager + } + + fn texture_manager(&mut self) -> &mut TextureManager<'l> { + &mut self.texture_manager + } +} + +#[cfg_attr(tarpaulin, skip)] +impl<'l> ConfigHolder for Renderer<'l> { + fn config(&self) -> &ConfigAccess { + &self.config + } +} diff --git a/rider-editor/src/tests.rs b/rider-editor/src/tests.rs new file mode 100644 index 0000000..345bdcc --- /dev/null +++ b/rider-editor/src/tests.rs @@ -0,0 +1,11 @@ +#[cfg(test)] +pub mod support { + use rider_config::Config; + use std::sync::*; + + pub fn build_config() -> Arc> { + let mut config = Config::new(); + config.set_theme(config.editor_config().current_theme().clone()); + Arc::new(RwLock::new(config)) + } +} diff --git a/src/ui/caret/caret.rs b/rider-editor/src/ui/caret/caret.rs similarity index 93% rename from src/ui/caret/caret.rs rename to rider-editor/src/ui/caret/caret.rs index fe627b3..7efb947 100644 --- a/src/ui/caret/caret.rs +++ b/rider-editor/src/ui/caret/caret.rs @@ -1,13 +1,9 @@ use crate::app::{UpdateResult as UR, WindowCanvas as WC}; -use crate::config::*; use crate::renderer::*; -use crate::ui::text_character::TextCharacter; use crate::ui::*; -use sdl2::pixels::Color; +use rider_config::ConfigAccess; use sdl2::rect::{Point, Rect}; -use sdl2::render::Texture; use std::ops::Deref; -use std::sync::*; #[derive(Clone, Debug, PartialEq)] pub struct Caret { @@ -70,13 +66,13 @@ impl Deref for Caret { } } +#[cfg_attr(tarpaulin, skip)] impl Render for Caret { fn render(&self, canvas: &mut WC, _renderer: &mut Renderer, context: &RenderContext) { use std::borrow::*; - use std::option::*; let dest = match context.borrow() { - RenderContext::RelativePosition(p) => move_render_point(p.clone(), self.dest()), + RenderContext::RelativePosition(p) => move_render_point(p.clone(), &self.dest), _ => self.dest().clone(), }; let start = Point::new(dest.x(), dest.y()); @@ -121,11 +117,12 @@ impl ClickHandler for Caret { } fn is_left_click_target(&self, point: &Point, context: &UpdateContext) -> bool { + let dest = self.dest(); is_in_rect( point, &match context { - &UpdateContext::ParentPosition(p) => move_render_point(p, self.dest()), - _ => self.dest().clone(), + &UpdateContext::ParentPosition(p) => move_render_point(p, &dest), + _ => dest, }, ) } @@ -136,18 +133,16 @@ impl RenderBox for Caret { self.dest().top_left() } - fn dest(&self) -> &Rect { - &self.dest + fn dest(&self) -> Rect { + self.dest.clone() } } #[cfg(test)] mod test_own_methods { - use crate::renderer::*; use crate::tests::*; use crate::ui::*; - use sdl2::pixels::*; - use sdl2::rect::*; + use sdl2::rect::{Point, Rect}; use std::sync::*; #[test] @@ -204,11 +199,9 @@ mod test_own_methods { #[cfg(test)] mod test_deref { - use crate::renderer::*; use crate::tests::*; use crate::ui::*; - use sdl2::pixels::*; - use sdl2::rect::*; + use sdl2::rect::Point; use std::sync::*; #[test] @@ -244,11 +237,9 @@ mod test_deref { #[cfg(test)] mod test_render_box { - use crate::renderer::*; use crate::tests::*; use crate::ui::*; - use sdl2::pixels::*; - use sdl2::rect::*; + use sdl2::rect::Point; use std::sync::*; #[test] @@ -264,11 +255,9 @@ mod test_render_box { #[cfg(test)] mod test_click_handler { use crate::app::*; - use crate::renderer::*; use crate::tests::*; use crate::ui::*; - use sdl2::pixels::*; - use sdl2::rect::*; + use sdl2::rect::Point; use std::sync::*; #[test] diff --git a/src/ui/caret/caret_color.rs b/rider-editor/src/ui/caret/caret_color.rs similarity index 97% rename from src/ui/caret/caret_color.rs rename to rider-editor/src/ui/caret/caret_color.rs index 98ec5d8..0e1b5d4 100644 --- a/src/ui/caret/caret_color.rs +++ b/rider-editor/src/ui/caret/caret_color.rs @@ -23,7 +23,6 @@ impl CaretColor { #[cfg(test)] mod test_getters { use super::*; - use sdl2::pixels::*; #[test] fn assert_bright() { diff --git a/src/ui/caret/caret_position.rs b/rider-editor/src/ui/caret/caret_position.rs similarity index 100% rename from src/ui/caret/caret_position.rs rename to rider-editor/src/ui/caret/caret_position.rs diff --git a/src/ui/caret/mod.rs b/rider-editor/src/ui/caret/mod.rs similarity index 100% rename from src/ui/caret/mod.rs rename to rider-editor/src/ui/caret/mod.rs diff --git a/src/ui/file/editor_file.rs b/rider-editor/src/ui/file/editor_file.rs similarity index 96% rename from src/ui/file/editor_file.rs rename to rider-editor/src/ui/file/editor_file.rs index 51f9d2e..8741d8a 100644 --- a/src/ui/file/editor_file.rs +++ b/rider-editor/src/ui/file/editor_file.rs @@ -1,13 +1,12 @@ use sdl2::rect::{Point, Rect}; -use std::rc::Rc; use std::sync::*; use crate::app::{UpdateResult as UR, WindowCanvas as WC}; -use crate::config::Config; use crate::renderer::Renderer; use crate::ui::file::editor_file_section::EditorFileSection; use crate::ui::text_character::TextCharacter; use crate::ui::*; +use rider_config::Config; #[derive(Clone, Debug)] pub struct EditorFile { @@ -114,6 +113,7 @@ impl TextCollection for EditorFile { } } +#[cfg_attr(tarpaulin, skip)] impl Render for EditorFile { fn render(&self, canvas: &mut WC, renderer: &mut Renderer, context: &RenderContext) { for section in self.sections.iter() { @@ -196,21 +196,16 @@ impl RenderBox for EditorFile { self.dest.top_left() } - fn dest(&self) -> &Rect { - &self.dest + fn dest(&self) -> Rect { + self.dest.clone() } } #[cfg(test)] mod test_render_box { - use crate::app::*; use crate::tests::support; use crate::ui::*; - use sdl2::rect::*; - use sdl2::*; - use std::borrow::*; - use std::rc::*; - use std::sync::*; + use sdl2::rect::{Point, Rect}; #[test] fn assert_dest() { diff --git a/src/ui/file/editor_file_section.rs b/rider-editor/src/ui/file/editor_file_section.rs similarity index 96% rename from src/ui/file/editor_file_section.rs rename to rider-editor/src/ui/file/editor_file_section.rs index a2e24a4..c972616 100644 --- a/src/ui/file/editor_file_section.rs +++ b/rider-editor/src/ui/file/editor_file_section.rs @@ -1,15 +1,14 @@ use sdl2::rect::{Point, Rect}; -use std::cell::Cell; -use std::rc::Rc; use std::sync::*; use crate::app::{UpdateResult as UR, WindowCanvas as WC}; -use crate::config::Config; -use crate::lexer::Language; use crate::renderer::Renderer; use crate::ui::file::editor_file_token::EditorFileToken; use crate::ui::text_character::TextCharacter; use crate::ui::*; +use rider_config::Config; +use rider_lexers; +use rider_lexers::Language; #[derive(Clone, Debug)] pub struct EditorFileSection { @@ -20,8 +19,6 @@ pub struct EditorFileSection { impl EditorFileSection { pub fn new(buffer: String, ext: String, config: Arc>) -> Self { - use crate::lexer; - let language = config .read() .unwrap() @@ -29,7 +26,7 @@ impl EditorFileSection { .get(ext.as_str()) .unwrap_or(&Language::PlainText) .clone(); - let lexer_tokens = lexer::parse(buffer.clone(), &language); + let lexer_tokens = rider_lexers::parse(buffer.clone(), language); let mut tokens: Vec = vec![]; let mut iterator = lexer_tokens.iter().peekable(); @@ -128,6 +125,7 @@ impl TextCollection for EditorFileSection { } } +#[cfg_attr(tarpaulin, skip)] impl Render for EditorFileSection { fn render(&self, canvas: &mut WC, renderer: &mut Renderer, context: &RenderContext) { for token in self.tokens.iter() { diff --git a/src/ui/file/editor_file_token.rs b/rider-editor/src/ui/file/editor_file_token.rs similarity index 92% rename from src/ui/file/editor_file_token.rs rename to rider-editor/src/ui/file/editor_file_token.rs index 3625e4d..b47bdf9 100644 --- a/src/ui/file/editor_file_token.rs +++ b/rider-editor/src/ui/file/editor_file_token.rs @@ -1,38 +1,17 @@ use crate::app::{UpdateResult as UR, WindowCanvas as WC}; -use crate::config::*; -use crate::lexer::TokenType; -use crate::renderer::managers::{FontDetails, TextDetails}; use crate::renderer::*; use crate::ui::*; +use rider_config::Config; +use rider_lexers::TokenType; use sdl2::pixels::Color; use sdl2::rect::{Point, Rect}; -use sdl2::render::Texture; -use sdl2::ttf::Font; -use std::rc::Rc; use std::sync::*; -impl TokenType { - pub fn to_color(&self, config: &Arc>) -> Color { - let config = config.read().unwrap(); - let ch = config.theme().code_highlighting(); - match self { - &TokenType::Whitespace { .. } => ch.whitespace().color().into(), - &TokenType::Keyword { .. } => ch.keyword().color().into(), - &TokenType::String { .. } => ch.string().color().into(), - &TokenType::Identifier { .. } => ch.identifier().color().into(), - &TokenType::Literal { .. } => ch.literal().color().into(), - &TokenType::Comment { .. } => ch.comment().color().into(), - &TokenType::Operator { .. } => ch.operator().color().into(), - &TokenType::Separator { .. } => ch.separator().color().into(), - } - } -} - #[derive(Clone, Debug)] pub struct EditorFileToken { last_in_line: bool, characters: Vec, - token_type: Rc, + token_type: TokenType, config: Arc>, } @@ -41,7 +20,7 @@ impl EditorFileToken { Self { last_in_line, characters: vec![], - token_type: Rc::new(token_type.clone()), + token_type: token_type.clone(), config, } } @@ -59,6 +38,21 @@ impl EditorFileToken { text_character.update_position(current); } } + + fn token_to_color(&self, config: &Arc>) -> Color { + let config = config.read().unwrap(); + let ch = config.theme().code_highlighting(); + match &self.token_type { + &TokenType::Whitespace { .. } => ch.whitespace().color().into(), + &TokenType::Keyword { .. } => ch.keyword().color().into(), + &TokenType::String { .. } => ch.string().color().into(), + &TokenType::Identifier { .. } => ch.identifier().color().into(), + &TokenType::Literal { .. } => ch.literal().color().into(), + &TokenType::Comment { .. } => ch.comment().color().into(), + &TokenType::Operator { .. } => ch.operator().color().into(), + &TokenType::Separator { .. } => ch.separator().color().into(), + } + } } impl TextWidget for EditorFileToken { @@ -129,6 +123,7 @@ impl TextCollection for EditorFileToken { } } +#[cfg_attr(tarpaulin, skip)] impl Render for EditorFileToken { /** * Must first create targets so even if new line appear renderer will know @@ -147,7 +142,7 @@ impl Render for EditorFileToken { if !self.characters.is_empty() { return; } - let color: Color = self.token_type.to_color(renderer.config()); + let color: Color = self.token_to_color(&renderer.config()); let chars: Vec = self.token_type.text().chars().collect(); for (index, c) in chars.iter().enumerate() { let last_in_line = self.last_in_line && index + 1 == chars.len(); diff --git a/src/ui/file/mod.rs b/rider-editor/src/ui/file/mod.rs similarity index 100% rename from src/ui/file/mod.rs rename to rider-editor/src/ui/file/mod.rs diff --git a/src/ui/file_editor/file_editor.rs b/rider-editor/src/ui/file_editor/file_editor.rs similarity index 95% rename from src/ui/file_editor/file_editor.rs rename to rider-editor/src/ui/file_editor/file_editor.rs index 3d5f1e0..683aac1 100644 --- a/src/ui/file_editor/file_editor.rs +++ b/rider-editor/src/ui/file_editor/file_editor.rs @@ -1,15 +1,12 @@ -use sdl2::pixels::*; -use sdl2::rect::*; -use std::borrow::*; -use std::mem; -use std::sync::*; - use crate::app::*; use crate::app::{UpdateResult as UR, WindowCanvas as WS}; use crate::ui::scroll_bar::horizontal_scroll_bar::*; use crate::ui::scroll_bar::vertical_scroll_bar::*; use crate::ui::scroll_bar::Scrollable; use crate::ui::*; +use sdl2::rect::{Point, Rect}; +use std::mem; +use std::sync::*; pub struct FileEditor { dest: Rect, @@ -84,7 +81,7 @@ impl FileEditor { } impl ScrollableView for FileEditor { - fn scroll_to(&mut self, x: i32, y: i32) { + fn scroll_by(&mut self, x: i32, y: i32) { let read_config = self.config.read().unwrap(); let value_x = read_config.scroll().speed() * x; @@ -133,6 +130,8 @@ impl FileAccess for FileEditor { if let Some(f) = self.file.as_ref() { self.full_rect = f.full_rect(); } + self.vertical_scroll_bar.set_location(0); + self.horizontal_scroll_bar.set_location(0); file } @@ -162,7 +161,7 @@ impl CaretAccess for FileEditor { fn move_caret(&mut self, dir: MoveDirection) { match dir { - MoveDirection::Left => {} + MoveDirection::Left => caret_manager::move_caret_left(self), MoveDirection::Right => caret_manager::move_caret_right(self), MoveDirection::Up => {} MoveDirection::Down => {} @@ -197,6 +196,7 @@ impl CaretAccess for FileEditor { } } +#[cfg_attr(tarpaulin, skip)] impl Render for FileEditor { fn render(&self, canvas: &mut WS, renderer: &mut Renderer, _context: &RenderContext) { canvas.set_clip_rect(self.dest.clone()); @@ -215,12 +215,10 @@ impl Render for FileEditor { ); self.vertical_scroll_bar.render( canvas, - renderer, &RenderContext::RelativePosition(self.dest.top_left()), ); self.horizontal_scroll_bar.render( canvas, - renderer, &RenderContext::RelativePosition(self.dest.top_left()), ); } @@ -301,8 +299,8 @@ impl RenderBox for FileEditor { self.dest.top_left() + self.scroll() } - fn dest(&self) -> &Rect { - &self.dest + fn dest(&self) -> Rect { + self.dest.clone() } } @@ -314,12 +312,8 @@ impl ConfigHolder for FileEditor { #[cfg(test)] mod tests { - use crate::app::*; use crate::ui::*; - use sdl2::rect::*; - use sdl2::*; - use std::borrow::*; - use std::rc::*; + use rider_config::Config; use std::sync::*; #[test] @@ -344,10 +338,6 @@ mod test_config_holder { use crate::app::*; use crate::tests::support; use crate::ui::*; - use sdl2::rect::*; - use sdl2::*; - use std::borrow::*; - use std::rc::*; use std::sync::*; #[test] @@ -369,14 +359,9 @@ mod test_config_holder { #[cfg(test)] mod test_render_box { - use crate::app::*; use crate::tests::support; use crate::ui::*; - use sdl2::rect::*; - use sdl2::*; - use std::borrow::*; - use std::rc::*; - use std::sync::*; + use sdl2::rect::{Point, Rect}; impl FileEditor { pub fn set_full_rect(&mut self, r: Rect) { @@ -421,7 +406,7 @@ mod test_render_box { widget.set_dest(Rect::new(x.clone(), y.clone(), 999, 999)); widget.set_full_rect(Rect::new(0, 0, 99999, 99999)); widget.update(1, &UpdateContext::Nothing); - widget.scroll_to(30, 40); + widget.scroll_by(30, 40); let result = widget.render_start_point().clone(); let expected = Point::new(x - (ss * 30), y - (ss * 40)); assert_eq!(result, expected); diff --git a/src/ui/file_editor/mod.rs b/rider-editor/src/ui/file_editor/mod.rs similarity index 91% rename from src/ui/file_editor/mod.rs rename to rider-editor/src/ui/file_editor/mod.rs index 5ed267c..1c3e406 100644 --- a/src/ui/file_editor/mod.rs +++ b/rider-editor/src/ui/file_editor/mod.rs @@ -1,10 +1,9 @@ +pub use crate::ui::file_editor::file_editor::*; use crate::ui::*; -use sdl2::rect::*; +use sdl2::rect::Point; pub mod file_editor; -pub use crate::ui::file_editor::file_editor::*; - pub trait FileAccess { fn has_file(&self) -> bool; @@ -30,7 +29,7 @@ pub trait CaretAccess { } pub trait ScrollableView { - fn scroll_to(&mut self, x: i32, y: i32); + fn scroll_by(&mut self, x: i32, y: i32); fn scroll(&self) -> Point; } diff --git a/rider-editor/src/ui/filesystem/directory.rs b/rider-editor/src/ui/filesystem/directory.rs new file mode 100644 index 0000000..89f0638 --- /dev/null +++ b/rider-editor/src/ui/filesystem/directory.rs @@ -0,0 +1,432 @@ +use crate::app::*; +use crate::renderer::*; +use crate::ui::*; +use sdl2::pixels::Color; +use sdl2::rect::{Point, Rect}; +use std::fs; +use std::path; +use std::sync::Arc; + +const CHILD_MARGIN: i32 = 4; +const DEFAULT_ICON_SIZE: u32 = 16; + +pub struct DirectoryView { + opened: bool, + expanded: bool, + name_width: u32, + icon_width: u32, + icon_height: u32, + height: u32, + path: String, + files: Vec, + directories: Vec, + pos: Point, + source: Rect, + config: ConfigAccess, +} + +impl DirectoryView { + pub fn new(path: String, config: ConfigAccess) -> Self { + Self { + opened: false, + expanded: false, + name_width: 0, + icon_width: DEFAULT_ICON_SIZE, + icon_height: DEFAULT_ICON_SIZE, + height: 0, + path, + files: vec![], + directories: vec![], + pos: Point::new(0, 0), + source: Rect::new(0, 0, 64, 64), + config, + } + } + + pub fn path(&self) -> String { + self.path.clone() + } + + pub fn dest(&self) -> Rect { + match self.expanded { + true => Rect::new( + self.pos.x(), + self.pos.y(), + self.icon_width + self.name_width + NAME_MARGIN as u32, + self.height, + ), + false => Rect::new( + self.pos.x(), + self.pos.y(), + self.icon_width + self.name_width + NAME_MARGIN as u32, + self.icon_height, + ), + } + } + + pub fn source(&self) -> &Rect { + &self.source + } + + pub fn open_directory(&mut self, dir_path: String, renderer: &mut Renderer) -> bool { + match dir_path { + _ if dir_path == self.path => { + if !self.opened { + self.opened = true; + self.expanded = true; + self.read_directory(renderer); + } else { + self.expanded = !self.expanded; + } + self.calculate_size(renderer); + true + } + _ if dir_path.contains((self.path.clone() + "/").as_str()) => { + if !self.opened { + self.opened = true; + self.expanded = true; + self.read_directory(renderer); + } + for dir in self.directories.iter_mut() { + if dir.open_directory(dir_path.clone(), renderer) { + break; + } + } + self.calculate_size(renderer); + true + } + _ => false, + } + } + + pub fn refresh(&mut self) { + unimplemented!() + } + + pub fn name(&self) -> String { + path::Path::new(&self.path) + .file_name() + .unwrap() + .to_str() + .unwrap() + .to_owned() + } + + pub fn name_width(&self) -> u32 { + self.name_width + } + + pub fn icon_width(&self) -> u32 { + self.icon_width + } + + pub fn height(&self) -> u32 { + match self.expanded { + true => self.height, + false => self.icon_height, + } + } + + fn read_directory(&mut self, renderer: &mut Renderer) { + let entries: fs::ReadDir = match fs::read_dir(self.path.clone()) { + Ok(d) => d, + _ => return, + }; + for e in entries { + let entry = match e { + Ok(entry) => entry, + _ => continue, + }; + let meta = match entry.metadata() { + Ok(meta) => meta, + _ => continue, + }; + if meta.is_dir() { + let path = match entry.path().to_str() { + Some(p) => p.to_string(), + _ => continue, + }; + let mut directory_view = DirectoryView::new(path, Arc::clone(&self.config)); + directory_view.prepare_ui(renderer); + self.directories.push(directory_view); + } else if meta.is_file() { + let file_name = match entry.file_name().to_str() { + Some(p) => p.to_string(), + _ => continue, + }; + let path = match entry.path().to_str() { + Some(p) => p.to_string(), + _ => continue, + }; + let mut file_entry = FileEntry::new(file_name, path, Arc::clone(&self.config)); + file_entry.prepare_ui(renderer); + self.files.push(file_entry); + } + } + self.files.sort_by(|a, b| a.name().cmp(&b.name())); + self.directories.sort_by(|a, b| a.name().cmp(&b.name())); + } + + fn render_icon(&self, canvas: &mut T, renderer: &mut Renderer, dest: &mut Rect) + where + T: RenderImage, + { + let dir_texture_path = { + let c = self.config.read().unwrap(); + let mut themes_dir = c.directories().themes_dir.clone(); + let path = c.theme().images().directory_icon(); + themes_dir.push(path); + themes_dir.to_str().unwrap().to_owned() + }; + let texture = renderer + .texture_manager() + .load(dir_texture_path.as_str()) + .unwrap_or_else(|_| panic!("Failed to load directory entry texture")); + + canvas + .render_image( + texture, + self.source.clone(), + Rect::new(dest.x(), dest.y(), self.icon_width, self.icon_height), + ) + .unwrap_or_else(|_| panic!("Failed to draw directory entry texture")); + } + + fn render_name(&self, canvas: &mut T, renderer: &mut Renderer, dest: &mut Rect) + where + T: RenderImage, + { + let mut d = dest.clone(); + d.set_x(dest.x() + NAME_MARGIN); + let font_details = build_font_details(self); + let font = renderer.font_manager().load(&font_details).unwrap(); + let name = self.name(); + let config = self.config.read().unwrap(); + let text_color = config.theme().code_highlighting().title.color(); + + for c in name.chars() { + let size = renderer.load_character_size(c.clone()); + let mut text_details = TextDetails { + color: Color::RGBA(text_color.r, text_color.g, text_color.b, text_color.a), + text: c.to_string(), + font: font_details.clone(), + }; + let text_texture = renderer + .texture_manager() + .load_text(&mut text_details, &font) + .unwrap(); + d.set_width(size.width()); + d.set_height(size.height()); + + canvas + .render_image(text_texture, self.source.clone(), d.clone()) + .unwrap_or_else(|_| panic!("Failed to draw directory entry texture")); + d.set_x(d.x() + size.width() as i32); + } + } + + fn render_children(&self, canvas: &mut T, renderer: &mut Renderer, dest: &mut Rect) + where + T: RenderImage, + { + if !self.expanded { + return; + } + let mut point = dest.top_left() + + Point::new( + self.icon_width as i32 + CHILD_MARGIN, + self.icon_height as i32 + CHILD_MARGIN, + ); + for dir in self.directories.iter() { + let context = RenderContext::RelativePosition(point.clone()); + dir.render(canvas, renderer, &context); + point = point + Point::new(0, dir.height() as i32 + CHILD_MARGIN as i32); + } + for file in self.files.iter() { + let context = RenderContext::RelativePosition(point.clone()); + file.render(canvas, renderer, &context); + point = point + Point::new(0, file.height() as i32 + CHILD_MARGIN as i32); + } + } + + fn calculate_size(&mut self, renderer: &mut Renderer) { + let size = renderer.load_character_size('W'); + self.height = size.height(); + self.icon_height = size.height(); + self.icon_width = size.height(); + self.name_width = 0; + + for c in self.name().chars() { + let size = renderer.load_character_size(c.clone()); + self.name_width += size.width(); + } + + for dir in self.directories.iter_mut() { + self.height = self.height + dir.height() + CHILD_MARGIN as u32; + } + for file in self.files.iter_mut() { + self.height = self.height + file.height() + CHILD_MARGIN as u32; + } + } + + fn name_and_icon_rect(&self) -> Rect { + Rect::new( + self.pos.x(), + self.pos.y(), + self.icon_width + self.name_width + NAME_MARGIN as u32, + self.icon_height, + ) + } +} + +impl ConfigHolder for DirectoryView { + fn config(&self) -> &ConfigAccess { + &self.config + } +} + +#[cfg_attr(tarpaulin, skip)] +impl DirectoryView { + pub fn render(&self, canvas: &mut T, renderer: &mut Renderer, context: &RenderContext) + where + T: RenderImage, + { + let dest = self.dest(); + let move_point = match context { + &RenderContext::RelativePosition(p) => p.clone(), + _ => Point::new(0, 0), + }; + let mut dest = move_render_point(move_point, &dest); + self.render_icon::(canvas, renderer, &mut dest); + self.render_name::(canvas, renderer, &mut dest.clone()); + self.render_children::(canvas, renderer, &mut dest); + } + + pub fn prepare_ui(&mut self, renderer: &mut Renderer) { + if self.opened { + for dir in self.directories.iter_mut() { + dir.prepare_ui(renderer); + } + for file in self.files.iter_mut() { + file.prepare_ui(renderer); + } + } + self.calculate_size(renderer); + } +} + +impl Update for DirectoryView { + fn update(&mut self, ticks: i32, context: &UpdateContext) -> UpdateResult { + if !path::Path::new(&self.path).exists() { + return UpdateResult::RefreshFsTree; + } + if self.opened { + for dir in self.directories.iter_mut() { + dir.update(ticks, context); + } + for file in self.files.iter_mut() { + file.update(ticks, context); + } + } + UpdateResult::NoOp + } +} + +impl RenderBox for DirectoryView { + fn render_start_point(&self) -> Point { + self.pos.clone() + } + + fn dest(&self) -> Rect { + Rect::new( + self.pos.x(), + self.pos.y(), + self.icon_width, + self.icon_height, + ) + } +} + +impl ClickHandler for DirectoryView { + fn on_left_click(&mut self, point: &Point, context: &UpdateContext) -> UpdateResult { + let dest = self.dest(); + let move_point = match context { + &UpdateContext::ParentPosition(p) => p.clone(), + _ => Point::new(0, 0), + }; + let dest = move_render_point(move_point.clone(), &dest); + + // icon or name is target of click + let icon_or_name = self.name_and_icon_rect(); + if move_render_point(move_point, &icon_or_name).contains_point(point.clone()) { + return UpdateResult::OpenDirectory(self.path.clone()); + } + + if !self.expanded { + return UpdateResult::NoOp; + } + + let mut p = dest.top_left() + + Point::new( + self.icon_width as i32 + CHILD_MARGIN, + self.icon_height as i32 + CHILD_MARGIN, + ); + for dir in self.directories.iter_mut() { + let context = UpdateContext::ParentPosition(p.clone()); + if dir.is_left_click_target(&point, &context) { + return dir.on_left_click(&point, &context); + } + p = p + Point::new(0, dir.height() as i32 + CHILD_MARGIN); + } + for file in self.files.iter_mut() { + let context = UpdateContext::ParentPosition(p.clone()); + if file.is_left_click_target(&point, &context) { + return file.on_left_click(&point, &context); + } + p = p + Point::new(0, file.height() as i32 + CHILD_MARGIN); + } + + UpdateResult::NoOp + } + + fn is_left_click_target(&self, point: &Point, context: &UpdateContext) -> bool { + let dest = self.dest(); + let move_point = match context { + UpdateContext::ParentPosition(p) => p.clone(), + _ => Point::new(0, 0), + }; + let dest = move_render_point(move_point.clone(), &dest); + + // icon or name is target of click + let name_and_icon_rect = self.name_and_icon_rect(); + if move_render_point(move_point.clone(), &name_and_icon_rect).contains_point(point.clone()) + { + return true; + } + if !self.expanded { + return false; + } + let mut p = dest.top_left() + + Point::new( + self.icon_width as i32 + CHILD_MARGIN, + self.icon_height as i32 + CHILD_MARGIN, + ); + // subdirectory is target of click + for dir in self.directories.iter() { + let context = UpdateContext::ParentPosition(p.clone()); + if dir.is_left_click_target(&point, &context) { + return true; + } + p = p + Point::new(0, dir.height() as i32 + CHILD_MARGIN); + } + // file inside directory is target of click + for file in self.files.iter() { + let context = UpdateContext::ParentPosition(p.clone()); + if file.is_left_click_target(&point, &context) { + return true; + } + p = p + Point::new(0, file.height() as i32 + CHILD_MARGIN); + } + false + } +} diff --git a/rider-editor/src/ui/filesystem/file.rs b/rider-editor/src/ui/filesystem/file.rs new file mode 100644 index 0000000..c90cd05 --- /dev/null +++ b/rider-editor/src/ui/filesystem/file.rs @@ -0,0 +1,204 @@ +use crate::app::*; +use crate::renderer::*; +use crate::ui::*; +use sdl2::pixels::Color; +use sdl2::rect::{Point, Rect}; +use std::collections::HashMap; +use std::path; + +pub struct FileEntry { + name_width: u32, + icon_width: u32, + height: u32, + name: String, + path: String, + dest: Rect, + source: Rect, + config: ConfigAccess, + char_sizes: HashMap, +} + +impl FileEntry { + pub fn new(name: String, path: String, config: ConfigAccess) -> Self { + Self { + name, + path, + name_width: 0, + icon_width: 0, + height: 0, + dest: Rect::new(0, 0, 16, 16), + source: Rect::new(0, 0, 64, 64), + config, + char_sizes: HashMap::new(), + } + } + + pub fn name_width(&self) -> u32 { + self.name_width + } + + pub fn icon_width(&self) -> u32 { + self.icon_width + } + + pub fn height(&self) -> u32 { + self.height + } + + pub fn name(&self) -> String { + self.name.clone() + } + + pub fn path(&self) -> String { + self.path.clone() + } + + pub fn dest(&self) -> &Rect { + &self.dest + } + + pub fn source(&self) -> &Rect { + &self.source + } + + pub fn full_dest(&self) -> Rect { + Rect::new( + self.dest.x(), + self.dest.y(), + self.icon_width + NAME_MARGIN as u32 + self.name_width, + self.height, + ) + } + + fn render_icon(&self, canvas: &mut T, renderer: &mut Renderer, dest: &mut Rect) + where + T: RenderImage, + { + let dir_texture_path = { + let c = self.config.read().unwrap(); + let mut themes_dir = c.directories().themes_dir.clone(); + let path = c.theme().images().file_icon(); + themes_dir.push(path); + themes_dir.to_str().unwrap().to_owned() + }; + let texture = renderer + .texture_manager() + .load(dir_texture_path.as_str()) + .unwrap_or_else(|_| panic!("Failed to load directory entry texture")); + dest.set_width(16); + dest.set_height(16); + canvas + .render_image(texture, self.source.clone(), dest.clone()) + .unwrap_or_else(|_| panic!("Failed to draw directory entry texture")); + } + + fn render_name(&self, canvas: &mut T, renderer: &mut Renderer, dest: &mut Rect) + where + T: RenderImage, + { + let mut d = dest.clone(); + d.set_x(dest.x() + NAME_MARGIN); + + let font_details = build_font_details(self); + let font = renderer.font_manager().load(&font_details).unwrap(); + let texture_manager = renderer.texture_manager(); + let name = self.name(); + + for c in name.chars() { + let size = self + .char_sizes + .get(&c) + .unwrap_or(&Rect::new(0, 0, 0, 0)) + .clone(); + let mut text_details = TextDetails { + color: Color::RGBA(255, 255, 255, 0), + text: c.to_string(), + font: font_details.clone(), + }; + let text_texture = texture_manager.load_text(&mut text_details, &font).unwrap(); + d.set_width(size.width()); + d.set_height(size.height()); + + canvas + .render_image(text_texture, self.source.clone(), d.clone()) + .unwrap_or_else(|_| panic!("Failed to draw directory entry texture")); + d.set_x(d.x() + size.width() as i32) + } + } +} + +impl ConfigHolder for FileEntry { + fn config(&self) -> &ConfigAccess { + &self.config + } +} + +#[cfg_attr(tarpaulin, skip)] +impl FileEntry { + pub fn render(&self, canvas: &mut T, renderer: &mut Renderer, context: &RenderContext) + where + T: RenderImage, + { + let mut dest = match context { + &RenderContext::RelativePosition(p) => move_render_point(p.clone(), &self.dest), + _ => self.dest.clone(), + }; + self.render_icon(canvas, renderer, &mut dest); + self.render_name(canvas, renderer, &mut dest.clone()); + } + + pub fn prepare_ui(&mut self, renderer: &mut Renderer) { + let w_rect = get_text_character_rect('W', renderer).unwrap(); + self.char_sizes.insert('W', w_rect.clone()); + self.height = w_rect.height(); + self.icon_width = w_rect.height(); + self.name_width = 0; + + for c in self.name().chars() { + let size = { get_text_character_rect(c.clone(), renderer).unwrap() }; + self.char_sizes.insert(c, size); + self.name_width += size.width(); + } + self.dest.set_width(w_rect.height()); + self.dest.set_height(w_rect.height()); + } +} + +impl Update for FileEntry { + fn update(&mut self, _ticks: i32, _context: &UpdateContext) -> UpdateResult { + if !path::Path::new(&self.path).exists() { + return UpdateResult::RefreshFsTree; + } + UpdateResult::NoOp + } +} + +impl RenderBox for FileEntry { + fn render_start_point(&self) -> Point { + self.dest.top_left() + } + + fn dest(&self) -> Rect { + self.dest.clone() + } +} + +impl ClickHandler for FileEntry { + fn on_left_click(&mut self, _point: &Point, _context: &UpdateContext) -> UpdateResult { + UpdateResult::OpenFile(self.path.clone()) + } + + fn is_left_click_target(&self, point: &Point, context: &UpdateContext) -> bool { + let dest = Rect::new( + self.dest.x(), + self.dest.y(), + self.icon_width + self.name_width + NAME_MARGIN as u32, + self.dest.height(), + ); + let rect = match context { + UpdateContext::ParentPosition(p) => move_render_point(p.clone(), &dest), + _ => dest, + }; + rect.contains_point(point.clone()) + } +} diff --git a/rider-editor/src/ui/filesystem/mod.rs b/rider-editor/src/ui/filesystem/mod.rs new file mode 100644 index 0000000..9e2843d --- /dev/null +++ b/rider-editor/src/ui/filesystem/mod.rs @@ -0,0 +1,7 @@ +pub use crate::ui::filesystem::directory::*; +pub use crate::ui::filesystem::file::*; + +pub mod directory; +pub mod file; + +pub const NAME_MARGIN: i32 = 20; diff --git a/src/ui/menu_bar.rs b/rider-editor/src/ui/menu_bar.rs similarity index 85% rename from src/ui/menu_bar.rs rename to rider-editor/src/ui/menu_bar.rs index 7c3b45f..021a0ca 100644 --- a/src/ui/menu_bar.rs +++ b/rider-editor/src/ui/menu_bar.rs @@ -1,11 +1,9 @@ use crate::app::{UpdateResult as UR, WindowCanvas as WC}; -use crate::config::*; use crate::renderer::*; use crate::ui::*; +use rider_config::ConfigAccess; use sdl2::pixels::Color; use sdl2::rect::{Point, Rect}; -use std::rc::Rc; -use std::sync::*; pub struct MenuBar { border_color: Color, @@ -16,12 +14,17 @@ pub struct MenuBar { impl MenuBar { pub fn new(config: ConfigAccess) -> Self { - let (background_color, w, h): (Color, u32, u16) = { + let (background_color, border_color, w, h): (Color, Color, u32, u16) = { let c = config.read().unwrap(); - (c.theme().background().into(), c.width(), c.menu_height()) + ( + c.theme().background().into(), + c.theme().border_color().into(), + c.width(), + c.menu_height(), + ) }; Self { - border_color: Color::RGB(10, 10, 10), + border_color, background_color, dest: Rect::new(0, 0, w as u32, h as u32), config, @@ -33,6 +36,7 @@ impl MenuBar { } } +#[cfg_attr(tarpaulin, skip)] impl Render for MenuBar { fn render(&self, canvas: &mut WC, _renderer: &mut Renderer, context: &RenderContext) { use std::borrow::*; @@ -41,23 +45,23 @@ impl Render for MenuBar { canvas.set_draw_color(self.background_color.clone()); canvas .fill_rect(match context.borrow() { - RenderContext::RelativePosition(p) => move_render_point(p.clone(), self.dest()), - _ => self.dest.clone(), + RenderContext::RelativePosition(p) => move_render_point(p.clone(), &self.dest), + _ => self.dest(), }) .unwrap_or_else(|_| panic!("Failed to draw main menu background")); - canvas.set_draw_color(self.border_color.clone()); + canvas.set_draw_color(self.border_color); canvas .draw_rect(match context.borrow() { - RenderContext::RelativePosition(p) => move_render_point(p.clone(), self.dest()), - _ => self.dest.clone(), + RenderContext::RelativePosition(p) => move_render_point((*p).clone(), &self.dest), + _ => self.dest(), }) .unwrap_or_else(|_| panic!("Failed to draw main menu background")); } fn prepare_ui(&mut self, _renderer: &mut Renderer) { let width = self.config.read().unwrap().width(); - let height = self.config.read().unwrap().menu_height() as u32; + let height = u32::from(self.config.read().unwrap().menu_height()); self.dest = Rect::new(0, 0, width, height); } } @@ -76,11 +80,11 @@ impl ClickHandler for MenuBar { } fn is_left_click_target(&self, point: &Point, context: &UpdateContext) -> bool { - let rect = match context.clone() { - UpdateContext::ParentPosition(p) => move_render_point(p.clone(), self.dest()), - _ => self.dest().clone(), + let rect = match *context { + UpdateContext::ParentPosition(p) => move_render_point(p.clone(), &self.dest), + _ => self.dest(), }; - is_in_rect(point, &rect) + rect.contains_point(point.clone()) } } @@ -89,19 +93,17 @@ impl RenderBox for MenuBar { self.dest.top_left() } - fn dest(&self) -> &Rect { - &self.dest + fn dest(&self) -> Rect { + self.dest } } #[cfg(test)] mod test_getters { - use crate::app::*; - use crate::renderer::*; use crate::tests::*; use crate::ui::*; - use sdl2::pixels::*; - use sdl2::rect::*; + use sdl2::pixels::Color; + use sdl2::rect::Rect; use std::sync::*; #[test] @@ -129,11 +131,9 @@ mod test_getters { #[cfg(test)] mod test_render_box { - use crate::renderer::*; use crate::tests::*; use crate::ui::*; - use sdl2::pixels::*; - use sdl2::rect::*; + use sdl2::rect::Point; use std::sync::*; #[test] @@ -149,11 +149,9 @@ mod test_render_box { #[cfg(test)] mod test_click_handler { use crate::app::*; - use crate::renderer::*; use crate::tests::*; use crate::ui::*; - use sdl2::pixels::*; - use sdl2::rect::*; + use sdl2::rect::Point; use std::sync::*; #[test] diff --git a/src/ui/mod.rs b/rider-editor/src/ui/mod.rs similarity index 69% rename from src/ui/mod.rs rename to rider-editor/src/ui/mod.rs index bbfe59f..198ba38 100644 --- a/src/ui/mod.rs +++ b/rider-editor/src/ui/mod.rs @@ -1,14 +1,20 @@ use sdl2::rect::{Point, Rect}; +use sdl2::render::Texture; +use std::rc::Rc; + +use crate::app::application::WindowCanvas; use crate::app::{UpdateResult as UR, WindowCanvas as WC}; -use crate::config::*; use crate::renderer::managers::*; use crate::renderer::Renderer; +use rider_config::*; pub mod caret; pub mod file; pub mod file_editor; +pub mod filesystem; pub mod menu_bar; +pub mod modal; pub mod project_tree; pub mod scroll_bar; pub mod text_character; @@ -16,7 +22,9 @@ pub mod text_character; pub use crate::ui::caret::*; pub use crate::ui::file::*; pub use crate::ui::file_editor::*; +pub use crate::ui::filesystem::*; pub use crate::ui::menu_bar::*; +pub use crate::ui::modal::*; pub use crate::ui::project_tree::*; pub use crate::ui::scroll_bar::*; pub use crate::ui::text_character::*; @@ -34,6 +42,38 @@ pub enum RenderContext { RelativePosition(Point), } +pub trait RenderRect { + fn render_rect(&mut self, rect: Rect, color: sdl2::pixels::Color) -> Result<(), String>; +} + +pub trait RenderBorder { + fn render_border(&mut self, rect: Rect, color: sdl2::pixels::Color) -> Result<(), String>; +} + +pub trait RenderImage { + fn render_image(&mut self, tex: Rc, src: Rect, dest: Rect) -> Result<(), String>; +} + +impl RenderRect for WindowCanvas { + fn render_rect(&mut self, rect: Rect, color: sdl2::pixels::Color) -> Result<(), String> { + self.set_draw_color(color); + self.fill_rect(rect) + } +} + +impl RenderBorder for WindowCanvas { + fn render_border(&mut self, rect: Rect, color: sdl2::pixels::Color) -> Result<(), String> { + self.set_draw_color(color); + self.draw_rect(rect) + } +} + +impl RenderImage for WindowCanvas { + fn render_image(&mut self, tex: Rc, src: Rect, dest: Rect) -> Result<(), String> { + self.copy_ex(&tex, Some(src), Some(dest), 0.0, None, false, false) + } +} + #[inline] pub fn is_in_rect(point: &Point, rect: &Rect) -> bool { rect.contains_point(point.clone()) @@ -47,7 +87,7 @@ where let c = config_holder.config().read().unwrap(); FontDetails::new( c.editor_config().font_path().as_str(), - c.editor_config().character_size().clone(), + c.editor_config().character_size(), ) } @@ -55,7 +95,7 @@ pub fn get_text_character_rect<'l, T>(c: char, renderer: &mut T) -> Option where T: ManagersHolder<'l> + ConfigHolder, { - let font_details = build_font_details(renderer); + let font_details = renderer.config().read().unwrap().editor_config().into(); renderer .font_manager() .load(&font_details) @@ -69,8 +109,9 @@ pub fn move_render_point(p: Point, d: &Rect) -> Rect { Rect::new(d.x() + p.x(), d.y() + p.y(), d.width(), d.height()) } +#[cfg_attr(tarpaulin, skip)] pub trait Render { - fn render(&self, canvas: &mut WC, renderer: &mut Renderer, parent: &RenderContext); + fn render(&self, canvas: &mut WC, renderer: &mut Renderer, context: &RenderContext); fn prepare_ui(&mut self, renderer: &mut Renderer); } @@ -88,14 +129,13 @@ pub trait ClickHandler { pub trait RenderBox { fn render_start_point(&self) -> Point; - fn dest(&self) -> &Rect; + fn dest(&self) -> Rect; } #[cfg(test)] mod tests { use super::*; use crate::tests::support; - use sdl2::rect::*; struct ConfigWrapper { pub inner: ConfigAccess, diff --git a/rider-editor/src/ui/modal/mod.rs b/rider-editor/src/ui/modal/mod.rs new file mode 100644 index 0000000..f8712d5 --- /dev/null +++ b/rider-editor/src/ui/modal/mod.rs @@ -0,0 +1,3 @@ +pub mod open_file; + +pub use crate::ui::modal::open_file::OpenFile; diff --git a/rider-editor/src/ui/modal/open_file.rs b/rider-editor/src/ui/modal/open_file.rs new file mode 100644 index 0000000..483faf2 --- /dev/null +++ b/rider-editor/src/ui/modal/open_file.rs @@ -0,0 +1,235 @@ +use crate::renderer::Renderer; +use crate::ui::*; +use crate::ui::{RenderContext as RC, UpdateContext as UC}; +use rider_config::ConfigAccess; +use sdl2::pixels::Color; +use sdl2::rect::{Point, Rect}; +use std::sync::Arc; + +const CONTENT_MARGIN_LEFT: i32 = 16; +const CONTENT_MARGIN_TOP: i32 = 24; +const DEFAULT_ICON_SIZE: u32 = 16; + +pub struct OpenFile { + root_path: String, + directory_view: DirectoryView, + vertical_scroll_bar: VerticalScrollBar, + horizontal_scroll_bar: HorizontalScrollBar, + dest: Rect, + full_dest: Rect, + background_color: Color, + border_color: Color, + config: ConfigAccess, +} + +impl OpenFile { + pub fn new(root_path: String, width: u32, height: u32, config: ConfigAccess) -> Self { + let (window_width, window_height, background_color, border_color) = { + let c = config.read().unwrap(); + let theme = c.theme(); + ( + c.width(), + c.height(), + theme.background().into(), + theme.border_color().into(), + ) + }; + Self { + directory_view: DirectoryView::new(root_path.clone(), Arc::clone(&config)), + vertical_scroll_bar: VerticalScrollBar::new(Arc::clone(&config)), + horizontal_scroll_bar: HorizontalScrollBar::new(Arc::clone(&config)), + root_path, + dest: Rect::new( + (window_width / 2) as i32 - (width / 2) as i32, + (window_height / 2) as i32 - (height / 2) as i32, + width, + height, + ), + full_dest: Rect::new(0, 0, DEFAULT_ICON_SIZE, DEFAULT_ICON_SIZE), + background_color, + border_color, + config, + } + } + + pub fn root_path(&self) -> String { + self.root_path.clone() + } + + pub fn open_directory(&mut self, dir_path: String, renderer: &mut Renderer) { + self.directory_view.open_directory(dir_path, renderer); + { + let dest = self.directory_view.dest(); + let full_dest = Rect::new( + dest.x(), + dest.y(), + dest.width() + (2 * CONTENT_MARGIN_LEFT as u32), + dest.height() + (2 * CONTENT_MARGIN_TOP as u32), + ); + self.full_dest = full_dest; + } + } + + pub fn full_rect(&self) -> &Rect { + &self.full_dest + } +} + +impl ScrollableView for OpenFile { + fn scroll_by(&mut self, x: i32, y: i32) { + let read_config = self.config.read().unwrap(); + + let value_x = read_config.scroll().speed() * x; + let value_y = read_config.scroll().speed() * y; + let old_x = self.horizontal_scroll_bar.scroll_value(); + let old_y = self.vertical_scroll_bar.scroll_value(); + + if value_x + old_x >= 0 { + self.horizontal_scroll_bar.scroll_to(value_x + old_x); + if self.horizontal_scroll_bar.scrolled_part() > 1.0 { + self.horizontal_scroll_bar.scroll_to(old_x); + } + } + if value_y + old_y >= 0 { + self.vertical_scroll_bar.scroll_to(value_y + old_y); + if self.vertical_scroll_bar.scrolled_part() > 1.0 { + self.vertical_scroll_bar.scroll_to(old_y); + } + } + } + + fn scroll(&self) -> Point { + Point::new( + -self.horizontal_scroll_bar.scroll_value(), + -self.vertical_scroll_bar.scroll_value(), + ) + } +} + +impl Update for OpenFile { + fn update(&mut self, ticks: i32, context: &UC) -> UR { + let (window_width, window_height, color, scroll_width, scroll_margin) = { + let c = self.config.read().unwrap(); + ( + c.width(), + c.height(), + c.theme().background().into(), + c.scroll().width(), + c.scroll().margin_right(), + ) + }; + + self.dest + .set_x((window_width / 2) as i32 - (self.dest.width() / 2) as i32); + self.dest + .set_y((window_height / 2) as i32 - (self.dest.height() / 2) as i32); + self.background_color = color; + + // Scroll bars + self.vertical_scroll_bar + .set_full_size(self.full_dest.height()); // full dest + self.vertical_scroll_bar.set_viewport(self.dest.height()); + self.vertical_scroll_bar + .set_location(self.dest.width() as i32 - (scroll_width as i32 + scroll_margin)); + self.vertical_scroll_bar.update(ticks, context); + + self.horizontal_scroll_bar + .set_full_size(self.full_dest.width()); // full dest + self.horizontal_scroll_bar.set_viewport(self.dest.width()); + self.horizontal_scroll_bar + .set_location(self.dest.height() as i32 - (scroll_width as i32 + scroll_margin)); + self.horizontal_scroll_bar.update(ticks, context); + + // End + UR::NoOp + } +} + +#[cfg_attr(tarpaulin, skip)] +impl OpenFile { + pub fn render(&self, canvas: &mut T, renderer: &mut Renderer, context: &RC) + where + T: RenderRect + RenderBorder + RenderImage, + { + let dest = match context { + RC::RelativePosition(p) => move_render_point(p.clone(), &self.dest), + _ => self.dest, + }; + + // Background + // canvas.set_clip_rect(dest.clone()); + canvas + .render_rect(dest, self.background_color) + .unwrap_or_else(|_| panic!("Failed to render open file modal background!")); + canvas + .render_border(dest, self.border_color) + .unwrap_or_else(|_| panic!("Failed to render open file modal border!")); + + let context = RC::RelativePosition( + dest.top_left() + Point::new(CONTENT_MARGIN_LEFT, CONTENT_MARGIN_TOP) + self.scroll(), + ); + + // directory tree + self.directory_view.render(canvas, renderer, &context); + + // Scroll bars + self.vertical_scroll_bar.render( + canvas, + &RenderContext::RelativePosition(self.dest.top_left()), + ); + self.horizontal_scroll_bar.render( + canvas, + &RenderContext::RelativePosition(self.dest.top_left()), + ); + } + + pub fn prepare_ui(&mut self, renderer: &mut Renderer) { + self.directory_view.prepare_ui(renderer); + } +} + +impl RenderBox for OpenFile { + fn render_start_point(&self) -> Point { + self.dest.top_left() + } + + fn dest(&self) -> Rect { + self.dest.clone() + } +} + +impl ClickHandler for OpenFile { + fn on_left_click(&mut self, point: &Point, context: &UC) -> UR { + let dest = match context { + UC::ParentPosition(p) => move_render_point(*p, &self.dest), + _ => self.dest, + }; + let context = UC::ParentPosition( + dest.top_left() + Point::new(CONTENT_MARGIN_LEFT, CONTENT_MARGIN_TOP) + self.scroll(), + ); + let res = self.directory_view.on_left_click(point, &context); + { + let dest = self.directory_view.dest(); + let full_dest = Rect::new( + dest.x(), + dest.y(), + dest.width() + (2 * CONTENT_MARGIN_LEFT as u32), + dest.height() + (2 * CONTENT_MARGIN_TOP as u32), + ); + self.full_dest = full_dest; + } + res + } + + fn is_left_click_target(&self, point: &Point, context: &UC) -> bool { + let dest = match context { + UC::ParentPosition(p) => move_render_point(p.clone(), &self.dest), + _ => self.dest.clone(), + }; + let context = UC::ParentPosition( + dest.top_left() + Point::new(CONTENT_MARGIN_LEFT, CONTENT_MARGIN_TOP) + self.scroll(), + ); + self.directory_view.is_left_click_target(point, &context); + true + } +} diff --git a/src/ui/project_tree/mod.rs b/rider-editor/src/ui/project_tree/mod.rs similarity index 100% rename from src/ui/project_tree/mod.rs rename to rider-editor/src/ui/project_tree/mod.rs diff --git a/src/ui/scroll_bar/horizontal_scroll_bar.rs b/rider-editor/src/ui/scroll_bar/horizontal_scroll_bar.rs similarity index 89% rename from src/ui/scroll_bar/horizontal_scroll_bar.rs rename to rider-editor/src/ui/scroll_bar/horizontal_scroll_bar.rs index d06da6f..fdcc8a4 100644 --- a/src/ui/scroll_bar/horizontal_scroll_bar.rs +++ b/rider-editor/src/ui/scroll_bar/horizontal_scroll_bar.rs @@ -1,9 +1,8 @@ -use crate::app::{UpdateResult as UR, WindowCanvas as WC}; -use crate::config::*; -use crate::renderer::*; +use crate::app::UpdateResult as UR; use crate::ui::*; -use sdl2::pixels::*; -use sdl2::rect::*; +use rider_config::ConfigAccess; +use sdl2::pixels::Color; +use sdl2::rect::Rect; pub struct HorizontalScrollBar { scroll_value: i32, @@ -54,22 +53,26 @@ impl Update for HorizontalScrollBar { } } -impl Render for HorizontalScrollBar { - fn render(&self, canvas: &mut WC, _renderer: &mut Renderer, context: &RenderContext) { +#[cfg_attr(tarpaulin, skip)] +impl HorizontalScrollBar { + pub fn render(&self, canvas: &mut T, context: &RenderContext) + where + T: RenderRect, + { if self.full_width < self.viewport { return; } - canvas.set_draw_color(Color::RGBA(255, 255, 255, 0)); canvas - .fill_rect(match context { - RenderContext::RelativePosition(p) => move_render_point(p.clone(), &self.rect), - _ => self.rect.clone(), - }) + .render_rect( + match context { + RenderContext::RelativePosition(p) => move_render_point(p.clone(), &self.rect), + _ => self.rect.clone(), + }, + Color::RGBA(255, 255, 255, 0), + ) .unwrap_or_else(|_| panic!("Failed to render vertical scroll back")); } - - fn prepare_ui(&mut self, _renderer: &mut Renderer) {} } impl Scrollable for HorizontalScrollBar { diff --git a/src/ui/scroll_bar/mod.rs b/rider-editor/src/ui/scroll_bar/mod.rs similarity index 68% rename from src/ui/scroll_bar/mod.rs rename to rider-editor/src/ui/scroll_bar/mod.rs index 3ab32b1..7013b94 100644 --- a/src/ui/scroll_bar/mod.rs +++ b/rider-editor/src/ui/scroll_bar/mod.rs @@ -1,9 +1,9 @@ +pub use crate::ui::scroll_bar::horizontal_scroll_bar::HorizontalScrollBar; +pub use crate::ui::scroll_bar::vertical_scroll_bar::VerticalScrollBar; + pub mod horizontal_scroll_bar; pub mod vertical_scroll_bar; -use crate::ui::scroll_bar::horizontal_scroll_bar::*; -use crate::ui::scroll_bar::vertical_scroll_bar::*; - pub trait Scrollable { fn scroll_to(&mut self, n: i32); diff --git a/src/ui/scroll_bar/vertical_scroll_bar.rs b/rider-editor/src/ui/scroll_bar/vertical_scroll_bar.rs similarity index 88% rename from src/ui/scroll_bar/vertical_scroll_bar.rs rename to rider-editor/src/ui/scroll_bar/vertical_scroll_bar.rs index bf51e5e..2655db2 100644 --- a/src/ui/scroll_bar/vertical_scroll_bar.rs +++ b/rider-editor/src/ui/scroll_bar/vertical_scroll_bar.rs @@ -1,9 +1,8 @@ -use crate::app::{UpdateResult as UR, WindowCanvas as WC}; -use crate::config::*; -use crate::renderer::*; +use crate::app::UpdateResult as UR; use crate::ui::*; -use sdl2::pixels::*; -use sdl2::rect::*; +use rider_config::ConfigAccess; +use sdl2::pixels::Color; +use sdl2::rect::Rect; pub struct VerticalScrollBar { scroll_value: i32, @@ -54,22 +53,26 @@ impl Update for VerticalScrollBar { } } -impl Render for VerticalScrollBar { - fn render(&self, canvas: &mut WC, _renderer: &mut Renderer, context: &RenderContext) { +#[cfg_attr(tarpaulin, skip)] +impl VerticalScrollBar { + pub fn render(&self, canvas: &mut T, context: &RenderContext) + where + T: RenderBorder, + { if self.full_height() < self.viewport() { return; } - canvas.set_draw_color(Color::RGBA(255, 255, 255, 0)); canvas - .fill_rect(match context { - RenderContext::RelativePosition(p) => move_render_point(p.clone(), &self.rect), - _ => self.rect.clone(), - }) + .render_border( + match context { + RenderContext::RelativePosition(p) => move_render_point(p.clone(), &self.rect), + _ => self.rect.clone(), + }, + Color::RGBA(255, 255, 255, 0), + ) .unwrap_or_else(|_| panic!("Failed to render vertical scroll back")); } - - fn prepare_ui(&mut self, _renderer: &mut Renderer) {} } impl Scrollable for VerticalScrollBar { @@ -94,6 +97,9 @@ impl Scrollable for VerticalScrollBar { } fn scrolled_part(&self) -> f64 { + if self.full_height() <= self.viewport() { + return 1.0; + } self.scroll_value().abs() as f64 / (self.full_height() - self.viewport()) as f64 } } diff --git a/src/ui/text_character.rs b/rider-editor/src/ui/text_character.rs similarity index 80% rename from src/ui/text_character.rs rename to rider-editor/src/ui/text_character.rs index c445f6d..8d400fb 100644 --- a/src/ui/text_character.rs +++ b/rider-editor/src/ui/text_character.rs @@ -1,17 +1,16 @@ use crate::app::{UpdateResult as UR, WindowCanvas as WC}; -use crate::config::*; -use crate::lexer::TokenType; use crate::renderer::managers::*; use crate::renderer::*; use crate::ui::caret::CaretPosition; use crate::ui::*; +use rider_config::{ConfigAccess, ConfigHolder}; use sdl2::pixels::Color; use sdl2::rect::{Point, Rect}; -use sdl2::render::Texture; -use sdl2::ttf::Font; -use std::rc::Rc; -use std::sync::*; + +pub trait CharacterSizeManager { + fn load_character_size(&mut self, c: char) -> Rect; +} #[derive(Clone, Debug)] pub struct TextCharacter { @@ -69,10 +68,10 @@ impl TextCharacter { pub fn update_position(&mut self, current: &mut Rect) { if self.is_new_line() { let y = self.source.height() as i32; - current.set_x(0); - current.set_y(current.y() + y); self.dest.set_x(current.x()); self.dest.set_y(current.y()); + current.set_x(0); + current.set_y(current.y() + y); } else { self.dest.set_x(current.x()); self.dest.set_y(current.y()); @@ -100,25 +99,18 @@ impl TextCharacter { } } -impl Render for TextCharacter { +#[cfg_attr(tarpaulin, skip)] +impl TextCharacter { /** * Must first create targets so even if new line appear renderer will know * where move render starting point */ - fn render(&self, canvas: &mut WC, renderer: &mut Renderer, context: &RenderContext) { + pub fn render(&self, canvas: &mut WC, renderer: &mut Renderer, context: &RenderContext) { if self.is_new_line() { return; } - let font_details = { - let config = renderer.config().read().unwrap(); - let ec = config.editor_config(); - FontDetails::new(ec.font_path().as_str(), ec.character_size().clone()) - }; - let font = renderer - .font_manager() - .load(&font_details) - .unwrap_or_else(|_| panic!("Could not load font for {:?}", font_details)); + let font_details: FontDetails = renderer.config().read().unwrap().editor_config().into(); let c = self.text_character.clone(); let mut details = TextDetails { @@ -127,9 +119,14 @@ impl Render for TextCharacter { font: font_details.clone(), }; let dest = match context { - RenderContext::RelativePosition(p) => move_render_point(p.clone(), self.dest()), - _ => self.dest.clone(), + RenderContext::RelativePosition(p) => move_render_point(p.clone(), &self.dest), + _ => self.dest(), }; + + let font = renderer + .font_manager() + .load(&font_details) + .unwrap_or_else(|_| panic!("Could not load font for {:?}", font_details)); if let Ok(texture) = renderer.texture_manager().load_text(&mut details, &font) { canvas .copy_ex( @@ -148,27 +145,26 @@ impl Render for TextCharacter { // canvas.draw_rect(dest.clone()).unwrap(); } - fn prepare_ui(&mut self, renderer: &mut Renderer) { - let font_details = build_font_details(renderer); + pub fn prepare_ui<'l, T>(&mut self, renderer: &mut T) + where + T: ConfigHolder + CharacterSizeManager + ManagersHolder<'l>, + { + let font_details: FontDetails = renderer.config().read().unwrap().editor_config().into(); - let font = renderer - .font_manager() - .load(&font_details) - .unwrap_or_else(|_| panic!("Font not found {:?}", font_details)); + let rect = renderer.load_character_size(self.text_character); + self.set_source(&rect); + self.set_dest(&rect); - let c = match self.text_character { - '\n' => 'W', - c => c, - }; - if let Some(rect) = get_text_character_rect(c, renderer) { - self.set_source(&rect); - self.set_dest(&rect); - } let mut details = TextDetails { text: self.text_character.to_string(), color: self.color.clone(), font: font_details.clone(), }; + + let font = renderer + .font_manager() + .load(&font_details) + .unwrap_or_else(|_| panic!("Font not found {:?}", font_details)); renderer .texture_manager() .load_text(&mut details, &font) @@ -191,13 +187,11 @@ impl ClickHandler for TextCharacter { } fn is_left_click_target(&self, point: &Point, context: &UpdateContext) -> bool { - is_in_rect( - point, - &match context { - &UpdateContext::ParentPosition(p) => move_render_point(p.clone(), self.dest()), - _ => self.dest().clone(), - }, - ) + let rect = match context { + &UpdateContext::ParentPosition(p) => move_render_point(p, &self.dest), + _ => self.dest(), + }; + is_in_rect(point, &rect) } } @@ -206,18 +200,17 @@ impl RenderBox for TextCharacter { self.dest.top_left() } - fn dest(&self) -> &Rect { - &self.dest + fn dest(&self) -> Rect { + self.dest } } #[cfg(test)] mod test_getters { - use crate::renderer::*; use crate::tests::*; use crate::ui::*; - use sdl2::pixels::*; - use sdl2::rect::*; + use sdl2::pixels::Color; + use sdl2::rect::Rect; use std::sync::*; #[test] @@ -323,7 +316,7 @@ mod test_getters { Color::RGB(1, 12, 123), Arc::clone(&config), ); - assert_eq!(widget.dest(), &Rect::new(0, 0, 0, 0)); + assert_eq!(widget.dest(), Rect::new(0, 0, 0, 0)); } #[test] @@ -343,38 +336,48 @@ mod test_getters { #[cfg(test)] mod test_own_methods { - use crate::renderer::*; use crate::tests::*; use crate::ui::*; - use sdl2::pixels::*; - use sdl2::rect::*; + use sdl2::rect::Rect; use std::sync::*; #[test] fn must_update_position_of_new_line() { let config = support::build_config(); - let mut widget = - TextCharacter::new('\n', 0, 0, true, Color::RGB(0, 0, 0), Arc::clone(&config)); + let mut widget = TextCharacter::new( + '\n', + 0, + 0, + true, + sdl2::pixels::Color::RGB(0, 0, 0), + Arc::clone(&config), + ); widget.set_dest(&Rect::new(10, 20, 30, 40)); widget.set_source(&Rect::new(50, 60, 70, 80)); let mut current = Rect::new(10, 23, 0, 0); widget.update_position(&mut current); assert_eq!(current, Rect::new(0, 103, 1, 1)); - assert_eq!(widget.dest(), &Rect::new(0, 103, 30, 40)); + assert_eq!(widget.dest(), Rect::new(10, 23, 30, 40)); assert_eq!(widget.source(), &Rect::new(50, 60, 70, 80)); } #[test] fn must_update_position_of_non_new_line() { let config = support::build_config(); - let mut widget = - TextCharacter::new('W', 0, 0, true, Color::RGB(0, 0, 0), Arc::clone(&config)); + let mut widget = TextCharacter::new( + 'W', + 0, + 0, + true, + sdl2::pixels::Color::RGB(0, 0, 0), + Arc::clone(&config), + ); widget.set_dest(&Rect::new(10, 20, 30, 40)); widget.set_source(&Rect::new(50, 60, 70, 80)); let mut current = Rect::new(10, 23, 0, 0); widget.update_position(&mut current); assert_eq!(current, Rect::new(80, 23, 1, 1)); - assert_eq!(widget.dest(), &Rect::new(10, 23, 70, 80)); + assert_eq!(widget.dest(), Rect::new(10, 23, 70, 80)); assert_eq!(widget.source(), &Rect::new(50, 60, 70, 80)); } } @@ -382,18 +385,22 @@ mod test_own_methods { #[cfg(test)] mod test_click_handler { use crate::app::*; - use crate::renderer::*; use crate::tests::*; use crate::ui::*; - use sdl2::pixels::*; - use sdl2::rect::*; + use sdl2::rect::{Point, Rect}; use std::sync::*; #[test] fn refute_when_not_click_target() { let config = support::build_config(); - let mut widget = - TextCharacter::new('\n', 0, 0, true, Color::RGB(0, 0, 0), Arc::clone(&config)); + let mut widget = TextCharacter::new( + '\n', + 0, + 0, + true, + sdl2::pixels::Color::RGB(0, 0, 0), + Arc::clone(&config), + ); widget.set_dest(&Rect::new(10, 20, 30, 40)); widget.set_source(&Rect::new(50, 60, 70, 80)); let point = Point::new(0, 0); @@ -405,8 +412,14 @@ mod test_click_handler { #[test] fn assert_when_click_target() { let config = support::build_config(); - let mut widget = - TextCharacter::new('\n', 0, 0, true, Color::RGB(0, 0, 0), Arc::clone(&config)); + let mut widget = TextCharacter::new( + '\n', + 0, + 0, + true, + sdl2::pixels::Color::RGB(0, 0, 0), + Arc::clone(&config), + ); widget.set_dest(&Rect::new(10, 20, 30, 40)); widget.set_source(&Rect::new(50, 60, 70, 80)); let point = Point::new(20, 30); @@ -418,8 +431,14 @@ mod test_click_handler { #[test] fn refute_when_not_click_target_because_parent() { let config = support::build_config(); - let mut widget = - TextCharacter::new('\n', 0, 0, true, Color::RGB(0, 0, 0), Arc::clone(&config)); + let mut widget = TextCharacter::new( + '\n', + 0, + 0, + true, + sdl2::pixels::Color::RGB(0, 0, 0), + Arc::clone(&config), + ); widget.set_dest(&Rect::new(10, 20, 30, 40)); widget.set_source(&Rect::new(50, 60, 70, 80)); let point = Point::new(20, 30); @@ -431,8 +450,14 @@ mod test_click_handler { #[test] fn assert_when_click_target_because_parent() { let config = support::build_config(); - let mut widget = - TextCharacter::new('\n', 0, 0, true, Color::RGB(0, 0, 0), Arc::clone(&config)); + let mut widget = TextCharacter::new( + '\n', + 0, + 0, + true, + sdl2::pixels::Color::RGB(0, 0, 0), + Arc::clone(&config), + ); widget.set_dest(&Rect::new(10, 20, 30, 40)); widget.set_source(&Rect::new(50, 60, 70, 80)); let point = Point::new(120, 130); @@ -451,7 +476,7 @@ mod test_click_handler { position.clone(), line.clone(), true, - Color::RGB(0, 0, 0), + sdl2::pixels::Color::RGB(0, 0, 0), Arc::clone(&config), ); let dest = Rect::new(10, 20, 30, 40); @@ -468,18 +493,22 @@ mod test_click_handler { #[cfg(test)] mod test_render_box { - use crate::renderer::*; use crate::tests::*; use crate::ui::*; - use sdl2::pixels::*; - use sdl2::rect::*; + use sdl2::rect::{Point, Rect}; use std::sync::*; #[test] fn must_return_top_left_point() { let config = support::build_config(); - let mut widget = - TextCharacter::new('\n', 0, 0, true, Color::RGB(0, 0, 0), Arc::clone(&config)); + let mut widget = TextCharacter::new( + '\n', + 0, + 0, + true, + sdl2::pixels::Color::RGB(0, 0, 0), + Arc::clone(&config), + ); widget.set_dest(&Rect::new(10, 20, 30, 40)); widget.set_source(&Rect::new(50, 60, 70, 80)); let result = widget.render_start_point(); @@ -491,18 +520,22 @@ mod test_render_box { #[cfg(test)] mod test_update { use crate::app::*; - use crate::renderer::*; use crate::tests::*; use crate::ui::*; - use sdl2::pixels::*; - use sdl2::rect::*; + use sdl2::rect::{Point, Rect}; use std::sync::*; #[test] fn assert_do_nothing() { let config = support::build_config(); - let mut widget = - TextCharacter::new('\n', 0, 0, true, Color::RGB(0, 0, 0), Arc::clone(&config)); + let mut widget = TextCharacter::new( + '\n', + 0, + 0, + true, + sdl2::pixels::Color::RGB(0, 0, 0), + Arc::clone(&config), + ); widget.set_dest(&Rect::new(10, 20, 30, 40)); widget.set_source(&Rect::new(50, 60, 70, 80)); let result = widget.update( diff --git a/rider-generator/Cargo.toml b/rider-generator/Cargo.toml new file mode 100644 index 0000000..a2750b2 --- /dev/null +++ b/rider-generator/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "rider-generator" +version = "0.1.0" +authors = ["Adrian Wozniak "] +edition = "2018" + +[dependencies] +rider-config = { path = "../rider-config", version = "0.1.0" } +rider-themes = { path = "../rider-themes", version = "0.1.0" } +log = "*" +simplelog = "*" +serde = "*" +serde_json = "*" +serde_derive = "*" +dirs = "*" +uuid = { version = "0.7", features = ["v4"] } +rand = "0.5" diff --git a/assets/fonts/Beyond Wonderland.ttf b/rider-generator/assets/fonts/Beyond Wonderland.ttf similarity index 100% rename from assets/fonts/Beyond Wonderland.ttf rename to rider-generator/assets/fonts/Beyond Wonderland.ttf diff --git a/assets/fonts/DejaVuSansMono.ttf b/rider-generator/assets/fonts/DejaVuSansMono.ttf similarity index 100% rename from assets/fonts/DejaVuSansMono.ttf rename to rider-generator/assets/fonts/DejaVuSansMono.ttf diff --git a/assets/fonts/ElaineSans-Medium.ttf b/rider-generator/assets/fonts/ElaineSans-Medium.ttf similarity index 100% rename from assets/fonts/ElaineSans-Medium.ttf rename to rider-generator/assets/fonts/ElaineSans-Medium.ttf diff --git a/rider-generator/assets/themes/default/images/directory-48x48.png b/rider-generator/assets/themes/default/images/directory-48x48.png new file mode 100644 index 0000000000000000000000000000000000000000..d1d5462af2d6e8a9688197615ab45a9f93cedf1c GIT binary patch literal 2004 zcmY*Y2~?9;7XCL95(uD;=wt`A^tRhO?5EoFXGcE_pKuI-o!*6>!|9}7czIXH8`@VPI%Lg%< za0cC(jvxp_ty09nD6xFjIC$+C?F0iPl#88JI3lK6)AxJq8pK1}re; z_i}PKYPC73*?Ae+x#=oZXt*j=z~k}Q95;@u8;8SNG{8bWU#(V_|E+|}X1g+3V4RiA zMZn}NPWi5`v2p8^%8;a#c=A^+iw#qf92U0L0$6OVE=?01LB^ptpUVvi2_#DaKd&&0 zWO~UY02XOXiCClzv()hrx`RARaZ zi9Dr}$jGpW$S_T`TBTN4%8G>byS9t?e33xl?&bzN1ppo}6bTDoRKB<(S^v#`xm<-Z z7&MbX-eOzio>E0<@EaNKg7q6>VQX9#%gxml!0#PnL3>FKc)*ngJpgrt(nBZ&;O8q} z%wj{$KT^zQ6W^6fW~HR92k_I>j{+_aKvlz53!lvfF&w7lPyp=OT~Css73-o@fZ;UB z2#{e>;AiKhlM#>&I|c0LBM%P`g%cs;zyY$QIE(G$?FB)&38arph=QQpQ$}h4N&bG` z76Bxm%??*tdJ8>FUR3*JVI2AZ@=D4J*95J8Q=i2SUK6lNBoy-9KqvHE>@E<22`ond z*vnHI5uvnLWEiACak$~mrf5I7>$CbeO+1VUhNFZPj-lj>Vi<efj{#iEKqQfxaeU>Bs{8AW(bx;ZHlnr*8PCP%`*Vo_w{Z86+njPJS z4rXIRr{M&KpJJf44t93-_V#qVSU+&?{J9|~LZF=TIBbpYImtjxsqEXY^M86b^S66E zbT>geW;Lt!{CQ4;F5CJeWzXkBv4DJkhz@~vt_alF)^ z-jO^6bL~9?dblF#ZQJC3 z3}4@FD!jBaKG3F?_Ko25LUEq0Z|yFta}=k+eds%BX^&qHFYD>9E67*&pj}rlI=_=P zqH4|0DWdh*71Xvu!m%xdE%)Q%H-y$T_t$c~9v<}laCkK5k^eWnHGP8v_bDf;4GYJf zub?wud?njzbXoGn>jQgF+Q$TrQJB-SGxo?pu$gIX8~lK>qpmTu>7>gKl$M3=mpgxG zpK9NCvv zU_Oz1EiY+$e4xABC1m#Qowr*qq?Qe7RIc~en>Rh|x;s4Cpa0_9S5)f`bD{pFL$AZj zfNhif!Q&Ie=x4qc5CJi!ITSS5{c+1easHJ^C`5+Y(=-T2oA+t#+Z2S;sh zbS+ERKEdOB-LA`7qc7rw%--94PhGudytA{yR7vaj?&PDO&R_C!^0y9aRIhIADE&y= z=5zGHgj?>#Hfw9bY3%S7(ao$j9~aY%@)le|@QTNCS9WER? zeB{8IvZ`O3eyg9Ed3;2>^ZcO{;#itt`RXMnJeNA^aKY0vQPjBWW!)`48ei*@>QEx3ARl3tgk&-KqK&4mGxNu+*aiBv4{OOQ(az2yFK vPoYHWFOkp^waouZ*i@|BnD_pl6C{4X^OwuWglI$ZEF>UmrAE=UR$KEwT%?}6 literal 0 HcmV?d00001 diff --git a/rider-generator/assets/themes/default/images/directory-512x512.png b/rider-generator/assets/themes/default/images/directory-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..deaa13a5be78b5b17b744ade674ce686c4c00e76 GIT binary patch literal 16260 zcmb7r2{=^y8~>pzGFc-PAuW z5|b3UF=G(2&DaNH&i{MnoTF5~|MUOPbD!tldz|xK-uLtF?|a_P#!5zdmNW!GGS(Z{ zZ-*dp@Soz4)MW6le)6Xg2r}Pny?)J3@_$|RglF@t*R=HHI`7J9uU?Ywzajosywl9f zmmPNvRaVt+(ce1s!E+>Hp|0GTy-I5W8Wuf$`!N56-KCHSIlJ8+3ie8FftqH!<<6*G zK1))VVG<~RK;e?aD@oa2wS%t2e8;WpRqH*CbssL*J?_>_sd&(|@6l*k>D!2rf`{)e zP#dL%91Z-R6i3+i?K*G!rzK(?PtKeb{p^>Js-p~+1~za+zg=Ys&%IV;Tb22(Xg#Ju zMt?$@?<%v}hKB?HD%Z_4L4VOK6IGIX^u>XHRVF-3IeMbp@04`+RCUoW*@}5R!!|L| z`AfNaNA><*y4?HReb^E(E5hs$FSz54wrZzi^uQV85cEi`HTv4}HhNI?o6+h!_9xu^ zuwxKu${i0+3&d}x)IAqYl5K~b4!B?Nl$R6zn33)_ntPv17j9z37+2fN1x{8KJK0N)r&N8ImT5L%=bZW*?(KV=>`~t!Mr6u*ds|0J6DW4Rr z!s89r=u6y8NN632o>jf_OsF}!h!?G($|zvB*uRQG^ir`ybs4N|X^0bzaziDvVRjh4 zbcFv@S(J<%W&_F~1tE4Gbq>?DNyIV*Y$-Cfi8^a}IKA&P1wXiavf~Kb_wr5kkSKJI zVIFCT znJ-leKD2dT`69>^e|nN=-!joO&dlRe>FGSB3t}a! zcfKuO{dYkB`EuAFx?Dex(1P*3lWTIME!_XJrg)3}Yn-Y03R`JOg1fYZfdO+sW96eG ze@&sFJZSd|yywY(B>rKZ0OKMz*O}(73^6oN5G-X;i*R~{asD<$i63enfOhaFnZks;IoHKMxc$H|iMv(o-_|h7x}1<*9Yf+cfBcB- zMNrZq?Xg;kn_bjR^4Ll_N0W7+`Z|*u%BQRh1)PWvI|h;}IN8z`F}xB~XsE5;1IfD} zDU9rShxS-Jh){zVb$SEZgL3~fve;8p+2&+6)FB%KZy4LWOQ}X5~{r&x84dehS81&{Oqrj2oT?wVUgewdE zbU9%;7l^y1j+aJ!@6%>)>^T^E_N)~@u3>9|_tt*~jx#1_YO#(jy27-sdRCsGpsr5* z%wP|<(=EH-M;RpN`CY#sr?`r|tAByH+|;m_7Y*i`m{22d3`Bm3Zz_pSU8fAM+!tc+ z!F}&KX^x88P&Z@ThI|&cFa%Lc)InxcnSNyBo($OOsNielS%ob>|_Z%3&s#p4b8H@pTz9@LIP_ z;-*te#f3(`a_A)DY${nk&~N|#rbVJt4wmY(Sqb7FDhP1+amn;*u0(3r`-+M|o543^ z6;80dimhdDu825YF%yNn`KA7YtIWA-y!PhTYlG$`D9WG<7X7SBQ^VmW`>keedMhU{ zOZMv9sT4Sqv;he3Xak1U>`q>az~AffEJa!JK>sTOS)Wl{{8f_K)oz*&JzD&a`^SYN zyBT7c*N95ASP`942U=u;YNFCN3g?0IS?Nu0)X&d%m+eLFZC~c}m+lOwRpN)eA zZI?t}{1*nR6o{P@UOiEmo}QDPJphSLdRRpmLs6V_d;z#Zd)EakcVA@hbZ0*07W6h} z8aVI3i*Tp$r?$FKs0@)$Jb49MltL8KRO21?h-^6R2Z?S~ri3UR<$69*A{guyX>dfW zp*F?ln#ZqXmKQ|o{HyAVR{fiz`onNF|%Ce)|`t*iUAr?b3A z4baYBFB9Umgaf#^W~TSm{V=b3dHUmQ| zw1u&rKs9gnqdjz$t55aQL}%$=x&)%;0ul4&jR16SG5*e63LR_Rbw`QK?^|nw?ONmFZ7StwDKG!ZaRANZG~f0iQudvLQw*s zCn9zx{>SaPjuNhJ3dZ`jVwy*JjGikoA^J5l|--z0)Aoi&BzXG2m6g6(H5U|1^0j* zo2(b0XH+WEOI-}}Oc4cmY4x(#knC`^h&}J$HA3vVl zVSd=5Mk?cKu6{<&qy>N#{aeuMFz?j(tUk+J1eHH6ru1@KmmtC`#kvcL^kZwSXTBf! zqb{%oz9KeXj7w}5sdpWkZwT9~2`~k)aUSW&ORsApX76EXk*|r6({e2Og*UD%P~gY| zWhGkJK85Ea+UkBXfHPuK03@76J3PI|1V;lFB$LQ7e+&U@!SOG(#x;?zh~XgyUEYPF z&xU0wt?6*Y@qkQ`jJ@8m-ONbgpPGTon}!=6SMv6KZK^CT2OhdM95h{0jucPZrp8^c z?bJUeGwSpr&rb?{ZophT#-(nw%Y6Gr51eQc;+iJ=z6xop>ah?$iD>q`QsjCzi})|! zAW1sd%bXK*IqIuKu!_j_B?{M3w@&F9a12->1#h2d?=D9rh!;kRJT>Nk0%16#lqnTT z(fMDBh}S6cMiR<)w5J-d$nA$2l;fFqG*qMupQU9TR1O9j(OnW&(+fg;L+UV%--6iY zQ26~s-;{XD{pq0hMeKNUY~KwXv(8<^H}+pp3=sJ(aDe?urU)Qnrty9ih$s-bhkbs) z)zA$!T+cUlrquw?=oh_&9Zk1Wrum3-jqLrNDWN}tgBN{^*2o&`8%g3MIYgY7vVq^~ z2mE2rzP<3a;=BzfQr{y3j}e?1bySr8i@iGz>1cLI7C@tE5>qCpHSm@K5{{{?&v

#J0H10^febWWbr;K>^nOjK7;&xR$gQ3wS zKd%v&RrhPcbLi8G-`ytOUey$E@kb)IM)D5oKr5a9&iWNa9cp^JMJTc)XGq*kAAc+F<&SfEf<`qRhKDpv{eNKM?tyo^} zIc6o(F{>zcxz7R*Dh?c~BaJ3)QC`e;=*S=lR{&e0>&AT#%o>Vz)+32vo;loB{)@4o$krnu=icHMlJ0&OkO3#C*&fEzBQF=&Qqeetw^B2$M z=~|(NuP~v%f@mWtn80%0^iI1EmGtye{Cwcb!UQ4+2+v=QZp5fL%uCq~-i<66q~OA= zDRQ8MgJX}rMfaj#_bL2vvw1nxg?=5ix)rN@QJGxwMSnAFtf%-1__>=25QlQvC+BMK z43rlpavb&qKMGdVyQ0^M?%p~_P(BvaO9&1+X%7$uFp1n#y=?SN&p8!c#D22Q6w@Db z2fw~^9&s_xy#rkGNB#un^B!v;hC{utI>wLC(N+>BzU;BiBjBY16xEZX8DWC2xO=;W{sfctnS} z7|on6{Sv)p>Bo42+(5_Q=gdyMc01lvhT8PZzy9D@Wt5#PigOx_!_2SsuhuQp?|XyY zxh0r@68c41vch*--UpM#<-7C58V_QB+RQ0e6=bNUJcmN5|6Yn{rf@LEM>_olJgK#^ zFff9#+wC!!5X=xrar9Ic6N@o4O-Iml*dsIev|tam=yNGboVZ;yFWSt=al~h)%T#Tf z#D8CV1nivBbN$v;+u7;;wR2;IJi7Un*7;gN7bBX7m?PH(_03!nBf7ihuQl0hJ={!| zXJ1V_WPBfiVEf6q!o_r>Ld_7nn3<+KX1^#|g~I^PU| z71xEl{QkbsNcNe@p!71{3H^hX5XOwJCnEN$%$GsddL`#Zs*SP?Lo$7q1_eNnKS%D< zgt_th_hh@=`5LsA8v~CVg*yk?o+>fZ5K}3gOfJ7X*Z9bM3VFp(c($k~PQ9~!j2{#Z z+nsBwC48rCS%5fB5x$#fhkb_L-%=b;Y=LNTDdZLKcW39(lrNdQ=sSFcT1ts~{1)U? zpZhmhkG#Lan2^dqTsu2vhzgtP!b#aUd|yv+?bQJg5*MdGj2q)J)r`G&Do=%96nrJ= z`h2I1HD46=v$MOGnAE({mbjTANlLtbk6We<8=A#WZSF$PB~RsB5t^-j%0AMxII&|T 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 JGuFDE`9EQ`-4Fl( literal 0 HcmV?d00001 diff --git a/rider-generator/assets/themes/default/images/directory-64x64.png b/rider-generator/assets/themes/default/images/directory-64x64.png new file mode 100644 index 0000000000000000000000000000000000000000..e18109b514961fc8beebcb2b0a5bcd799a65dfb5 GIT binary patch literal 2322 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 literal 0 HcmV?d00001 diff --git a/rider-generator/assets/themes/default/images/file-48x48.png b/rider-generator/assets/themes/default/images/file-48x48.png new file mode 100644 index 0000000000000000000000000000000000000000..7e0d8a52383e7163472cbcfc297d70f7277a0a15 GIT binary patch literal 866 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 literal 0 HcmV?d00001 diff --git a/rider-generator/assets/themes/railscasts/images/directory-512x512.png b/rider-generator/assets/themes/railscasts/images/directory-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..deaa13a5be78b5b17b744ade674ce686c4c00e76 GIT binary patch literal 16260 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

#J0H10^febWWbr;K>^nOjK7;&xR$gQ3wS zKd%v&RrhPcbLi8G-`ytOUey$E@kb)IM)D5oKr5a9&iWNa9cp^JMJTc)XGq*kAAc+F<&SfEf<`qRhKDpv{eNKM?tyo^} zIc6o(F{>zcxz7R*Dh?c~BaJ3)QC`e;=*S=lR{&e0>&AT#%o>Vz)+32vo;loB{)@4o$krnu=icHMlJ0&OkO3#C*&fEzBQF=&Qqeetw^B2$M z=~|(NuP~v%f@mWtn80%0^iI1EmGtye{Cwcb!UQ4+2+v=QZp5fL%uCq~-i<66q~OA= zDRQ8MgJX}rMfaj#_bL2vvw1nxg?=5ix)rN@QJGxwMSnAFtf%-1__>=25QlQvC+BMK z43rlpavb&qKMGdVyQ0^M?%p~_P(BvaO9&1+X%7$uFp1n#y=?SN&p8!c#D22Q6w@Db z2fw~^9&s_xy#rkGNB#un^B!v;hC{utI>wLC(N+>BzU;BiBjBY16xEZX8DWC2xO=;W{sfctnS} z7|on6{Sv)p>Bo42+(5_Q=gdyMc01lvhT8PZzy9D@Wt5#PigOx_!_2SsuhuQp?|XyY zxh0r@68c41vch*--UpM#<-7C58V_QB+RQ0e6=bNUJcmN5|6Yn{rf@LEM>_olJgK#^ zFff9#+wC!!5X=xrar9Ic6N@o4O-Iml*dsIev|tam=yNGboVZ;yFWSt=al~h)%T#Tf z#D8CV1nivBbN$v;+u7;;wR2;IJi7Un*7;gN7bBX7m?PH(_03!nBf7ihuQl0hJ={!| zXJ1V_WPBfiVEf6q!o_r>Ld_7nn3<+KX1^#|g~I^PU| z71xEl{QkbsNcNe@p!71{3H^hX5XOwJCnEN$%$GsddL`#Zs*SP?Lo$7q1_eNnKS%D< zgt_th_hh@=`5LsA8v~CVg*yk?o+>fZ5K}3gOfJ7X*Z9bM3VFp(c($k~PQ9~!j2{#Z z+nsBwC48rCS%5fB5x$#fhkb_L-%=b;Y=LNTDdZLKcW39(lrNdQ=sSFcT1ts~{1)U? zpZhmhkG#Lan2^dqTsu2vhzgtP!b#aUd|yv+?bQJg5*MdGj2q)J)r`G&Do=%96nrJ= z`h2I1HD46=v$MOGnAE({mbjTANlLtbk6We<8=A#WZSF$PB~RsB5t^-j%0AMxII&|T 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 JGuFDE`9EQ`-4Fl( literal 0 HcmV?d00001 diff --git a/rider-generator/assets/themes/railscasts/images/directory-64x64.png b/rider-generator/assets/themes/railscasts/images/directory-64x64.png new file mode 100644 index 0000000000000000000000000000000000000000..d6ee7c3ccef7e3cd219b5505fb705075195101e8 GIT binary patch literal 2322 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 literal 0 HcmV?d00001 diff --git a/rider-generator/assets/themes/railscasts/images/file-48x48.png b/rider-generator/assets/themes/railscasts/images/file-48x48.png new file mode 100644 index 0000000000000000000000000000000000000000..721382a440e1ca09a0b125111a7054b1609443c2 GIT binary patch literal 866 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 std::io::Result<()> { + if !directories.themes_dir.exists() { + fs::create_dir_all(&directories.themes_dir)?; + images::create(directories)?; + } + + if !directories.fonts_dir.exists() { + fs::create_dir_all(&directories.fonts_dir)?; + } + write_default_fonts(directories)?; + + if !directories.log_dir.exists() { + fs::create_dir_all(&directories.log_dir)?; + } + + if !directories.project_dir.exists() { + fs::create_dir_all(&directories.project_dir)?; + } + Ok(()) +} + +fn write_default_fonts(directories: &Directories) -> std::io::Result<()> { + let path = directories.fonts_dir.clone().to_str().unwrap().to_owned(); + let mut buf = PathBuf::new(); + buf.push(path); + buf.push("DejaVuSansMono.ttf"); + if !buf.exists() { + let contents = include_bytes!("../assets/fonts/DejaVuSansMono.ttf"); + fs::write(buf, contents.to_vec())?; + } + + let path = directories.fonts_dir.clone().to_str().unwrap().to_owned(); + let mut buf = PathBuf::new(); + buf.push(path); + buf.push("ElaineSans-Medium.ttf"); + if !buf.exists() { + let contents = include_bytes!("../assets/fonts/ElaineSans-Medium.ttf"); + fs::write(buf, contents.to_vec())?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs::create_dir_all; + use std::path::Path; + use uuid::Uuid; + + #[cfg(test)] + fn join(a: String, b: String) -> String { + vec![a, b].join("/") + } + + #[test] + fn assert_create_fonts() { + let uniq = Uuid::new_v4(); + let test_path = join("/tmp/rider-tests".to_owned(), uniq.to_string()); + create_dir_all(test_path.clone()).unwrap(); + let directories = Directories::new(Some(test_path.clone()), None); + assert_eq!(create(&directories).is_ok(), true); + assert_eq!( + Path::new(join(test_path.clone(), "rider/fonts".to_owned()).as_str()).exists(), + true + ); + } + + #[test] + fn assert_create_log() { + let uniq = Uuid::new_v4(); + let test_path = join("/tmp/rider-tests".to_owned(), uniq.to_string()); + create_dir_all(test_path.clone()).unwrap(); + let directories = Directories::new(Some(test_path.clone()), None); + assert_eq!(create(&directories).is_ok(), true); + assert_eq!( + Path::new(join(test_path.clone(), "rider/log".to_owned()).as_str()).exists(), + true + ); + } + + #[test] + fn assert_create_themes() { + let uniq = Uuid::new_v4(); + let test_path = join("/tmp/rider-tests".to_owned(), uniq.to_string()); + create_dir_all(test_path.clone()).unwrap(); + let directories = Directories::new(Some(test_path.clone()), None); + assert_eq!( + Path::new(join(test_path.clone(), "rider/themes".to_owned()).as_str()).exists(), + false + ); + assert_eq!(create(&directories).is_ok(), true); + assert_eq!( + Path::new(join(test_path.clone(), "rider/themes".to_owned()).as_str()).exists(), + true + ); + } +} diff --git a/rider-generator/src/images.rs b/rider-generator/src/images.rs new file mode 100644 index 0000000..5726483 --- /dev/null +++ b/rider-generator/src/images.rs @@ -0,0 +1,218 @@ +use crate::write_bytes_to::write_bytes_to; +use rider_config::directories::*; +use std::fs::create_dir_all; +use std::path::PathBuf; + +pub fn create(directories: &Directories) -> std::io::Result<()> { + default_theme(directories)?; + railscasts_theme(directories)?; + Ok(()) +} + +fn create_default_directory_icon(dir: &PathBuf) -> std::io::Result<()> { + let blob = include_bytes!("../assets/themes/default/images/directory-64x64.png"); + write_bytes_to(dir, "directory-64x64.png", blob)?; + Ok(()) +} + +fn create_default_file_icon(dir: &PathBuf) -> std::io::Result<()> { + let blob = include_bytes!("../assets/themes/default/images/file-64x64.png"); + write_bytes_to(dir, "file-64x64.png", blob)?; + Ok(()) +} + +fn default_theme(directories: &Directories) -> std::io::Result<()> { + let mut dir = PathBuf::new(); + dir.push(directories.themes_dir.clone()); + dir.push("default"); + dir.push("images"); + let r = create_dir_all(&dir); + #[cfg_attr(tarpaulin, skip)] + r.unwrap_or_else(|_| panic!("Cannot create themes config directory")); + + create_default_directory_icon(&dir)?; + create_default_file_icon(&dir)?; + Ok(()) +} + +fn create_railscasts_directory_icon(dir: &PathBuf) -> std::io::Result<()> { + let blob = include_bytes!("../assets/themes/railscasts/images/directory-64x64.png"); + write_bytes_to(dir, "directory-64x64.png", blob)?; + Ok(()) +} + +fn create_railscasts_file_icon(dir: &PathBuf) -> std::io::Result<()> { + let blob = include_bytes!("../assets/themes/railscasts/images/file-64x64.png"); + write_bytes_to(dir, "file-64x64.png", blob)?; + Ok(()) +} + +fn railscasts_theme(directories: &Directories) -> std::io::Result<()> { + let mut dir = PathBuf::new(); + dir.push(directories.themes_dir.clone()); + dir.push("railscasts"); + dir.push("images"); + create_dir_all(&dir)?; + create_railscasts_directory_icon(&dir)?; + create_railscasts_file_icon(&dir)?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs::create_dir_all; + use std::path::{Path, PathBuf}; + use uuid::Uuid; + + #[cfg(test)] + fn join(a: String, b: String) -> String { + vec![a, b].join("/") + } + + #[test] + fn assert_create() { + let uniq = Uuid::new_v4(); + let test_path = join("/tmp/rider-tests".to_owned(), uniq.to_string()); + create_dir_all(test_path.clone()).unwrap(); + let directories = Directories::new(Some(test_path.clone()), None); + let themes_dir = join(test_path.clone(), "rider/themes".to_owned()); + assert_eq!( + Path::new( + join( + themes_dir.clone(), + "railscasts/images/directory-64x64.png".to_owned() + ) + .as_str() + ) + .exists(), + false + ); + assert_eq!( + Path::new( + join( + themes_dir.clone(), + "railscasts/images/file-64x64.png".to_owned() + ) + .as_str() + ) + .exists(), + false + ); + assert_eq!( + Path::new( + join( + themes_dir.clone(), + "default/images/directory-64x64.png".to_owned() + ) + .as_str() + ) + .exists(), + false + ); + assert_eq!( + Path::new( + join( + themes_dir.clone(), + "default/images/file-64x64.png".to_owned() + ) + .as_str() + ) + .exists(), + false + ); + assert_eq!(create(&directories).is_ok(), true); + assert_eq!( + Path::new( + join( + themes_dir.clone(), + "railscasts/images/directory-64x64.png".to_owned() + ) + .as_str() + ) + .exists(), + true + ); + assert_eq!( + Path::new( + join( + themes_dir.clone(), + "railscasts/images/file-64x64.png".to_owned() + ) + .as_str() + ) + .exists(), + true + ); + assert_eq!( + Path::new( + join( + themes_dir.clone(), + "default/images/directory-64x64.png".to_owned() + ) + .as_str() + ) + .exists(), + true + ); + assert_eq!( + Path::new( + join( + themes_dir.clone(), + "default/images/file-64x64.png".to_owned() + ) + .as_str() + ) + .exists(), + true + ); + } + + #[test] + fn assert_create_default_directory_icon() { + let uniq = Uuid::new_v4(); + let test_path = join("/tmp/rider-tests".to_owned(), uniq.to_string()); + create_dir_all(test_path.clone()).unwrap(); + let file_path: String = join(test_path.clone(), "directory-64x64.png".to_owned()); + let dir: PathBuf = test_path.into(); + assert_eq!(Path::new(file_path.as_str()).exists(), false); + assert_eq!(create_default_directory_icon(&dir).is_ok(), true); + assert_eq!(Path::new(file_path.as_str()).exists(), true); + } + + #[test] + fn assert_create_default_file_icon() { + let uniq = Uuid::new_v4(); + let test_path = join("/tmp/rider-tests".to_owned(), uniq.to_string()); + create_dir_all(test_path.clone()).unwrap(); + let file_path: String = join(test_path.clone(), "file-64x64.png".to_owned()); + let dir: PathBuf = test_path.into(); + assert_eq!(Path::new(file_path.as_str()).exists(), false); + assert_eq!(create_default_file_icon(&dir).is_ok(), true); + assert_eq!(Path::new(file_path.as_str()).exists(), true); + } + + #[test] + fn assert_create_railscasts_directory_icon() { + let uniq = Uuid::new_v4(); + let test_path = join("/tmp/rider-tests".to_owned(), uniq.to_string()); + create_dir_all(test_path.clone()).unwrap(); + let file_path: String = join(test_path.clone(), "directory-64x64.png".to_owned()); + let dir: PathBuf = test_path.into(); + assert_eq!(Path::new(file_path.as_str()).exists(), false); + assert_eq!(create_railscasts_directory_icon(&dir).is_ok(), true); + assert_eq!(Path::new(file_path.as_str()).exists(), true); + } + + #[test] + fn assert_create_railscasts_file_icon() { + let uniq = Uuid::new_v4(); + let test_path = join("/tmp/rider-tests".to_owned(), uniq.to_string()); + create_dir_all(test_path.clone()).unwrap(); + let file_path: String = join(test_path.clone(), "file-64x64.png".to_owned()); + let dir: PathBuf = test_path.into(); + assert_eq!(Path::new(file_path.as_str()).exists(), false); + assert_eq!(create_railscasts_file_icon(&dir).is_ok(), true); + assert_eq!(Path::new(file_path.as_str()).exists(), true); + } +} diff --git a/rider-generator/src/main.rs b/rider-generator/src/main.rs new file mode 100644 index 0000000..906c4ac --- /dev/null +++ b/rider-generator/src/main.rs @@ -0,0 +1,120 @@ +extern crate dirs; +extern crate log; +extern crate rand; +extern crate rider_config; +extern crate rider_themes; +extern crate serde; +extern crate serde_derive; +extern crate serde_json; +extern crate simplelog; +extern crate uuid; + +use rider_config::directories::Directories; + +pub mod config; +pub mod images; +pub mod themes; +pub mod write_bytes_to; + +fn main() -> std::io::Result<()> { + let directories = Directories::new(None, None); + config::create(&directories)?; + themes::create(&directories)?; + images::create(&directories)?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::env::set_var; + use std::fs::create_dir_all; + use std::path::Path; + use uuid::Uuid; + + #[cfg(test)] + fn exists(dir: &String, sub: &str) -> bool { + let joined = join(dir.clone(), sub.to_owned()); + Path::new(joined.as_str()).exists() + } + + #[cfg(test)] + fn join(a: String, b: String) -> String { + vec![a, b].join("/") + } + + #[test] + fn assert_main() { + let uniq = Uuid::new_v4(); + let joined = join("/tmp/rider-tests".to_owned(), uniq.to_string()); + let test_path = joined.as_str(); + create_dir_all(test_path.to_owned()).unwrap(); + + set_var("XDG_CONFIG_HOME", test_path); + set_var("XDG_RUNTIME_DIR", test_path); + + assert_eq!(exists(&test_path.to_owned(), ".rider"), false); + assert_eq!(main().is_ok(), true); + assert_eq!(exists(&test_path.to_owned(), ".rider"), true); + } + + #[test] + fn assert_fonts_dir() { + let uniq = Uuid::new_v4(); + let joined = join("/tmp/rider-tests".to_owned(), uniq.to_string()); + create_dir_all(joined.clone()).unwrap(); + set_var("XDG_CONFIG_HOME", joined.as_str().clone()); + set_var("XDG_RUNTIME_HOME", joined.as_str().clone()); + assert_eq!(exists(&joined, "rider/fonts"), false); + assert_eq!(main().is_ok(), true); + assert_eq!(exists(&joined, "rider/fonts"), true); + } + + #[test] + fn assert_log_dir() { + let uniq = Uuid::new_v4(); + let joined = join("/tmp/rider-tests".to_owned(), uniq.to_string()); + create_dir_all(joined.clone()).unwrap(); + set_var("XDG_CONFIG_HOME", joined.as_str().clone()); + set_var("XDG_RUNTIME_HOME", joined.as_str().clone()); + assert_eq!(exists(&joined, "rider/log"), false); + assert_eq!(main().is_ok(), true); + assert_eq!(exists(&joined, "rider/log"), true); + } + + #[test] + fn assert_themes_dir() { + let uniq = Uuid::new_v4(); + let joined = join("/tmp/rider-tests".to_owned(), uniq.to_string()); + create_dir_all(joined.clone()).unwrap(); + set_var("XDG_CONFIG_HOME", joined.as_str().clone()); + set_var("XDG_RUNTIME_HOME", joined.as_str().clone()); + assert_eq!(exists(&joined, "rider/themes"), false); + assert_eq!(main().is_ok(), true); + assert_eq!(exists(&joined, "rider/themes"), true); + } + + #[test] + fn assert_default_json() { + let uniq = Uuid::new_v4(); + let joined = join("/tmp/rider-tests".to_owned(), uniq.to_string()); + create_dir_all(joined.clone()).unwrap(); + set_var("XDG_CONFIG_HOME", joined.as_str().clone()); + set_var("XDG_RUNTIME_HOME", joined.as_str().clone()); + assert_eq!(exists(&joined, "rider/themes/default.json"), false); + assert_eq!(main().is_ok(), true); + assert_eq!(exists(&joined, "rider/themes/default.json"), true); + } + + #[test] + fn assert_railscasts_json() { + let uniq = Uuid::new_v4(); + let joined = join("/tmp/rider-tests".to_owned(), uniq.to_string()); + create_dir_all(joined.clone()).unwrap(); + set_var("XDG_CONFIG_HOME", joined.as_str().clone()); + set_var("XDG_RUNTIME_HOME", joined.as_str().clone()); + assert_eq!(exists(&joined, "rider/themes/railscasts.json"), false); + assert_eq!(main().is_ok(), true); + assert_eq!(exists(&joined, "rider/themes/railscasts.json"), true); + } +} diff --git a/rider-generator/src/themes.rs b/rider-generator/src/themes.rs new file mode 100644 index 0000000..92b2e03 --- /dev/null +++ b/rider-generator/src/themes.rs @@ -0,0 +1,81 @@ +use crate::*; +use rider_themes::predef::*; +use rider_themes::Theme; +use std::fs; +use std::path::PathBuf; + +pub fn create(directories: &Directories) -> std::io::Result<()> { + fs::create_dir_all(directories.themes_dir.clone())?; + for theme in default_styles() { + write_theme(&theme, directories)?; + } + Ok(()) +} + +fn write_theme(theme: &Theme, directories: &Directories) -> std::io::Result<()> { + let mut theme_path = PathBuf::new(); + theme_path.push(directories.themes_dir.clone()); + theme_path.push(format!("{}.json", theme.name())); + let contents = serde_json::to_string_pretty(&theme).unwrap(); + fs::write(&theme_path, contents.clone())?; + Ok(()) +} + +fn default_styles() -> Vec { + vec![default::build_theme(), railscasts::build_theme()] +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs::create_dir_all; + use std::path::Path; + use uuid::Uuid; + + #[test] + fn assert_default_styles() { + assert_eq!(default_styles().len(), 2); + } + + #[cfg(test)] + fn join(a: String, b: String) -> String { + vec![a, b].join("/") + } + + #[test] + fn assert_create_default() { + let uniq = Uuid::new_v4(); + let test_path = join("/tmp/rider-tests".to_owned(), uniq.to_string()); + create_dir_all(test_path.clone()).unwrap(); + let directories = Directories::new(Some(test_path.clone()), None); + let rider_dir = join(test_path.clone(), "rider".to_owned()); + assert_eq!( + Path::new(join(rider_dir.clone(), "themes/default.json".to_owned()).as_str()).exists(), + false + ); + assert_eq!(create(&directories).is_ok(), true); + assert_eq!( + Path::new(join(rider_dir.clone(), "themes/default.json".to_owned()).as_str()).exists(), + true + ); + } + + #[test] + fn assert_create_railscasts() { + let uniq = Uuid::new_v4(); + let test_path = join("/tmp/rider-tests".to_owned(), uniq.to_string()); + create_dir_all(test_path.clone()).unwrap(); + let directories = Directories::new(Some(test_path.clone()), None); + let rider_dir = join(test_path.clone(), "rider".to_owned()); + assert_eq!( + Path::new(join(rider_dir.clone(), "themes/default.json".to_owned()).as_str()).exists(), + false + ); + assert_eq!(create(&directories).is_ok(), true); + assert_eq!( + Path::new(join(rider_dir.clone(), "themes/railscasts.json".to_owned()).as_str()) + .exists(), + true + ); + } +} diff --git a/rider-generator/src/write_bytes_to.rs b/rider-generator/src/write_bytes_to.rs new file mode 100644 index 0000000..616b39b --- /dev/null +++ b/rider-generator/src/write_bytes_to.rs @@ -0,0 +1,34 @@ +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; + +pub fn write_bytes_to(dir: &PathBuf, file: &str, blob: &[u8]) -> std::io::Result<()> { + let mut path = dir.clone(); + path.push(file); + let mut f = File::create(path.to_str().unwrap())?; + f.write_all(blob)?; + f.flush()?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::env::temp_dir; + use std::path::Path; + use uuid::Uuid; + + #[test] + fn must_create_file() { + let test_dir = temp_dir(); + let file_name = Uuid::new_v4().to_string(); + let blob: Vec = vec![1, 2, 3, 4]; + let res = write_bytes_to(&test_dir, file_name.as_str(), blob.as_slice()); + assert_eq!(res.is_ok(), true); + + let mut test_file_path = test_dir.clone(); + test_file_path.push(file_name); + let file_path = Path::new(&test_file_path); + assert_eq!(file_path.exists(), true); + } +} diff --git a/rider-lexers/Cargo.toml b/rider-lexers/Cargo.toml new file mode 100644 index 0000000..bdd7b11 --- /dev/null +++ b/rider-lexers/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "rider-lexers" +version = "0.1.0" +authors = ["Adrian Wozniak "] +edition = "2018" + +[dependencies] +plex = "*" +log = "*" +simplelog = "*" diff --git a/src/lexer/mod.rs b/rider-lexers/src/lib.rs similarity index 90% rename from src/lexer/mod.rs rename to rider-lexers/src/lib.rs index f954299..72bc94a 100644 --- a/src/lexer/mod.rs +++ b/rider-lexers/src/lib.rs @@ -1,3 +1,6 @@ +extern crate log; +extern crate simplelog; + use std::ops::Deref; pub mod plain; @@ -114,19 +117,19 @@ impl Token { } pub fn line(&self) -> usize { - self.line.clone() + self.line } pub fn character(&self) -> usize { - self.character.clone() + self.character } pub fn start(&self) -> usize { - self.start.clone() + self.start } pub fn end(&self) -> usize { - self.end.clone() + self.end } pub fn move_to(&self, line: usize, character: usize, start: usize, end: usize) -> Self { @@ -140,14 +143,14 @@ impl Token { } } -pub fn parse(text: String, language: &Language) -> Vec { +pub fn parse(text: String, language: Language) -> Vec { match language { - &Language::PlainText => plain::lexer::Lexer::new(text.as_str()) - .inspect(|tok| warn!("tok: {:?}", tok)) + Language::PlainText => plain::lexer::Lexer::new(text.as_str()) + // .inspect(|tok| warn!("tok: {:?}", tok)) .map(|t| t.0) .collect(), - &Language::Rust => rust_lang::lexer::Lexer::new(text.as_str()) - .inspect(|tok| warn!("tok: {:?}", tok)) + Language::Rust => rust_lang::lexer::Lexer::new(text.as_str()) + // .inspect(|tok| warn!("tok: {:?}", tok)) .map(|t| t.0) .collect(), } @@ -155,12 +158,13 @@ pub fn parse(text: String, language: &Language) -> Vec { #[cfg(test)] mod tests { - use crate::lexer::*; + use super::*; + use crate::Token; #[test] fn must_parse_plain() { let buffer = "foo bar"; - let language = &Language::PlainText; + let language = Language::PlainText; let result = parse(buffer.to_string(), language); assert_eq!(result.len(), 3); } @@ -168,7 +172,7 @@ mod tests { #[test] fn must_parse_rust() { let buffer = "foo bar"; - let language = &Language::Rust; + let language = Language::Rust; let result = parse(buffer.to_string(), language); assert_eq!(result.len(), 3); } diff --git a/src/lexer/plain.rs b/rider-lexers/src/plain.rs similarity index 83% rename from src/lexer/plain.rs rename to rider-lexers/src/plain.rs index 4244719..8527b3c 100644 --- a/src/lexer/plain.rs +++ b/rider-lexers/src/plain.rs @@ -1,7 +1,5 @@ -use crate::lexer::{Token, TokenType}; - pub mod lexer { - use crate::lexer::{Span, Token, TokenType}; + use crate::{Span, Token, TokenType}; use plex::lexer; lexer! { @@ -10,6 +8,7 @@ pub mod lexer { r"[ \t\r\n]" => (TokenType::Whitespace { token: Token::new(text.to_string(), 0, 0, 0, 0) }, text), + r"[^ \t\r\n]+" => (TokenType::Identifier { token: Token::new(text.to_string(), 0, 0, 0, 0) }, text), @@ -37,31 +36,25 @@ pub mod lexer { type Item = (TokenType, Span); fn next(&mut self) -> Option<(TokenType, Span)> { - loop { - let tok: (TokenType, &str) = - if let Some(((token_type, text), new_remaining)) = next_token(self.remaining) { - self.remaining = new_remaining; - if token_type.is_new_line() { - self.line += 1; - self.character = text.len(); - } else { - self.character += text.len(); - } - (token_type, text) + let tok: (TokenType, &str) = + if let Some(((token_type, text), new_remaining)) = next_token(self.remaining) { + self.remaining = new_remaining; + if token_type.is_new_line() { + self.line += 1; + self.character = text.len(); } else { - return None; - }; - match tok { - (tok, text) => { - let span = self.span_in(text); - let token = tok.move_to( - self.line.clone(), - self.character - text.len(), - span.lo.clone(), - span.hi.clone(), - ); - return Some((token, span)); + self.character += text.len(); } + (token_type, text) + } else { + return None; + }; + match tok { + (tok, text) => { + let span = self.span_in(text); + let token = + tok.move_to(self.line, self.character - text.len(), span.lo, span.hi); + Some((token, span)) } } } @@ -80,8 +73,8 @@ pub mod lexer { #[cfg(test)] mod tests { - use crate::lexer::plain::*; - use crate::lexer::*; + use super::*; + use crate::{Token, TokenType}; #[test] fn must_parse_simple_text() { diff --git a/src/lexer/rust_lang.rs b/rider-lexers/src/rust_lang.rs similarity index 91% rename from src/lexer/rust_lang.rs rename to rider-lexers/src/rust_lang.rs index c0551fb..704d9db 100644 --- a/src/lexer/rust_lang.rs +++ b/rider-lexers/src/rust_lang.rs @@ -1,7 +1,5 @@ -use crate::lexer::{Token, TokenType}; - pub mod lexer { - use crate::lexer::{Span, Token, TokenType}; + use crate::{Span, Token, TokenType}; use plex::lexer; lexer! { @@ -11,6 +9,10 @@ pub mod lexer { token: Token::new(text.to_string(), 0, 0, 0, 0) }, text), + "(r\"|\")" => (TokenType::String { + token: Token::new(text.to_string(), 0, 0, 0, 0) + }, text), + r"([0-9]+|[0-9]+\.[0-9]+|'[^']')" => (TokenType::Literal { token: Token::new(text.to_string(), 0, 0, 0, 0) }, text), @@ -30,6 +32,10 @@ pub mod lexer { r"[^0-9 \t\r\n:+-/*,';<>=%()\[\]{}][^ \t\r\n:+-/*,';<>=%()\[\]{}]*" => (TokenType::Identifier { token: Token::new(text.to_string(), 0, 0, 0, 0) }, text), + + r"'[^0-9 \t\r\n:+-/*,';<>=%()\[\]{}][^ \t\r\n:+-/*,';<>=%()\[\]{}]*" => (TokenType::Identifier { + token: Token::new(text.to_string(), 0, 0, 0, 0) + }, text), } pub struct Lexer<'a> { @@ -54,31 +60,25 @@ pub mod lexer { type Item = (TokenType, Span); fn next(&mut self) -> Option<(TokenType, Span)> { - loop { - let tok: (TokenType, &str) = - if let Some(((token_type, text), new_remaining)) = next_token(self.remaining) { - self.remaining = new_remaining; - if token_type.is_new_line() { - self.line += 1; - self.character = text.len(); - } else { - self.character += text.len(); - } - (token_type, text) + let tok: (TokenType, &str) = + if let Some(((token_type, text), new_remaining)) = next_token(self.remaining) { + self.remaining = new_remaining; + (token_type, text) + } else { + return None; + }; + match tok { + (tok, text) => { + let line = self.line; + if tok.is_new_line() { + self.line += 1; + self.character = text.len(); } else { - return None; - }; - match tok { - (tok, text) => { - let span = self.span_in(text); - let token = tok.move_to( - self.line.clone(), - self.character - text.len(), - span.lo.clone(), - span.hi.clone(), - ); - return Some((token, span)); + self.character += text.len(); } + let span = self.span_in(text); + let token = tok.move_to(line, self.character - text.len(), span.lo, span.hi); + Some((token, span)) } } } @@ -97,8 +97,8 @@ pub mod lexer { #[cfg(test)] mod tests { - use crate::lexer::rust_lang::*; - use crate::lexer::*; + use super::*; + use crate::{Token, TokenType}; #[test] fn must_parse_simple_text() { @@ -207,7 +207,7 @@ mod tests { token: Token::new("foo".to_string(), 0, 0, 0, 3), }, TokenType::Whitespace { - token: Token::new("\n".to_string(), 1, 0, 3, 4), + token: Token::new("\n".to_string(), 0, 0, 3, 4), }, TokenType::Identifier { token: Token::new("bar".to_string(), 1, 1, 4, 7), @@ -402,7 +402,7 @@ mod tests { token: Token::new("{".to_string(), 0, 30, 30, 31), }, TokenType::Whitespace { - token: Token::new("\n".to_string(), 1, 0, 31, 32), + token: Token::new("\n".to_string(), 0, 0, 31, 32), }, TokenType::Whitespace { token: Token::new(" ".to_string(), 1, 1, 32, 44), @@ -423,7 +423,7 @@ mod tests { token: Token::new("b".to_string(), 1, 17, 48, 49), }, TokenType::Whitespace { - token: Token::new("\n".to_string(), 2, 0, 49, 50), + token: Token::new("\n".to_string(), 1, 0, 49, 50), }, TokenType::Whitespace { token: Token::new(" ".to_string(), 2, 1, 50, 58), diff --git a/rider-themes/Cargo.toml b/rider-themes/Cargo.toml new file mode 100644 index 0000000..38e8680 --- /dev/null +++ b/rider-themes/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "rider-themes" +version = "0.1.0" +authors = ["Adrian Wozniak "] +edition = "2018" + +[dependencies] +dirs = "*" +serde = "*" +serde_json = "*" +serde_derive = "*" + +[dependencies.sdl2] +version = "0.31.0" +features = ["gfx", "image", "mixer", "ttf"] diff --git a/src/themes/caret_color.rs b/rider-themes/src/caret_color.rs similarity index 79% rename from src/themes/caret_color.rs rename to rider-themes/src/caret_color.rs index a2965ed..a1f4b3c 100644 --- a/src/themes/caret_color.rs +++ b/rider-themes/src/caret_color.rs @@ -1,5 +1,5 @@ -use crate::themes::SerdeColor; -use crate::themes::ThemeConfig; +use crate::SerdeColor; +use crate::ThemeConfig; #[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] pub struct CaretColor { @@ -10,7 +10,7 @@ pub struct CaretColor { impl Default for CaretColor { fn default() -> Self { Self { - bright: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), + bright: ThemeConfig::new(SerdeColor::new(120, 120, 120, 0), false, false), blur: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), } } diff --git a/rider-themes/src/code_highlighting_color.rs b/rider-themes/src/code_highlighting_color.rs new file mode 100644 index 0000000..2ed1761 --- /dev/null +++ b/rider-themes/src/code_highlighting_color.rs @@ -0,0 +1,273 @@ +use crate::SerdeColor; +use crate::ThemeConfig; + +#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] +pub struct CodeHighlightingColor { + pub comment: ThemeConfig, + pub constant: ThemeConfig, + pub error: ThemeConfig, + pub warning: ThemeConfig, + pub identifier: ThemeConfig, + pub keyword: ThemeConfig, + pub literal: ThemeConfig, + pub number: ThemeConfig, + pub operator: ThemeConfig, + pub separator: ThemeConfig, + pub statement: ThemeConfig, + pub string: ThemeConfig, + pub title: ThemeConfig, + pub type_: ThemeConfig, + pub todo: ThemeConfig, + pub pre_proc: ThemeConfig, + pub special: ThemeConfig, + pub whitespace: ThemeConfig, +} + +impl Default for CodeHighlightingColor { + fn default() -> Self { + Self { + comment: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), + constant: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), + error: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), + warning: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), + identifier: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), + keyword: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), + literal: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), + number: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), + operator: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), + separator: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), + statement: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), + string: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), + title: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), + type_: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), + todo: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), + pre_proc: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), + special: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), + whitespace: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), + } + } +} + +impl CodeHighlightingColor { + pub fn comment(&self) -> &ThemeConfig { + &self.comment + } + + pub fn constant(&self) -> &ThemeConfig { + &self.constant + } + + pub fn error(&self) -> &ThemeConfig { + &self.error + } + + pub fn warning(&self) -> &ThemeConfig { + &self.warning + } + + pub fn identifier(&self) -> &ThemeConfig { + &self.identifier + } + + pub fn keyword(&self) -> &ThemeConfig { + &self.keyword + } + + pub fn literal(&self) -> &ThemeConfig { + &self.literal + } + + pub fn number(&self) -> &ThemeConfig { + &self.number + } + + pub fn operator(&self) -> &ThemeConfig { + &self.operator + } + + pub fn separator(&self) -> &ThemeConfig { + &self.separator + } + + pub fn statement(&self) -> &ThemeConfig { + &self.statement + } + + pub fn string(&self) -> &ThemeConfig { + &self.string + } + + pub fn title(&self) -> &ThemeConfig { + &self.title + } + + pub fn type_(&self) -> &ThemeConfig { + &self.type_ + } + + pub fn todo(&self) -> &ThemeConfig { + &self.todo + } + + pub fn pre_proc(&self) -> &ThemeConfig { + &self.pre_proc + } + + pub fn special(&self) -> &ThemeConfig { + &self.special + } + + pub fn whitespace(&self) -> &ThemeConfig { + &self.whitespace + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn assert_comment() { + let target = CodeHighlightingColor::default(); + let result = target.comment().clone(); + let expected = ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false); + assert_eq!(result, expected); + } + + #[test] + fn assert_constant() { + let target = CodeHighlightingColor::default(); + let result = target.constant().clone(); + let expected = ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false); + assert_eq!(result, expected); + } + + #[test] + fn assert_error() { + let target = CodeHighlightingColor::default(); + let result = target.error().clone(); + let expected = ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false); + assert_eq!(result, expected); + } + + #[test] + fn assert_warning() { + let target = CodeHighlightingColor::default(); + let result = target.warning().clone(); + let expected = ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false); + assert_eq!(result, expected); + } + + #[test] + fn assert_identifier() { + let target = CodeHighlightingColor::default(); + let result = target.identifier().clone(); + let expected = ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false); + assert_eq!(result, expected); + } + + #[test] + fn assert_keyword() { + let target = CodeHighlightingColor::default(); + let result = target.keyword().clone(); + let expected = ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false); + assert_eq!(result, expected); + } + + #[test] + fn assert_literal() { + let target = CodeHighlightingColor::default(); + let result = target.literal().clone(); + let expected = ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false); + assert_eq!(result, expected); + } + + #[test] + fn assert_number() { + let target = CodeHighlightingColor::default(); + let result = target.number().clone(); + let expected = ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false); + assert_eq!(result, expected); + } + + #[test] + fn assert_operator() { + let target = CodeHighlightingColor::default(); + let result = target.operator().clone(); + let expected = ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false); + assert_eq!(result, expected); + } + + #[test] + fn assert_separator() { + let target = CodeHighlightingColor::default(); + let result = target.separator().clone(); + let expected = ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false); + assert_eq!(result, expected); + } + + #[test] + fn assert_statement() { + let target = CodeHighlightingColor::default(); + let result = target.statement().clone(); + let expected = ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false); + assert_eq!(result, expected); + } + + #[test] + fn assert_string() { + let target = CodeHighlightingColor::default(); + let result = target.string().clone(); + let expected = ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false); + assert_eq!(result, expected); + } + + #[test] + fn assert_title() { + let target = CodeHighlightingColor::default(); + let result = target.title().clone(); + let expected = ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false); + assert_eq!(result, expected); + } + + #[test] + fn assert_type_() { + let target = CodeHighlightingColor::default(); + let result = target.type_().clone(); + let expected = ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false); + assert_eq!(result, expected); + } + + #[test] + fn assert_todo() { + let target = CodeHighlightingColor::default(); + let result = target.todo().clone(); + let expected = ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false); + assert_eq!(result, expected); + } + + #[test] + fn assert_pre_proc() { + let target = CodeHighlightingColor::default(); + let result = target.pre_proc().clone(); + let expected = ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false); + assert_eq!(result, expected); + } + + #[test] + fn assert_special() { + let target = CodeHighlightingColor::default(); + let result = target.special().clone(); + let expected = ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false); + assert_eq!(result, expected); + } + + #[test] + fn assert_whitespace() { + let target = CodeHighlightingColor::default(); + let result = target.whitespace().clone(); + let expected = ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false); + assert_eq!(result, expected); + } + +} diff --git a/src/themes/diff_color.rs b/rider-themes/src/diff_color.rs similarity index 67% rename from src/themes/diff_color.rs rename to rider-themes/src/diff_color.rs index 398df64..ac7e586 100644 --- a/src/themes/diff_color.rs +++ b/rider-themes/src/diff_color.rs @@ -1,5 +1,5 @@ -use crate::themes::SerdeColor; -use crate::themes::ThemeConfig; +use crate::SerdeColor; +use crate::ThemeConfig; #[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] pub struct DiffColor { @@ -12,9 +12,9 @@ pub struct DiffColor { impl Default for DiffColor { fn default() -> Self { Self { - add: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), - delete: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), - change: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), + add: ThemeConfig::new(SerdeColor::new(0, 200, 0, 0), false, false), + delete: ThemeConfig::new(SerdeColor::new(200, 0, 0, 0), false, false), + change: ThemeConfig::new(SerdeColor::new(0, 0, 200, 0), false, false), text: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), } } diff --git a/rider-themes/src/images.rs b/rider-themes/src/images.rs new file mode 100644 index 0000000..ad9e833 --- /dev/null +++ b/rider-themes/src/images.rs @@ -0,0 +1,51 @@ +#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] +pub struct ThemeImages { + directory_icon: String, + file_icon: String, +} + +impl ThemeImages { + pub fn new(directory_icon: String, file_icon: String) -> Self { + Self { + file_icon, + directory_icon, + } + } + + pub fn directory_icon(&self) -> String { + self.directory_icon.clone() + } + + pub fn file_icon(&self) -> String { + self.file_icon.clone() + } +} + +impl Default for ThemeImages { + fn default() -> Self { + Self { + directory_icon: "default/images/directory-64x64.png".to_string(), + file_icon: "default/images/file-64x64.png".to_string(), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn assert_directory_icon() { + let config = ThemeImages::new("foo".to_owned(), "bar".to_owned()); + let result = config.directory_icon(); + let expected = "foo".to_owned(); + assert_eq!(result, expected); + } + #[test] + fn assert_file_icon() { + let config = ThemeImages::new("foo".to_owned(), "bar".to_owned()); + let result = config.file_icon(); + let expected = "bar".to_owned(); + assert_eq!(result, expected); + } +} diff --git a/rider-themes/src/lib.rs b/rider-themes/src/lib.rs new file mode 100644 index 0000000..3f47f00 --- /dev/null +++ b/rider-themes/src/lib.rs @@ -0,0 +1,21 @@ +extern crate serde; +extern crate serde_json; +#[macro_use] +extern crate serde_derive; + +pub mod caret_color; +pub mod code_highlighting_color; +pub mod diff_color; +pub mod images; +pub mod predef; +pub mod serde_color; +pub mod theme; +pub mod theme_config; + +pub use crate::caret_color::CaretColor; +pub use crate::code_highlighting_color::CodeHighlightingColor; +pub use crate::diff_color::DiffColor; +pub use crate::images::ThemeImages; +pub use crate::serde_color::SerdeColor; +pub use crate::theme::Theme; +pub use crate::theme_config::ThemeConfig; diff --git a/src/themes/predef/default.rs b/rider-themes/src/predef/default.rs similarity index 68% rename from src/themes/predef/default.rs rename to rider-themes/src/predef/default.rs index 14e6bb2..b9f537e 100644 --- a/src/themes/predef/default.rs +++ b/rider-themes/src/predef/default.rs @@ -1,4 +1,4 @@ -use crate::themes::Theme; +use crate::Theme; pub fn build_theme() -> Theme { Theme::default() diff --git a/src/themes/predef/mod.rs b/rider-themes/src/predef/mod.rs similarity index 100% rename from src/themes/predef/mod.rs rename to rider-themes/src/predef/mod.rs diff --git a/src/themes/predef/railscasts.rs b/rider-themes/src/predef/railscasts.rs similarity index 85% rename from src/themes/predef/railscasts.rs rename to rider-themes/src/predef/railscasts.rs index a89f5f9..29b6c07 100644 --- a/src/themes/predef/railscasts.rs +++ b/rider-themes/src/predef/railscasts.rs @@ -1,14 +1,16 @@ -use crate::themes::caret_color::CaretColor; -use crate::themes::CodeHighlightingColor; -use crate::themes::DiffColor; -use crate::themes::SerdeColor; -use crate::themes::Theme; -use crate::themes::ThemeConfig; +use crate::caret_color::CaretColor; +use crate::CodeHighlightingColor; +use crate::DiffColor; +use crate::SerdeColor; +use crate::Theme; +use crate::ThemeConfig; +use crate::ThemeImages; pub fn build_theme() -> Theme { Theme::new( "railscasts".to_string(), SerdeColor::new(18, 18, 18, 0), + SerdeColor::new(200, 200, 200, 0), CaretColor::new( ThemeConfig::new(SerdeColor::new(121, 121, 121, 0), false, false), ThemeConfig::new(SerdeColor::new(21, 21, 21, 0), false, false), @@ -39,5 +41,9 @@ pub fn build_theme() -> Theme { ThemeConfig::new(SerdeColor::new(135, 0, 135, 0), false, false), ThemeConfig::new(SerdeColor::new(18, 18, 18, 0), false, false), ), + ThemeImages::new( + "railscasts/images/directory-64x64.png".to_owned(), + "railscasts/images/file-64x64.png".to_owned(), + ), ) } diff --git a/rider-themes/src/serde_color.rs b/rider-themes/src/serde_color.rs new file mode 100644 index 0000000..7daabaa --- /dev/null +++ b/rider-themes/src/serde_color.rs @@ -0,0 +1,49 @@ +use sdl2::pixels::Color; + +#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] +pub struct SerdeColor { + pub r: u8, + pub g: u8, + pub b: u8, + pub a: u8, +} + +impl SerdeColor { + pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self { + Self { r, g, b, a } + } +} + +impl Into for &SerdeColor { + fn into(self) -> Color { + Color { + r: self.r, + g: self.g, + b: self.b, + a: self.a, + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use sdl2::pixels::Color; + + #[test] + fn must_cast_serde_color_to_color() { + let target = SerdeColor::new(12, 34, 56, 78); + let color: Color = (&target).into(); + let expected = Color::RGBA(12, 34, 56, 78); + assert_eq!(color, expected); + } + + #[test] + fn must_assign_to_proper_fields() { + let color = SerdeColor::new(12, 34, 56, 78); + assert_eq!(color.r, 12); + assert_eq!(color.g, 34); + assert_eq!(color.b, 56); + assert_eq!(color.a, 78); + } +} diff --git a/rider-themes/src/theme.rs b/rider-themes/src/theme.rs new file mode 100644 index 0000000..80c3ab6 --- /dev/null +++ b/rider-themes/src/theme.rs @@ -0,0 +1,141 @@ +use crate::CaretColor; +use crate::CodeHighlightingColor; +use crate::DiffColor; +use crate::SerdeColor; +use crate::ThemeImages; + +#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] +pub struct Theme { + name: String, + background: SerdeColor, + border_color: SerdeColor, + caret: CaretColor, + code_highlighting: CodeHighlightingColor, + diff: DiffColor, + images: ThemeImages, +} + +impl Default for Theme { + fn default() -> Self { + Self { + name: "default".to_string(), + background: SerdeColor::new(255, 255, 255, 0), + border_color: SerdeColor::new(0, 0, 0, 0), + caret: CaretColor::default(), + code_highlighting: CodeHighlightingColor::default(), + diff: DiffColor::default(), + images: ThemeImages::default(), + } + } +} + +impl Theme { + pub fn new( + name: String, + background: SerdeColor, + border_color: SerdeColor, + caret: CaretColor, + code_highlighting: CodeHighlightingColor, + diff: DiffColor, + images: ThemeImages, + ) -> Self { + Self { + name, + background, + border_color, + caret, + code_highlighting, + diff, + images, + } + } + + pub fn name(&self) -> &String { + &self.name + } + + pub fn background(&self) -> &SerdeColor { + &self.background + } + + pub fn border_color(&self) -> &SerdeColor { + &self.border_color + } + + pub fn caret(&self) -> &CaretColor { + &self.caret + } + + pub fn diff(&self) -> &DiffColor { + &self.diff + } + + pub fn code_highlighting(&self) -> &CodeHighlightingColor { + &self.code_highlighting + } + + pub fn images(&self) -> &ThemeImages { + &self.images + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn assert_name() { + let target = Theme::default(); + let result = target.name().clone(); + let expected = "default".to_owned(); + assert_eq!(result, expected); + } + + #[test] + fn assert_background() { + let target = Theme::default(); + let result = target.background().clone(); + let expected = SerdeColor::new(255, 255, 255, 0); + assert_eq!(result, expected); + } + + #[test] + fn assert_border_color() { + let target = Theme::default(); + let result = target.border_color().clone(); + let expected = SerdeColor::new(0, 0, 0, 0); + assert_eq!(result, expected); + } + + #[test] + fn assert_caret() { + let target = Theme::default(); + let result = target.caret().clone(); + let expected = CaretColor::default(); + assert_eq!(result, expected); + } + + #[test] + fn assert_diff() { + let target = Theme::default(); + let result = target.diff().clone(); + let expected = DiffColor::default(); + assert_eq!(result, expected); + } + + #[test] + fn assert_code_highlighting() { + let target = Theme::default(); + let result = target.code_highlighting().clone(); + let expected = CodeHighlightingColor::default(); + assert_eq!(result, expected); + } + + #[test] + fn assert_images() { + let target = Theme::default(); + let result = target.images().clone(); + let expected = ThemeImages::default(); + assert_eq!(result, expected); + } +} diff --git a/rider-themes/src/theme_config.rs b/rider-themes/src/theme_config.rs new file mode 100644 index 0000000..e1d03bc --- /dev/null +++ b/rider-themes/src/theme_config.rs @@ -0,0 +1,59 @@ +use crate::SerdeColor; + +#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] +pub struct ThemeConfig { + color: SerdeColor, + italic: bool, + bold: bool, +} + +impl ThemeConfig { + pub fn new(color: SerdeColor, italic: bool, bold: bool) -> Self { + Self { + color, + italic, + bold, + } + } + + pub fn color(&self) -> &SerdeColor { + &self.color + } + + pub fn italic(&self) -> bool { + self.italic + } + + pub fn bold(&self) -> bool { + self.bold + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn assert_color() { + let target = ThemeConfig::new(SerdeColor::new(29, 20, 45, 72), true, false); + let result = target.color().clone(); + let expected = SerdeColor::new(29, 20, 45, 72); + assert_eq!(result, expected); + } + + #[test] + fn assert_italic() { + let target = ThemeConfig::new(SerdeColor::new(29, 20, 45, 72), true, false); + let result = target.italic(); + let expected = true; + assert_eq!(result, expected); + } + + #[test] + fn assert_bold() { + let target = ThemeConfig::new(SerdeColor::new(29, 20, 45, 72), false, true); + let result = target.bold(); + let expected = true; + assert_eq!(result, expected); + } +} diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100755 index 0000000..ee22031 --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env zsh + +cargo test -p rider-generator +cargo test -p rider-config +cargo test -p rider-themes +cargo test -p rider-lexers +cargo test -p rider-editor diff --git a/src/app/app_state.rs b/src/app/app_state.rs deleted file mode 100644 index dc0b98d..0000000 --- a/src/app/app_state.rs +++ /dev/null @@ -1,113 +0,0 @@ -use crate::app::caret_manager; -use crate::app::file_content_manager; -use crate::app::{UpdateResult, WindowCanvas as WC}; -use crate::config::*; -use crate::renderer::Renderer; -use crate::ui::caret::*; -use crate::ui::file::editor_file::EditorFile; -use crate::ui::file::*; -use crate::ui::menu_bar::MenuBar; -use crate::ui::text_character::TextCharacter; -use crate::ui::*; -use sdl2::rect::{Point, Rect}; -use sdl2::VideoSubsystem as VS; -use std::boxed::Box; -use std::rc::Rc; -use std::sync::*; - -pub struct AppState { - menu_bar: MenuBar, - files: Vec, - config: Arc>, - file_editor: FileEditor, -} - -impl AppState { - pub fn new(config: Arc>) -> Self { - Self { - menu_bar: MenuBar::new(Arc::clone(&config)), - files: vec![], - file_editor: FileEditor::new(Arc::clone(&config)), - config, - } - } - - pub fn open_file(&mut self, file_path: String, renderer: &mut Renderer) { - use std::fs::read_to_string; - - if let Ok(buffer) = read_to_string(&file_path) { - let mut file = EditorFile::new(file_path.clone(), buffer, self.config.clone()); - file.prepare_ui(renderer); - match self.file_editor.open_file(file) { - Some(old) => self.files.push(old), - _ => (), - } - } else { - eprintln!("Failed to open file: {}", file_path); - }; - } - - pub fn file_editor(&self) -> &FileEditor { - &self.file_editor - } - - pub fn file_editor_mut(&mut self) -> &mut FileEditor { - &mut self.file_editor - } -} - -impl Render for AppState { - fn render(&self, canvas: &mut WC, renderer: &mut Renderer, _context: &RenderContext) { - self.file_editor - .render(canvas, renderer, &RenderContext::Nothing); - self.menu_bar - .render(canvas, renderer, &RenderContext::Nothing); - } - - fn prepare_ui(&mut self, renderer: &mut Renderer) { - self.menu_bar.prepare_ui(renderer); - self.file_editor.prepare_ui(renderer); - } -} - -impl Update for AppState { - fn update(&mut self, ticks: i32, context: &UpdateContext) -> UpdateResult { - self.menu_bar.update(ticks, context); - self.file_editor.update(ticks, context); - UpdateResult::NoOp - } -} - -impl AppState { - pub fn on_left_click(&mut self, point: &Point, video_subsystem: &mut VS) -> UpdateResult { - if self - .menu_bar - .is_left_click_target(point, &UpdateContext::Nothing) - { - video_subsystem.text_input().stop(); - return self.menu_bar.on_left_click(point, &UpdateContext::Nothing); - } else { - if !self - .file_editor - .is_left_click_target(point, &UpdateContext::Nothing) - { - return UpdateResult::NoOp; - } else { - video_subsystem.text_input().start(); - self.file_editor - .on_left_click(point, &UpdateContext::Nothing); - } - } - UpdateResult::NoOp - } - - pub fn is_left_click_target(&self, _point: &Point) -> bool { - true - } -} - -impl ConfigHolder for AppState { - fn config(&self) -> &ConfigAccess { - &self.config - } -} diff --git a/src/app/application.rs b/src/app/application.rs deleted file mode 100644 index 246e96a..0000000 --- a/src/app/application.rs +++ /dev/null @@ -1,260 +0,0 @@ -pub use crate::app::app_state::AppState; -pub use crate::config::{Config, ConfigAccess, ConfigHolder}; -pub use crate::renderer::Renderer; -use crate::themes::*; -use crate::ui::caret::{CaretPosition, MoveDirection}; -use crate::ui::*; - -use std::rc::Rc; -use std::sync::*; -use std::thread::sleep; -use std::time::Duration; - -use sdl2::event::*; -use sdl2::hint; -use sdl2::keyboard::{Keycode, Mod}; -use sdl2::mouse::*; -use sdl2::pixels::{Color, PixelFormatEnum}; -use sdl2::rect::{Point, Rect}; -use sdl2::render::Canvas; -use sdl2::rwops::RWops; -use sdl2::surface::Surface; -use sdl2::ttf::Sdl2TtfContext; -use sdl2::video::Window; -use sdl2::EventPump; -use sdl2::{Sdl, TimerSubsystem, VideoSubsystem}; - -pub type WindowCanvas = Canvas; - -#[derive(PartialEq, Clone, Debug)] -pub enum UpdateResult { - NoOp, - Stop, - RefreshPositions, - MouseLeftClicked(Point), - MoveCaret(Rect, CaretPosition), - DeleteFront, - DeleteBack, - Input(String), - InsertNewLine, - MoveCaretLeft, - MoveCaretRight, - MoveCaretUp, - MoveCaretDown, - Scroll { x: i32, y: i32 }, - WindowResize { width: i32, height: i32 }, -} - -pub enum Task { - OpenFile { file_path: String }, -} - -pub struct Application { - config: Arc>, - clear_color: Color, - sdl_context: Sdl, - canvas: WindowCanvas, - video_subsystem: VideoSubsystem, - tasks: Vec, -} - -impl Application { - pub fn new() -> Self { - let config = Arc::new(RwLock::new(Config::new())); - let sdl_context = sdl2::init().unwrap(); - - hint::set("SDL_GL_MULTISAMPLEBUFFERS", "1"); - hint::set("SDL_GL_MULTISAMPLESAMPLES", "8"); - hint::set("SDL_GL_ACCELERATED_VISUAL", "1"); - hint::set("SDL_HINT_RENDER_SCALE_QUALITY", "2"); - hint::set("SDL_HINT_VIDEO_ALLOW_SCREENSAVER", "1"); - - let video_subsystem = sdl_context.video().unwrap(); - - let mut window: Window = { - let c = config.read().unwrap(); - video_subsystem - .window("Rider", c.width(), c.height()) - .position_centered() - .resizable() - .opengl() - .build() - .unwrap() - }; - let icon_bytes = include_bytes!("../../assets/gear-64x64.bmp").clone(); - let mut rw = RWops::from_bytes(&icon_bytes).unwrap(); - let mut icon = Surface::load_bmp_rw(&mut rw).unwrap(); - window.set_icon(&mut icon); - - let canvas = window.into_canvas().accelerated().build().unwrap(); - let clear_color: Color = { config.read().unwrap().theme().background().into() }; - - Self { - sdl_context, - video_subsystem, - canvas, - tasks: vec![], - clear_color, - config, - } - } - - pub fn init(&mut self) { - self.clear(); - } - - pub fn run(&mut self) { - let mut timer: TimerSubsystem = self.sdl_context.timer().unwrap(); - let mut event_pump = self.sdl_context.event_pump().unwrap(); - let font_context = sdl2::ttf::init().unwrap(); - let texture_creator = self.canvas.texture_creator(); - let sleep_time = Duration::new(0, 1_000_000_000u32 / 60); - let mut app_state = AppState::new(Arc::clone(&self.config)); - let mut renderer = Renderer::new(Arc::clone(&self.config), &font_context, &texture_creator); - app_state.prepare_ui(&mut renderer); - - 'running: loop { - match self.handle_events(&mut event_pump) { - UpdateResult::Stop => break 'running, - UpdateResult::RefreshPositions => (), - UpdateResult::NoOp => (), - UpdateResult::MoveCaret(_, _pos) => (), - UpdateResult::MouseLeftClicked(point) => { - app_state.on_left_click(&point, &mut self.video_subsystem); - } - UpdateResult::DeleteFront => { - app_state.file_editor_mut().delete_front(&mut renderer); - } - UpdateResult::DeleteBack => { - app_state.file_editor_mut().delete_back(&mut renderer); - } - UpdateResult::Input(text) => { - app_state.file_editor_mut().insert_text(text, &mut renderer); - } - UpdateResult::InsertNewLine => { - app_state.file_editor_mut().insert_new_line(&mut renderer); - } - UpdateResult::MoveCaretLeft => { - app_state.file_editor_mut().move_caret(MoveDirection::Left); - } - UpdateResult::MoveCaretRight => { - app_state.file_editor_mut().move_caret(MoveDirection::Right); - } - UpdateResult::MoveCaretUp => { - app_state.file_editor_mut().move_caret(MoveDirection::Up); - } - UpdateResult::MoveCaretDown => { - app_state.file_editor_mut().move_caret(MoveDirection::Down); - } - UpdateResult::Scroll { x, y } => { - app_state.file_editor_mut().scroll_to(-x, -y); - } - UpdateResult::WindowResize { width, height } => { - let mut c = app_state.config().write().unwrap(); - if width > 0 { - c.set_width(width as u32); - } - if height > 0 { - c.set_height(height as u32); - } - } - } - for task in self.tasks.iter() { - match task { - Task::OpenFile { file_path } => { - use crate::ui::file::editor_file::*; - app_state.open_file(file_path.clone(), &mut renderer); - } - } - } - self.tasks.clear(); - - self.clear(); - - app_state.update(timer.ticks() as i32, &UpdateContext::Nothing); - app_state.render(&mut self.canvas, &mut renderer, &RenderContext::Nothing); - - self.present(); - sleep(sleep_time); - } - } - - pub fn open_file(&mut self, file_path: String) { - self.tasks.push(Task::OpenFile { file_path }); - } - - fn present(&mut self) { - self.canvas.present(); - } - - fn clear(&mut self) { - self.canvas.set_draw_color(self.clear_color.clone()); - self.canvas.clear(); - } - - fn handle_events(&mut self, event_pump: &mut EventPump) -> UpdateResult { - for event in event_pump.poll_iter() { - match event { - Event::Quit { .. } => return UpdateResult::Stop, - Event::MouseButtonUp { - mouse_btn, x, y, .. - } => match mouse_btn { - MouseButton::Left => return UpdateResult::MouseLeftClicked(Point::new(x, y)), - _ => (), - }, - Event::KeyDown { keycode, .. } => { - let keycode = if keycode.is_some() { - keycode.unwrap() - } else { - return UpdateResult::NoOp; - }; - match keycode { - Keycode::Backspace => return UpdateResult::DeleteFront, - Keycode::Delete => return UpdateResult::DeleteBack, - Keycode::KpEnter | Keycode::Return => return UpdateResult::InsertNewLine, - Keycode::Left => return UpdateResult::MoveCaretLeft, - Keycode::Right => return UpdateResult::MoveCaretRight, - Keycode::Up => return UpdateResult::MoveCaretUp, - Keycode::Down => return UpdateResult::MoveCaretDown, - _ => UpdateResult::NoOp, - }; - } - Event::TextInput { text, .. } => { - return UpdateResult::Input(text); - } - Event::MouseWheel { - direction, x, y, .. - } => { - match direction { - MouseWheelDirection::Normal => { - return UpdateResult::Scroll { x, y }; - } - MouseWheelDirection::Flipped => { - return UpdateResult::Scroll { x, y: -y }; - } - _ => { - // ignore - } - }; - } - Event::Window { - win_event: WindowEvent::Resized(w, h), - .. - } => { - return UpdateResult::WindowResize { - width: w, - height: h, - }; - } - _ => (), - } - } - UpdateResult::NoOp - } -} - -impl ConfigHolder for Application { - fn config(&self) -> &ConfigAccess { - &self.config - } -} diff --git a/src/app/caret_manager.rs b/src/app/caret_manager.rs deleted file mode 100644 index 2058ae7..0000000 --- a/src/app/caret_manager.rs +++ /dev/null @@ -1,64 +0,0 @@ -use crate::app::AppState; -use crate::ui::*; -use sdl2::rect::{Point, Rect}; - -pub fn move_caret_right(file_editor: &mut FileEditor) { - let file: &EditorFile = match file_editor.file() { - None => return, - Some(f) => f, - }; - let c: TextCharacter = match file.get_character_at(file_editor.caret().text_position() + 1) { - Some(text_character) => text_character, - None => return, // EOF - }; - let caret_rect = file_editor.caret().dest().clone(); - let pos = file_editor.caret().position(); - let (d, p): (Rect, CaretPosition) = match ( - c.is_last_in_line(), - c.is_new_line(), - c.dest().y() == caret_rect.y(), - ) { - (true, true, false) => { - let prev: TextCharacter = if c.position() != 0 { - file.get_character_at(c.position() - 1).unwrap_or(c.clone()) - } else { - c.clone() - }; - let mut dest = prev.dest().clone(); - dest.set_x(dest.x() + dest.width() as i32); - (dest, pos.moved(1, 0, 0)) - } - (false, true, false) => { - let prev: TextCharacter = if c.position() != 0 { - file.get_character_at(c.position() - 1).unwrap_or(c.clone()) - } else { - c.clone() - }; - let mut dest = prev.dest().clone(); - if !prev.is_new_line() { - dest.set_x(dest.x() + dest.width() as i32); - } - (dest, pos.moved(1, 0, 0)) - } - (true, false, false) => { - // move after character, stay on current line - (c.dest().clone(), pos.moved(1, 0, 0)) - } - (true, false, true) => { - // move to new line - (c.dest().clone(), pos.moved(1, 0, 0)) - } - _ => (c.dest().clone(), pos.moved(1, 0, 0)), - }; - file_editor - .caret_mut() - .move_caret(p, Point::new(d.x(), d.y())); -} - -pub fn move_caret_left(file_editor: &mut FileEditor) { - let _file: &EditorFile = match file_editor.file() { - None => return, - Some(f) => f, - }; - let _line = file_editor.caret().line_number(); -} diff --git a/src/config/config.rs b/src/config/config.rs deleted file mode 100644 index 3c11652..0000000 --- a/src/config/config.rs +++ /dev/null @@ -1,138 +0,0 @@ -use crate::config::creator; -use crate::config::EditorConfig; -use crate::config::ScrollConfig; -use crate::lexer::Language; -use crate::themes::Theme; -use dirs; -use std::collections::HashMap; -use std::fs; - -pub type LanguageMapping = HashMap; - -#[derive(Debug, Clone)] -pub struct Config { - width: u32, - height: u32, - menu_height: u16, - editor_config: EditorConfig, - theme: Theme, - extensions_mapping: LanguageMapping, - scroll: ScrollConfig, -} - -impl Config { - pub fn new() -> Self { - creator::create(); - let editor_config = EditorConfig::new(); - let mut extensions_mapping = HashMap::new(); - extensions_mapping.insert(".".to_string(), Language::PlainText); - extensions_mapping.insert("txt".to_string(), Language::PlainText); - extensions_mapping.insert("rs".to_string(), Language::Rust); - - Self { - width: 1024, - height: 860, - menu_height: 60, - theme: Theme::load(editor_config.current_theme().clone()), - editor_config, - extensions_mapping, - scroll: ScrollConfig::new(), - } - } - - pub fn width(&self) -> u32 { - self.width - } - - pub fn set_width(&mut self, w: u32) { - self.width = w; - } - - pub fn height(&self) -> u32 { - self.height - } - - pub fn set_height(&mut self, h: u32) { - self.height = h; - } - - pub fn editor_config(&self) -> &EditorConfig { - &self.editor_config - } - - pub fn theme(&self) -> &Theme { - &self.theme - } - - pub fn menu_height(&self) -> u16 { - self.menu_height - } - - pub fn editor_top_margin(&self) -> i32 { - (self.menu_height() as i32) + (self.editor_config().margin_top() as i32) - } - - pub fn editor_left_margin(&self) -> i32 { - self.editor_config().margin_left() as i32 - } - - pub fn extensions_mapping(&self) -> &LanguageMapping { - &self.extensions_mapping - } - - pub fn scroll(&self) -> &ScrollConfig { - &self.scroll - } - - pub fn scroll_mut(&mut self) -> &mut ScrollConfig { - &mut self.scroll - } -} - -#[cfg(test)] -mod tests { - use crate::config::*; - use crate::lexer::*; - - #[test] - fn must_return_language_mapping() { - let config = Config::new(); - - let mapping = config.extensions_mapping(); - { - let mut keys: Vec = mapping.keys().map(|s| s.to_string()).collect(); - let mut expected: Vec = - vec![".".to_string(), "txt".to_string(), "rs".to_string()]; - keys.sort(); - expected.sort(); - assert_eq!(keys, expected); - } - { - let mut keys: Vec = mapping.values().map(|s| s.clone()).collect(); - let mut expected: Vec = - vec![Language::PlainText, Language::PlainText, Language::Rust]; - keys.sort(); - expected.sort(); - assert_eq!(keys, expected); - } - } - - #[test] - fn assert_scroll() { - let config = Config::new(); - let result = config.scroll(); - let expected = ScrollConfig::new(); - assert_eq!(result.clone(), expected); - } - - #[test] - fn assert_scroll_mut() { - let mut config = Config::new(); - let result = config.scroll_mut(); - result.set_margin_right(1236); - let mut expected = ScrollConfig::new(); - expected.set_margin_right(1236); - assert_eq!(result.clone(), expected); - } - -} diff --git a/src/config/creator.rs b/src/config/creator.rs deleted file mode 100644 index 8a3db7b..0000000 --- a/src/config/creator.rs +++ /dev/null @@ -1,51 +0,0 @@ -use crate::config::directories::*; -use crate::themes::config_creator; -use dirs; -use std::fs; -use std::path; - -pub fn create() { - if !themes_dir().exists() { - let r = fs::create_dir_all(&themes_dir()); - #[cfg_attr(tarpaulin, skip)] - r.unwrap_or_else(|_| panic!("Cannot create themes config directory")); - } - - if !fonts_dir().exists() { - let r = fs::create_dir_all(&fonts_dir()); - #[cfg_attr(tarpaulin, skip)] - r.unwrap_or_else(|_| panic!("Cannot create fonts config directory")); - write_default_fonts(); - } - - if !log_dir().exists() { - let r = fs::create_dir_all(&log_dir()); - #[cfg_attr(tarpaulin, skip)] - r.unwrap_or_else(|_| panic!("Cannot create log directory")); - } - - if !project_dir().exists() { - let r = fs::create_dir_all(&project_dir()); - #[cfg_attr(tarpaulin, skip)] - r.unwrap_or_else(|_| panic!("Cannot create project directory")); - } -} - -fn write_default_fonts() { - { - let mut default_font_path = fonts_dir(); - default_font_path.push("DejaVuSansMono.ttf"); - let contents = include_bytes!("../../assets/fonts/DejaVuSansMono.ttf"); - let r = fs::write(default_font_path, contents.to_vec()); - #[cfg_attr(tarpaulin, skip)] - r.unwrap_or_else(|_| panic!("Cannot write default font file!")); - } - { - let mut default_font_path = fonts_dir(); - default_font_path.push("ElaineSans-Medium.ttf"); - let contents = include_bytes!("../../assets/fonts/ElaineSans-Medium.ttf"); - let r = fs::write(default_font_path, contents.to_vec()); - #[cfg_attr(tarpaulin, skip)] - r.unwrap_or_else(|_| panic!("Cannot write default font file!")); - } -} diff --git a/src/config/directories.rs b/src/config/directories.rs deleted file mode 100644 index bf39ab2..0000000 --- a/src/config/directories.rs +++ /dev/null @@ -1,36 +0,0 @@ -use dirs; -use std::path::PathBuf; - -pub fn log_dir() -> PathBuf { - let mut log_dir = config_dir(); - log_dir.push("log"); - log_dir -} - -pub fn themes_dir() -> PathBuf { - let mut themes_dir = config_dir(); - themes_dir.push("themes"); - themes_dir -} - -pub fn fonts_dir() -> PathBuf { - let mut fonts_dir = config_dir(); - fonts_dir.push("fonts"); - fonts_dir -} - -pub fn config_dir() -> PathBuf { - let home_dir = dirs::config_dir().unwrap(); - - let mut config_dir = home_dir.clone(); - config_dir.push("rider"); - config_dir -} - -pub fn project_dir() -> PathBuf { - let runtime = dirs::runtime_dir().unwrap(); - - let mut project_dir = runtime.clone(); - project_dir.push(".rider"); - project_dir -} diff --git a/src/config/editor_config.rs b/src/config/editor_config.rs deleted file mode 100644 index cb6e6b9..0000000 --- a/src/config/editor_config.rs +++ /dev/null @@ -1,44 +0,0 @@ -use crate::config::directories; - -#[derive(Debug, Clone)] -pub struct EditorConfig { - character_size: u16, - font_path: String, - current_theme: String, - margin_left: u16, - margin_top: u16, -} - -impl EditorConfig { - pub fn new() -> Self { - let mut default_font_path = directories::fonts_dir(); - default_font_path.push("DejaVuSansMono.ttf"); - Self { - character_size: 14, - font_path: default_font_path.to_str().unwrap().to_string(), - current_theme: "railscasts".to_string(), - margin_left: 10, - margin_top: 10, - } - } - - pub fn character_size(&self) -> u16 { - self.character_size - } - - pub fn font_path(&self) -> &String { - &self.font_path - } - - pub fn current_theme(&self) -> &String { - &self.current_theme - } - - pub fn margin_left(&self) -> u16 { - self.margin_left - } - - pub fn margin_top(&self) -> u16 { - self.margin_top - } -} diff --git a/src/main.rs b/src/main.rs index ca5ddb7..7f8ba6e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,59 +1,53 @@ -#![allow(unused_imports)] - -extern crate dirs; -extern crate plex; -extern crate rand; -extern crate sdl2; -#[macro_use] -extern crate serde; -#[macro_use] -extern crate serde_derive; -#[macro_use] -extern crate serde_json; -#[macro_use] -extern crate log; -extern crate simplelog; -#[macro_use] -extern crate lazy_static; - -use crate::app::Application; -use crate::config::directories::log_dir; -use log::Level; -use simplelog::*; -use std::fs::create_dir_all; -use std::fs::File; - -pub mod app; -pub mod config; -pub mod lexer; -pub mod renderer; -#[cfg(test)] -pub mod tests; -pub mod themes; -pub mod ui; - -fn init_logger() { - use simplelog::SharedLogger; - - let mut log_file_path = log_dir(); - log_file_path.push("rider.log"); - - let mut outputs: Vec> = vec![WriteLogger::new( - LevelFilter::Info, - Config::default(), - File::create(log_file_path).unwrap(), - )]; - if let Some(term) = TermLogger::new(LevelFilter::Warn, Config::default()) { - outputs.push(term); - } - - CombinedLogger::init(outputs).unwrap(); -} +extern crate rider_config; +use std::process::Command; fn main() { - let mut app = Application::new(); - app.init(); - init_logger(); - app.open_file("./assets/examples/test.rs".to_string()); - app.run(); + let generator = rider_config::directories::get_binary_path("rider-generator").unwrap(); + println!("generator will be {:?}", generator); + Command::new(generator).status().unwrap(); + + let editor = rider_config::directories::get_binary_path("rider-editor").unwrap(); + println!("editor will be {:?}", editor); + Command::new(editor).status().unwrap(); +} + +#[cfg(test)] +mod tests { + use super::*; + use std::env::set_var; + use std::fs::create_dir_all; + use std::path::Path; + use uuid::Uuid; + + #[cfg(test)] + fn exists(dir: &String, sub: &str) -> bool { + Path::new(join(dir.clone(), sub.to_owned()).as_str()).exists() + } + + #[cfg(test)] + fn join(a: String, b: String) -> String { + vec![a, b].join("/") + } + + #[test] + fn assert_main() { + let uniq = Uuid::new_v4(); + let test_path = join("/tmp/rider-tests".to_owned(), uniq.to_string()); + create_dir_all(test_path.clone()).unwrap(); + set_var("XDG_CONFIG_HOME", test_path.as_str()); + set_var("XDG_RUNTIME_DIR", test_path.as_str()); + let rider_dir = join(test_path.clone(), "rider".to_owned()); + assert_eq!(exists(&rider_dir, "themes"), false); + assert_eq!(exists(&rider_dir, "log"), false); + assert_eq!(exists(&test_path, ".rider"), false); + assert_eq!(exists(&rider_dir, "themes/default.json"), false); + assert_eq!(exists(&rider_dir, "themes/railscasts.json"), false); + main(); + assert_eq!(exists(&rider_dir, "fonts"), true); + assert_eq!(exists(&rider_dir, "log"), true); + assert_eq!(exists(&rider_dir, "themes"), true); + assert_eq!(exists(&test_path, ".rider"), true); + assert_eq!(exists(&rider_dir, "themes/default.json"), true); + assert_eq!(exists(&rider_dir, "themes/railscasts.json"), true); + } } diff --git a/src/renderer/renderer.rs b/src/renderer/renderer.rs deleted file mode 100644 index a872539..0000000 --- a/src/renderer/renderer.rs +++ /dev/null @@ -1,45 +0,0 @@ -use crate::app::WindowCanvas as WC; -use crate::config::{Config, ConfigAccess, ConfigHolder}; -use crate::renderer::managers::*; -use sdl2::rect::{Point, Rect}; -use sdl2::render::{Texture, TextureCreator}; -use sdl2::ttf::Sdl2TtfContext; -use sdl2::video::WindowContext as WinCtxt; -use std::rc::Rc; -use std::sync::*; - -pub struct Renderer<'l> { - config: ConfigAccess, - font_manager: FontManager<'l>, - texture_manager: TextureManager<'l, WinCtxt>, -} - -impl<'l> Renderer<'l> { - pub fn new( - config: ConfigAccess, - font_context: &'l Sdl2TtfContext, - texture_creator: &'l TextureCreator, - ) -> Self { - Self { - config, - font_manager: FontManager::new(&font_context), - texture_manager: TextureManager::new(&texture_creator), - } - } -} - -impl<'l> ManagersHolder<'l> for Renderer<'l> { - fn font_manager(&mut self) -> &mut FontManager<'l> { - &mut self.font_manager - } - - fn texture_manager(&mut self) -> &mut TextureManager<'l, WinCtxt> { - &mut self.texture_manager - } -} - -impl<'l> ConfigHolder for Renderer<'l> { - fn config(&self) -> &ConfigAccess { - &self.config - } -} diff --git a/src/tests.rs b/src/tests.rs deleted file mode 100644 index 04199f4..0000000 --- a/src/tests.rs +++ /dev/null @@ -1,28 +0,0 @@ -#[cfg(test)] -pub mod support { - use crate::config::*; - use crate::renderer::*; - use sdl2::render::{Canvas, WindowCanvas}; - use sdl2::*; - use sdl2::{Sdl, TimerSubsystem, VideoSubsystem}; - use std::borrow::*; - use std::sync::*; - - pub fn build_config() -> Arc> { - Arc::new(RwLock::new(Config::new())) - } - - pub fn build_canvas() -> WindowCanvas { - let sdl_context = sdl2::init().unwrap(); - let video_subsystem = sdl_context.video().unwrap(); - - let window = video_subsystem - .window("Test", 1, 1) - .borderless() - .opengl() - .build() - .unwrap(); - - window.into_canvas().accelerated().build().unwrap() - } -} diff --git a/src/themes/code_highlighting_color.rs b/src/themes/code_highlighting_color.rs deleted file mode 100644 index 4ae0def..0000000 --- a/src/themes/code_highlighting_color.rs +++ /dev/null @@ -1,123 +0,0 @@ -use crate::themes::SerdeColor; -use crate::themes::ThemeConfig; - -#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] -pub struct CodeHighlightingColor { - pub comment: ThemeConfig, - pub constant: ThemeConfig, - pub error: ThemeConfig, - pub warning: ThemeConfig, - pub identifier: ThemeConfig, - pub keyword: ThemeConfig, - pub literal: ThemeConfig, - pub number: ThemeConfig, - pub operator: ThemeConfig, - pub separator: ThemeConfig, - pub statement: ThemeConfig, - pub string: ThemeConfig, - pub title: ThemeConfig, - pub type_: ThemeConfig, - pub todo: ThemeConfig, - pub pre_proc: ThemeConfig, - pub special: ThemeConfig, - pub whitespace: ThemeConfig, -} - -impl Default for CodeHighlightingColor { - fn default() -> Self { - Self { - comment: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), - constant: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), - error: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), - warning: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), - identifier: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), - keyword: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), - literal: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), - number: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), - operator: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), - separator: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), - statement: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), - string: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), - title: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), - type_: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), - todo: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), - pre_proc: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), - special: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), - whitespace: ThemeConfig::new(SerdeColor::new(0, 0, 0, 0), false, false), - } - } -} - -impl CodeHighlightingColor { - pub fn comment(&self) -> &ThemeConfig { - &self.comment - } - - pub fn constant(&self) -> &ThemeConfig { - &self.constant - } - - pub fn error(&self) -> &ThemeConfig { - &self.error - } - - pub fn warning(&self) -> &ThemeConfig { - &self.warning - } - - pub fn identifier(&self) -> &ThemeConfig { - &self.identifier - } - - pub fn keyword(&self) -> &ThemeConfig { - &self.keyword - } - - pub fn literal(&self) -> &ThemeConfig { - &self.literal - } - - pub fn number(&self) -> &ThemeConfig { - &self.number - } - - pub fn operator(&self) -> &ThemeConfig { - &self.operator - } - - pub fn separator(&self) -> &ThemeConfig { - &self.separator - } - - pub fn statement(&self) -> &ThemeConfig { - &self.statement - } - - pub fn string(&self) -> &ThemeConfig { - &self.string - } - - pub fn title(&self) -> &ThemeConfig { - &self.title - } - - pub fn type_(&self) -> &ThemeConfig { - &self.type_ - } - - pub fn todo(&self) -> &ThemeConfig { - &self.todo - } - - pub fn pre_proc(&self) -> &ThemeConfig { - &self.pre_proc - } - - pub fn special(&self) -> &ThemeConfig { - &self.special - } - - pub fn whitespace(&self) -> &ThemeConfig { - &self.whitespace - } -} diff --git a/src/themes/config_creator.rs b/src/themes/config_creator.rs deleted file mode 100644 index 2f5c432..0000000 --- a/src/themes/config_creator.rs +++ /dev/null @@ -1,26 +0,0 @@ -use crate::config::directories::*; -use crate::themes::predef::*; -use crate::themes::*; -use dirs; -use std::fs; -use std::path::PathBuf; - -pub fn create() { - fs::create_dir_all(themes_dir()) - .unwrap_or_else(|_| panic!("Cannot create theme config directory")); - for theme in default_styles() { - write_theme(&theme); - } -} - -fn write_theme(theme: &Theme) { - let mut theme_path = themes_dir(); - theme_path.push(format!("{}.json", theme.name())); - let contents = serde_json::to_string_pretty(&theme).unwrap(); - fs::write(&theme_path, contents.clone()) - .unwrap_or_else(|_| panic!("Failed to crate theme config file")); -} - -fn default_styles() -> Vec { - vec![default::build_theme(), railscasts::build_theme()] -} diff --git a/src/themes/mod.rs b/src/themes/mod.rs deleted file mode 100644 index ef08ffb..0000000 --- a/src/themes/mod.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::config::directories::*; -use sdl2::pixels::Color; -use serde::ser::{Serialize, SerializeMap, SerializeSeq, Serializer}; -use serde_json; -use std::env; -use std::fs; -use std::path::PathBuf; - -pub mod caret_color; -pub mod code_highlighting_color; -pub mod config_creator; -pub mod diff_color; -pub mod predef; -pub mod serde_color; -pub mod theme; -pub mod theme_config; - -pub use crate::themes::caret_color::CaretColor; -pub use crate::themes::code_highlighting_color::CodeHighlightingColor; -pub use crate::themes::diff_color::DiffColor; -pub use crate::themes::serde_color::SerdeColor; -pub use crate::themes::theme::Theme; -pub use crate::themes::theme_config::ThemeConfig; diff --git a/src/themes/serde_color.rs b/src/themes/serde_color.rs deleted file mode 100644 index c269de1..0000000 --- a/src/themes/serde_color.rs +++ /dev/null @@ -1,26 +0,0 @@ -use sdl2::pixels::Color; - -#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] -pub struct SerdeColor { - pub r: u8, - pub g: u8, - pub b: u8, - pub a: u8, -} - -impl SerdeColor { - pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self { - Self { r, g, b, a } - } -} - -impl Into for &SerdeColor { - fn into(self) -> Color { - Color { - r: self.r, - g: self.g, - b: self.b, - a: self.a, - } - } -} diff --git a/src/themes/theme.rs b/src/themes/theme.rs deleted file mode 100644 index 496c529..0000000 --- a/src/themes/theme.rs +++ /dev/null @@ -1,91 +0,0 @@ -use crate::config::directories::themes_dir; -use crate::themes::CaretColor; -use crate::themes::CodeHighlightingColor; -use crate::themes::DiffColor; -use crate::themes::SerdeColor; -use dirs; -use std::fs; - -#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] -pub struct Theme { - name: String, - background: SerdeColor, - caret: CaretColor, - code_highlighting: CodeHighlightingColor, - diff: DiffColor, -} - -impl Default for Theme { - fn default() -> Self { - use crate::themes::config_creator; - Self { - name: "default".to_string(), - background: SerdeColor::new(255, 255, 255, 0), - caret: CaretColor::default(), - code_highlighting: CodeHighlightingColor::default(), - diff: DiffColor::default(), - } - } -} - -impl Theme { - pub fn new( - name: String, - background: SerdeColor, - caret: CaretColor, - code_highlighting: CodeHighlightingColor, - diff: DiffColor, - ) -> Self { - Self { - name, - background, - caret, - code_highlighting, - diff, - } - } - - pub fn name(&self) -> &String { - &self.name - } - - pub fn background(&self) -> &SerdeColor { - &self.background - } - - pub fn caret(&self) -> &CaretColor { - &self.caret - } - - pub fn diff(&self) -> &DiffColor { - &self.diff - } - - pub fn code_highlighting(&self) -> &CodeHighlightingColor { - &self.code_highlighting - } - - pub fn load(theme_name: String) -> Self { - let home_dir = dirs::config_dir().unwrap(); - let mut config_dir = home_dir.clone(); - config_dir.push("rider"); - fs::create_dir_all(&config_dir) - .unwrap_or_else(|_| panic!("Cannot create config directory")); - Self::load_content(format!("{}.json", theme_name).as_str()) - } - - fn load_content(file_name: &str) -> Theme { - let mut config_file = themes_dir(); - config_file.push(file_name); - let contents = match fs::read_to_string(&config_file) { - Ok(s) => s, - Err(_) => { - use crate::themes::config_creator; - config_creator::create(); - fs::read_to_string(&config_file) - .unwrap_or_else(|_| panic!("Failed to load theme config file")) - } - }; - serde_json::from_str(&contents).unwrap_or_default() - } -} diff --git a/src/themes/theme_config.rs b/src/themes/theme_config.rs deleted file mode 100644 index 66e542a..0000000 --- a/src/themes/theme_config.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::themes::SerdeColor; - -#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] -pub struct ThemeConfig { - color: SerdeColor, - italic: bool, - bold: bool, -} - -impl ThemeConfig { - pub fn new(color: SerdeColor, italic: bool, bold: bool) -> Self { - Self { - color, - italic, - bold, - } - } - - pub fn color(&self) -> &SerdeColor { - &self.color - } - - pub fn italic(&self) -> bool { - self.italic - } - - pub fn bold(&self) -> bool { - self.bold - } -} diff --git a/assets/examples/example.txt b/test_files/example.txt similarity index 100% rename from assets/examples/example.txt rename to test_files/example.txt diff --git a/assets/examples/test.rs b/test_files/test.rs similarity index 100% rename from assets/examples/test.rs rename to test_files/test.rs