From 9c82168fbd0f14c424b11d7204f9e3c5c08f8f16 Mon Sep 17 00:00:00 2001 From: Pavel Sobolev Date: Sun, 11 Jul 2021 17:06:02 +0300 Subject: [PATCH 01/16] Add a blank-window application; add metadata. --- .cargo/config.toml | 2 + .github/workflows/debug-build.yml | 71 +++++++++++++ .gitignore | 7 ++ Cargo.lock | 166 ++++++++++++++++++++++++++++++ Cargo.toml | 33 ++++++ assets/shuchu-32.png | Bin 0 -> 1534 bytes assets/shuchu-64.png | Bin 0 -> 3215 bytes assets/shuchu.desktop | 6 ++ assets/shuchu.svg | 13 +++ src/channels.rs | 52 ++++++++++ src/events.rs | 17 +++ src/main.rs | 27 +++++ src/ui/app.rs | 31 ++++++ src/ui/mod.rs | 2 + src/ui/windows/icon.rs | 6 ++ src/ui/windows/main.rs | 28 +++++ src/ui/windows/mod.rs | 6 ++ 17 files changed, 467 insertions(+) create mode 100644 .cargo/config.toml create mode 100644 .github/workflows/debug-build.yml create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 assets/shuchu-32.png create mode 100644 assets/shuchu-64.png create mode 100644 assets/shuchu.desktop create mode 100644 assets/shuchu.svg create mode 100644 src/channels.rs create mode 100644 src/events.rs create mode 100644 src/main.rs create mode 100644 src/ui/app.rs create mode 100644 src/ui/mod.rs create mode 100644 src/ui/windows/icon.rs create mode 100644 src/ui/windows/main.rs create mode 100644 src/ui/windows/mod.rs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..d38acd0 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[alias] +lint = ["clippy", "--", "-D", "clippy::pedantic", "-D", "clippy::cargo", "-A", "clippy::option_if_let_else"] diff --git a/.github/workflows/debug-build.yml b/.github/workflows/debug-build.yml new file mode 100644 index 0000000..1db097c --- /dev/null +++ b/.github/workflows/debug-build.yml @@ -0,0 +1,71 @@ +name: Debug Build + +on: + pull_request: + branches: main + +defaults: + run: + shell: bash + +jobs: + build: + runs-on: ${{ matrix.os }} + name: ${{ ( startsWith(matrix.os, 'ubuntu') && 'Linux' ) || + ( startsWith(matrix.os, 'windows') && 'Windows' ) || + ( startsWith(matrix.os, 'mac') && 'macOS' ) }} + strategy: + matrix: + # os: [windows-latest, macos-10.15, ubuntu-18.04] + os: [windows-latest] + + steps: + - name: Download dependencies (Linux only) + if: ${{ startsWith(matrix.os, 'ubuntu') }} + run: | + sudo apt-get update + sudo apt-get install -y libpango1.0-dev libx11-dev libxext-dev \ + libxft-dev libxinerama-dev libxcursor-dev \ + libxrender-dev libxfixes-dev ninja-build \ + appstream + sudo wget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage -O /usr/local/bin/linuxdeploy + sudo chmod +x /usr/local/bin/linuxdeploy + - name: Checkout the repository + uses: actions/checkout@v2 + - name: Build + run: | + cargo lint + cargo build + cargo build --release + if ${{ startsWith(matrix.os, 'windows') }}; then + mv target/debug/shuchu.exe shuchu-debug.exe + mv target/release/shuchu.exe . + else + mv target/debug/shuchu shuchu-debug + mv target/release/shuchu . + strip shuchu shuchu-debug + fi + - name: Create an AppImage (Linux only) + if: ${{ startsWith(matrix.os, 'ubuntu') }} + run: | + linuxdeploy --appdir AppDir -e shuchu -d assets/shuchu.desktop -i assets/shuchu-64.png -o appimage + mv Shuchu*.AppImage Shuchu.AppImage + - name: Create an App Bundle (macOS only) + if: ${{ startsWith(matrix.os, 'mac') }} + run: | + cargo install cargo-bundle + cargo bundle + mv target/debug/bundle/osx/Shuchu.app . + - name: Upload the binaries + uses: actions/upload-artifact@v2 + with: + name: ${{ ( startsWith(matrix.os, 'ubuntu') && 'Linux' ) || + ( startsWith(matrix.os, 'windows') && 'Windows' ) || + ( startsWith(matrix.os, 'macOS') && 'macOS' ) }} + path: | + shuchu-debug + shuchu-debug.exe + shuchu + shuchu.exe + Shuchu.AppImage + Shuchu.app diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4dfbfeb --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# Rust +/target + +# AppImage +/AppDir +/*.AppImage +/appimage-builder-cache diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..ab7e490 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,166 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "cc" +version = "1.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a72c244c1ff497a746a7e1fb3d14bd08420ecda70c8f25c7112f2781652d787" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cmake" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb6210b637171dfba4cda12e579ac6dc73f5165ad56133e5d72ef3131f320855" +dependencies = [ + "cc", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" +dependencies = [ + "cfg-if", + "lazy_static", +] + +[[package]] +name = "fltk" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fd9596a1677232504499345ad56fbad899093cd281eaf371d41941c638dc753" +dependencies = [ + "bitflags", + "fltk-derive", + "fltk-sys", + "lazy_static", + "objc", + "raw-window-handle", +] + +[[package]] +name = "fltk-derive" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7272e4cb6beabf0a37a8070f1700795d99fc3612dd7292842fa171e2f67d8b76" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "fltk-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4d8037f9cea0c3ce53fe91c9b73950b559ae58c987647127450bae539014ca0" +dependencies = [ + "cmake", + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "proc-macro2" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "raw-window-handle" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a441a7a6c80ad6473bd4b74ec1c9a4c951794285bf941c2126f607c72e48211" +dependencies = [ + "libc", +] + +[[package]] +name = "shuchu" +version = "0.1.0" +dependencies = [ + "crossbeam-channel", + "fltk", +] + +[[package]] +name = "syn" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ba3362f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "shuchu" +version = "0.1.0" +authors = ["paveloom"] +edition = "2018" +description = "Whoa! What's that?!" +documentation = "https://docs.rs/shuchu" +readme = "README.md" +homepage = "https://paveloom-a.github.io/Shuchu" +repository = "https://github.com/paveloom-a/Shuchu" +license-file = "LICENSE.md" +keywords = ["productivity", "focus", "timer", "rewards", "life"] +categories = ["graphics"] + +[package.metadata.bundle] +name = "Shuchu" +identifier = "io.github.paveloom.shuchu" +icon = ["assets/shuchu-64.png"] +resources = ["assets"] +copyright = "Copyright (c) Pavel Sobolev 2021." +category = "public.app-category.productivity" + +[profile.dev] +panic = 'abort' + +[profile.release] +lto = true +codegen-units = 1 +panic = 'abort' + +[dependencies] +fltk = { version = "~1.1.0", features = ["fltk-bundled", "use-ninja"] } +crossbeam-channel = {version = "~0.5.1"} diff --git a/assets/shuchu-32.png b/assets/shuchu-32.png new file mode 100644 index 0000000000000000000000000000000000000000..3353d6271ebaf48e70265a0d36c0402d341a9dcc GIT binary patch literal 1534 zcmV;$x9$Bx*|n>Tw( zN=lXh8~F5n0sI!wwQJYzO474u&xX8SZw~kd*z`UC{s3rVVq#E^E?v6RLw-9hy$^st z0-Bnd3cCQdZ6`vZP#RbOK3J<^&no9l0!&U$rri$1Fv^b{IWh`_fSlTeoiY zI%6$sb?7bl{WIW8;FF4qiiMLWPkwdg%$bQ}$BxBz?%escX`05&%uL0rSFfI|U%x&P zjYfZHnr1c@i+u?E1sGl{gsVRR+P{B))VA%EmdUp57d16C(||T?*zlVFjZQak6ZitS z02~9pS1iDffQ;Yo&pvlF{BP1IyCG$*}8S>WLsNX@7UPb;Op0~AI{CqMI({Oy_S}i{_^s27c}G8UA2yo zKjBnTQerhWHa@s;;X-eFd;8tJd-pyDR8>{=Vq|3GzNFmSBIRt^d`Cw|kI(0`9FX4? z15pY59++cUXm{`4{V&V1W+ln8teGoUu5{Jc*GDTWD`z%t+!$+WY8oCG7`Q8GQIg{E zc(lB{eBN1KR3so@CA-OIx$5fbrgOR63rQLp8oC<_g;L7_-L_@Rme~CK`~wGK#EC?j zHQXH`U{7>4nk2Z*RW?U>HUTKqiwZaqJk*)Y5NlufqS#!otF4NvS{} zP-z&39S(XaK! zRaRCe8XFrQ?Ay2RvGY)s&*#f_b#>jAB+Ig9tE#HpH^fiZ0 zI-PzZDed;h-;B zK~#90-CJ!;R96~)?qwKZ7(f`6!uZW>QH-qhtJ2l@)znq8YlB)Fce{;g_Q$I0N6r2) zZK`n-W7f1kx`{MhjqPTYWH-$QjoOqcAUFb!Gl+nK3QCbr0hv3@2gAMh?2qe^a}P5x zgEP?VlbnQe?z!i@&pYRy^Pcy80iuK10IC5D0w@HqHcIpSEWrWz5ddMze*w&&Icm+*KzPv^{;^8#umJOU6u zb>c=AGeMEd&d%;644h8q!-RwcH`GcvHH9*RkKkDI>B_ow>)P24dcFP^-+%x8J*N2} zfLRfgBP>Kx0cmt4D=W(#sMFfDYgfPh_S=nAQv!fLM^KKi5J?57#?orFnjrnSZ@>N4 z%e3tU5EDUJLPjJN;G-*oAjrWwW@Kb6Te4(HC)H%c6rBi>R6qw^X=rFj7_0MVpMCa- zX<8RSS*8R@P^>%xfXtmc*TJ3wH2V7b>YzRiE>EHC5kmtgQeLmOl_o7o(sM${AR&av zvh0Fd0S!|ja5$ep9Dv^dI0K*rz(XjPn|=U20BWJw0wX9cE^hVzeeT@35{*WaI(_hm^p4v zKGne34~Bexetx|m2oJ|P)@VF^`m_SdoquGEPd@piQlrrfvtv``I)H5eJ^*kCz&a_($b!vJ$trH5QGLoh&0}6k|gQ5&1Ng#wryLm&q@Mt z0%QNE6TF?I^}5c^ z&XmN&MAy7|^PIW4xx?GHZ;x59V8Jh;T)9;cMe*6%wQJqg)zz;vk4^$O9*&%$A`yzQ zRE-dRb*)xA)Y{rw9pDG_*=)9wrAwEN7fB)jNPK*}aPs8IQlHP)!VVMATO{E;vluq?}t zH{X1dHLpAI3P)tPmvT9RpY;3eJ{d|~&u)GPB-+2$8^ z*RNL)=4KfP4u_*@^XAR>StaD<<&CmOcJAECTAu~cR={8B3HR^ce~T7K6vg)BVtd+27(rV~-PCjh`dX~8=>I#dKRGc#3&j??LEjD!LRA!Nsn9jayC($b<@H7i%H zWR0v*uJ=fU#+J~PS+i!%@&7G~VtaFQv&mo2&d%=8>-AX?m=geS^ypEQC%LGoNEMIV zuwg?F^UA2XBcXs){~z)3@t&BNn2|7fe}8`)-7!BuzZDV0vSrIuc?Fe~m9cbv`SRt% z%qydOi@`$J;b7!qG=2K?K~O0#y}i9dbjR%MY$?=1T`m_Pg!m&zJjZdPE<$l}ap`pZ z$&)9lJ~L*_U|&gO8zQj+MhH)vHch2*uUDN0kdTlNG-S@=-FM&J!1Mg{mnCWY_U$$> z!4r<-v~=C$@u>2R;^N}iO;5|zBcgy2p#(ut36PSKq7tCJy*=oPdui$h#pTPFSJc(j z+2}S&k{on>=FFM>bbVl8fE{y?9XApRctK5=3?==|FHj2I(PFX01sfzYGc)KjKT=au zr_uGkzCP6+vvlcF)sky%ZDm)$3#J~CJ)jvcFB`Hf^B#}qr}+5z*8xDU*Dus+wH}|( z7Y_hMMMX;pAs&w7My1B3r>C#Jdi84Q7hila!{_s<e2y-c z>mK!7G8hc>)mQ|ei-rPr(?T3NbVx_rbUK|&mSra;i)YT9IjYOjsZ*z_UWL5d?S8D&=~NjeckkXc(|rmG z3RH?mloK*lx4m1pZmCRCyWKv@5pw0qm5Paw9~v5Zws7IXPI{~rD^}DKwNJnmAUAng zG!^h6J(=}HpD=g`&Fb=PP#szP?Lv9WGJ5a@58n9b&L zX0F<3k_Qx*`7?UbYuB!o(n86y+`V}5;>WBKOeRy8)oQiKvK&+qPN%c!(4j-N0B$ka zX0sL3eI-fiH5d%+um3@`6#yvdt|OBsBqR)az20V8IG@kgx@gg&fNpTTUhmzrXOI2t z*|RsVT)9$SSXg-T(@#IWm7Sg45vV`Mabj_Cv6(P9a^y%Q)Au1FbKH-MSOBWrzg4SN zHOaC($gYIlyLYpW%dkpvaJtDyS@4tVCsP_Xx z2$5=QYl|~7GE}F-c!lFQ>G0vhH$+ivXUVs=wpMF28aAP;a5BmJaf}uGz4zX$VOPRS zxb1d(+0LCiD`I0~*~H0_l9`#=_|;cm{owU_1E%Rbdi2Pm)oR%Q?qf`1P=p2#;37+6 z_3G6R**$^5pwsEBYiVgIw_2@b_wU~??e6Zb5=D`9p3)F6Uc6Yw^Ss2&*AK=~GuhAr zxXK7c5)u*yuU@@cM#jl22nB*5JlL>d!>Bj`l!VS-O%ZPuv`{>;qn5PEWO`sv|6ndZnyg(2@oL?MX|HJ zz5UMl^XG5mD`R##{5cv#>Icoz@3gu!GvU7Dj{113q%NwNL`(gk9002ovPDHLkV1jb! BAvpj5 literal 0 HcmV?d00001 diff --git a/assets/shuchu.desktop b/assets/shuchu.desktop new file mode 100644 index 0000000..238bf61 --- /dev/null +++ b/assets/shuchu.desktop @@ -0,0 +1,6 @@ +[Desktop Entry] +Type=Application +Name=Shuchu +Exec=shuchu +Icon=shuchu-64 +Categories=Utility; diff --git a/assets/shuchu.svg b/assets/shuchu.svg new file mode 100644 index 0000000..e304526 --- /dev/null +++ b/assets/shuchu.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/channels.rs b/src/channels.rs new file mode 100644 index 0000000..c13eac2 --- /dev/null +++ b/src/channels.rs @@ -0,0 +1,52 @@ +use crossbeam_channel::{self, Receiver, Sender}; + +macro_rules! channels_default_impl { + ( + $(#[$attr:meta])* + pub struct $name:ident { + $( + #[$field_doc:meta] + pub $field_name:ident: $field_type:ty, + )* + }) => { + $(#[$attr])* + pub struct $name { + $( + #[$field_doc] + pub $field_name: $field_type, + )* + } + + impl $name { + /// Get the default set of the application's channels + pub fn default() -> $name { + $name { + $( + $field_name: Channel::new(crossbeam_channel::unbounded()), + )* + } + } + } + } +} + +#[derive(Clone)] +pub struct Channel { + pub s: Sender, + pub r: Receiver, +} + +impl Channel { + fn new((s, r): (Sender, Receiver)) -> Channel { + Channel { s, r } + } +} + +channels_default_impl! { + /// A struct providing access to the application's channels + #[derive(Clone)] + pub struct Channels { + /// Channel 1: Nothing Yet + pub nothing: Channel, + } +} diff --git a/src/events.rs b/src/events.rs new file mode 100644 index 0000000..5e8b584 --- /dev/null +++ b/src/events.rs @@ -0,0 +1,17 @@ +// /// Consume the first argument in exchange of unit +// macro_rules! unit { +// ($_t:tt $unit:expr) => { +// $unit +// }; +// } + +// /// Create events using provided identifiers +// macro_rules! events { +// ( $first_event:ident $(,)? $($other_events:ident),* ) => { +// pub const $first_event: i32 = 1000 + <[()]>::len(&[$(unit!(($other_events) ())),*]) as i32; +// events!($($other_events),*); +// }; +// () => {}; +// } + +// events!(); diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..18b30a6 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,27 @@ +// Switch from the console subsystem to the windows subsystem in the release mode +#![cfg_attr(all(windows, not(debug_assertions)), windows_subsystem = "windows")] + +mod channels; +mod events; +mod ui; + +use fltk::app; + +fn main() { + // Application channels + let channels = channels::Channels::default(); + + // Override the system's screen scaling + for i in 0..app::screen_count() { + app::set_screen_scale(i, 1.0); + } + + // 0. App + let app = ui::app::new(); + + // 1. Window + let _window = ui::windows::main(&channels); + + // Start the event loop + while app.wait() {} +} diff --git a/src/ui/app.rs b/src/ui/app.rs new file mode 100644 index 0000000..ad45943 --- /dev/null +++ b/src/ui/app.rs @@ -0,0 +1,31 @@ +use fltk::{ + app::{self, App}, + enums::FrameType, +}; + +/// A struct providing access to the application's constants +pub struct Constants { + pub window_min_width: i32, + pub window_min_height: i32, +} + +impl Constants { + /// Get the default set of the application's constants + const fn default() -> Constants { + Constants { + window_min_width: 1000, + window_min_height: 600, + } + } +} + +/// Default set of the application's constants +pub const CONSTANTS: Constants = Constants::default(); + +/// Create a new App +pub fn new() -> App { + let app = App::default(); + app::background(255, 255, 255); + app::set_frame_type(FrameType::BorderBox); + app +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs new file mode 100644 index 0000000..68ee9ec --- /dev/null +++ b/src/ui/mod.rs @@ -0,0 +1,2 @@ +pub mod app; +pub mod windows; diff --git a/src/ui/windows/icon.rs b/src/ui/windows/icon.rs new file mode 100644 index 0000000..22046dc --- /dev/null +++ b/src/ui/windows/icon.rs @@ -0,0 +1,6 @@ +use fltk::image::PngImage; + +/// Load a Window Icon +pub fn icon() -> PngImage { + PngImage::from_data(include_bytes!("../../../assets/shuchu-32.png")).unwrap() +} diff --git a/src/ui/windows/main.rs b/src/ui/windows/main.rs new file mode 100644 index 0000000..d088f1e --- /dev/null +++ b/src/ui/windows/main.rs @@ -0,0 +1,28 @@ +use fltk::{prelude::*, window::Window}; + +use super::icon; +use crate::channels::Channels; +use crate::ui::app::CONSTANTS; + +/// Create the Main Window +pub fn main(_channels: &Channels) -> Window { + let mut window = Window::new( + 100, + 100, + CONSTANTS.window_min_width, + CONSTANTS.window_min_height, + "Shuchu", + ); + window.set_icon(Some(icon())); + window.size_range( + CONSTANTS.window_min_width, + CONSTANTS.window_min_height, + 0, + 0, + ); + window.make_resizable(true); + + window.end(); + window.show(); + window +} diff --git a/src/ui/windows/mod.rs b/src/ui/windows/mod.rs new file mode 100644 index 0000000..3505d8f --- /dev/null +++ b/src/ui/windows/mod.rs @@ -0,0 +1,6 @@ +mod icon; +mod main; + +pub use main::main; + +use icon::icon; From c2dfff371b2501d952d36e1c5aea5dcb0624c06f Mon Sep 17 00:00:00 2001 From: Pavel Sobolev Date: Tue, 13 Jul 2021 20:01:06 +0300 Subject: [PATCH 02/16] Sketch a layout; add buttons' `handle`s, icons, a working timer. --- assets/menu/arrow.svg | 3 + assets/menu/coins.svg | 107 +++++++++++++++++++++++++++++ assets/menu/minus.svg | 4 ++ assets/menu/plus.svg | 3 + assets/menu/sand-clock.svg | 16 +++++ src/events.rs | 30 ++++---- src/ui/app.rs | 12 ++-- src/ui/focus/arrow.rs | 40 +++++++++++ src/ui/focus/coins.rs | 66 ++++++++++++++++++ src/ui/focus/mod.rs | 10 +++ src/ui/focus/pane.rs | 22 ++++++ src/ui/focus/timer.rs | 137 +++++++++++++++++++++++++++++++++++++ src/ui/logic.rs | 56 +++++++++++++++ src/ui/mod.rs | 4 ++ src/ui/rewards/list.rs | 12 ++++ src/ui/rewards/menubar.rs | 56 +++++++++++++++ src/ui/rewards/mod.rs | 8 +++ src/ui/rewards/pane.rs | 22 ++++++ src/ui/windows/main.rs | 19 ++--- 19 files changed, 599 insertions(+), 28 deletions(-) create mode 100644 assets/menu/arrow.svg create mode 100644 assets/menu/coins.svg create mode 100644 assets/menu/minus.svg create mode 100644 assets/menu/plus.svg create mode 100644 assets/menu/sand-clock.svg create mode 100644 src/ui/focus/arrow.rs create mode 100644 src/ui/focus/coins.rs create mode 100644 src/ui/focus/mod.rs create mode 100644 src/ui/focus/pane.rs create mode 100644 src/ui/focus/timer.rs create mode 100644 src/ui/logic.rs create mode 100644 src/ui/rewards/list.rs create mode 100644 src/ui/rewards/menubar.rs create mode 100644 src/ui/rewards/mod.rs create mode 100644 src/ui/rewards/pane.rs diff --git a/assets/menu/arrow.svg b/assets/menu/arrow.svg new file mode 100644 index 0000000..92db234 --- /dev/null +++ b/assets/menu/arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/menu/coins.svg b/assets/menu/coins.svg new file mode 100644 index 0000000..a913357 --- /dev/null +++ b/assets/menu/coins.svg @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/menu/minus.svg b/assets/menu/minus.svg new file mode 100644 index 0000000..50be9f5 --- /dev/null +++ b/assets/menu/minus.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/menu/plus.svg b/assets/menu/plus.svg new file mode 100644 index 0000000..4f80048 --- /dev/null +++ b/assets/menu/plus.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/menu/sand-clock.svg b/assets/menu/sand-clock.svg new file mode 100644 index 0000000..24e1914 --- /dev/null +++ b/assets/menu/sand-clock.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/events.rs b/src/events.rs index 5e8b584..073fa88 100644 --- a/src/events.rs +++ b/src/events.rs @@ -1,17 +1,17 @@ -// /// Consume the first argument in exchange of unit -// macro_rules! unit { -// ($_t:tt $unit:expr) => { -// $unit -// }; -// } +/// Consume the first argument in exchange of unit +macro_rules! unit { + ($_t:tt $unit:expr) => { + $unit + }; +} -// /// Create events using provided identifiers -// macro_rules! events { -// ( $first_event:ident $(,)? $($other_events:ident),* ) => { -// pub const $first_event: i32 = 1000 + <[()]>::len(&[$(unit!(($other_events) ())),*]) as i32; -// events!($($other_events),*); -// }; -// () => {}; -// } +/// Create events using provided identifiers +macro_rules! events { + ( $first_event:ident $(,)? $($other_events:ident),* ) => { + pub const $first_event: i32 = 1000 + <[()]>::len(&[$(unit!(($other_events) ())),*]) as i32; + events!($($other_events),*); + }; + () => {}; +} -// events!(); +events!(START_TIMER, TICK, STOP_TIMER); diff --git a/src/ui/app.rs b/src/ui/app.rs index ad45943..7bb43e3 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -5,16 +5,20 @@ use fltk::{ /// A struct providing access to the application's constants pub struct Constants { - pub window_min_width: i32, - pub window_min_height: i32, + pub window_width: i32, + pub window_height: i32, + pub focus_pane_height: i32, + pub rewards_menubar_height: i32, } impl Constants { /// Get the default set of the application's constants const fn default() -> Constants { Constants { - window_min_width: 1000, - window_min_height: 600, + window_width: 340, + window_height: 400, + focus_pane_height: 60, + rewards_menubar_height: 30, } } } diff --git a/src/ui/focus/arrow.rs b/src/ui/focus/arrow.rs new file mode 100644 index 0000000..beb2793 --- /dev/null +++ b/src/ui/focus/arrow.rs @@ -0,0 +1,40 @@ +use fltk::{ + button::Button, + enums::{FrameType, Shortcut}, + image::SvgImage, + prelude::*, +}; + +use crate::ui::logic; + +pub fn arrow() -> Button { + let mut arrow = Button::default(); + arrow.set_frame(FrameType::FlatBox); + + draw_frame(&mut arrow); + arrow.draw(draw_frame); + + let mut arrow_image = + SvgImage::from_data(include_str!("../../../assets/menu/arrow.svg")).unwrap(); + arrow_image.scale(48, 40, true, true); + arrow.set_image(Some(arrow_image)); + + arrow.set_shortcut(Shortcut::from_char('r')); + arrow.set_callback(|_| println!("Rate pressed!")); + arrow.handle(logic::button_handle); + + arrow +} + +fn draw_frame(f: &mut T) { + if let Some(p) = f.parent() { + if let Some(lw) = p.child(0) { + if let Some(rw) = p.child(1) { + let lx = lw.x() + lw.w(); + let w = 48 + 15; + f.set_pos(lx + (rw.x() - lx - w) / 2, p.y() + 15); + f.set_size(w, 30); + } + } + } +} diff --git a/src/ui/focus/coins.rs b/src/ui/focus/coins.rs new file mode 100644 index 0000000..5040326 --- /dev/null +++ b/src/ui/focus/coins.rs @@ -0,0 +1,66 @@ +use fltk::{ + button::Button, + draw, + enums::{Align, Color, FrameType, LabelType, Shortcut}, + image::SvgImage, + prelude::*, +}; + +use crate::ui::logic; + +pub fn coins() -> Button { + let mut coins = Button::default().with_label("99999"); + coins.set_label_type(LabelType::None); + coins.set_frame(FrameType::FlatBox); + + draw_button(&mut coins); + coins.draw(draw); + + coins.set_shortcut(Shortcut::from_char('c')); + coins.set_callback(|_| { + println!("Coins pressed!"); + }); + coins.handle(logic::button_handle); + + coins +} + +fn draw(b: &mut T) { + draw::push_clip(b.x(), b.y(), b.w(), b.h()); + draw_button(b); + draw_label(b); + draw_image(b); + draw::pop_clip(); +} + +fn draw_button(b: &mut T) { + let (lw, _) = b.measure_label(); + if let Some(p) = b.parent() { + b.set_pos(p.x() + p.w() - lw - 60, p.y() + 10); + b.set_size(lw + 50, p.h() - 20); + } +} + +fn draw_label(b: &mut T) { + let color = draw::get_color(); + + draw::set_font(draw::font(), 16); + draw::set_draw_color(Color::Black); + draw::draw_text2( + &b.label(), + b.x() + 30, + b.y(), + b.w() - 40, + b.h(), + Align::Right, + ); + + draw::set_draw_color(color); +} + +fn draw_image(b: &mut T) { + let mut coins_image = + SvgImage::from_data(include_str!("../../../assets/menu/coins.svg")).unwrap(); + coins_image.scale(24, 24, true, true); + coins_image.draw(b.x() + 6, b.y() + 7, 24, 24); +} diff --git a/src/ui/focus/mod.rs b/src/ui/focus/mod.rs new file mode 100644 index 0000000..5c9d2e9 --- /dev/null +++ b/src/ui/focus/mod.rs @@ -0,0 +1,10 @@ +mod arrow; +mod coins; +mod pane; +mod timer; + +pub use pane::pane; + +use arrow::arrow; +use coins::coins; +use timer::timer; diff --git a/src/ui/focus/pane.rs b/src/ui/focus/pane.rs new file mode 100644 index 0000000..7c2b8ee --- /dev/null +++ b/src/ui/focus/pane.rs @@ -0,0 +1,22 @@ +use fltk::{enums::FrameType, group::Group, prelude::*}; + +use super::{arrow, coins, timer}; +use crate::ui::app::CONSTANTS; + +pub fn pane() -> Group { + let mut pane = Group::default().with_pos(10, 10); + pane.set_frame(FrameType::BorderBox); + + if let Some(parent) = pane.parent() { + pane.set_size(parent.width() - 20, CONSTANTS.focus_pane_height); + } + + // The order of the widgets is important + + let _timer = timer(); + let _coins = coins(); + let _arrow = arrow(); + + pane.end(); + pane +} diff --git a/src/ui/focus/timer.rs b/src/ui/focus/timer.rs new file mode 100644 index 0000000..b2df89b --- /dev/null +++ b/src/ui/focus/timer.rs @@ -0,0 +1,137 @@ +use fltk::{ + app, + button::Button, + draw, + enums::{Align, Color, FrameType, LabelType, Shortcut}, + image::SvgImage, + prelude::*, +}; +use std::sync::atomic::{AtomicBool, Ordering}; + +static REPEAT_TICK: AtomicBool = AtomicBool::new(false); + +use crate::events; +use crate::ui::logic; + +pub fn timer() -> Button { + let mut timer = Button::default().with_label("00:00:00"); + timer.set_label_type(LabelType::None); + timer.set_frame(FrameType::FlatBox); + + draw_button(&mut timer); + timer.draw(draw); + + logic(&mut timer); + + timer +} + +fn draw(b: &mut T) { + draw::push_clip(b.x(), b.y(), b.w(), b.h()); + draw_button(b); + draw_label(b); + draw_image(b); + draw::pop_clip(); +} + +fn draw_button(b: &mut T) { + if let Some(p) = b.parent() { + b.set_pos(p.x() + 10, p.y() + 10); + b.set_size(110, p.h() - 20); + } +} + +fn draw_label(b: &mut T) { + let color = draw::get_color(); + + draw::set_font(draw::font(), 16); + draw::set_draw_color(Color::Black); + draw::draw_text2( + &b.label(), + b.x() + 32, + b.y(), + b.w() - 40, + b.h(), + Align::Right, + ); + + draw::set_draw_color(color); +} + +fn draw_image(b: &mut T) { + let mut coins_image = + SvgImage::from_data(include_str!("../../../assets/menu/sand-clock.svg")).unwrap(); + coins_image.scale(24, 24, true, true); + coins_image.draw(b.x() + 6, b.y() + 7, 24, 24); +} + +fn logic(timer: &mut T) { + timer.set_shortcut(Shortcut::from_char('f')); + timer.set_callback(start); + timer.handle({ + let mut counting = false; + let mut seconds = 0; + move |t, ev| match ev.bits() { + events::START_TIMER => { + if !counting { + counting = true; + REPEAT_TICK.store(true, Ordering::Release); + app::add_timeout(1.0, tick); + app::remove_timeout(tick); + } + true + } + events::TICK => { + seconds += 1; + let hours = seconds / 3600; + let minutes = seconds / 60 % 60; + let seconds = seconds % 60; + t.set_label(&format!( + "{}:{}:{}", + if hours > 9 { + format!("{}", hours) + } else { + format!("0{}", hours) + }, + if minutes > 9 { + format!("{}", minutes) + } else { + format!("0{}", minutes) + }, + if seconds > 9 { + format!("{}", seconds) + } else { + format!("0{}", seconds) + } + )); + true + } + events::STOP_TIMER => { + counting = false; + seconds = 0; + REPEAT_TICK.store(false, Ordering::Release); + app::remove_timeout(tick); + t.set_label("00:00:00"); + true + } + _ => logic::button_handle(t, ev), + } + }); +} + +fn start(b: &mut T) { + app::handle_main(events::START_TIMER).ok(); + b.set_callback(stop); +} + +fn stop(b: &mut T) { + app::handle_main(events::STOP_TIMER).ok(); + b.set_callback(start); +} + +fn tick() { + if REPEAT_TICK.load(Ordering::Acquire) { + app::handle_main(events::TICK).ok(); + app::repeat_timeout(1.0, tick); + } +} diff --git a/src/ui/logic.rs b/src/ui/logic.rs new file mode 100644 index 0000000..c333d27 --- /dev/null +++ b/src/ui/logic.rs @@ -0,0 +1,56 @@ +use fltk::{ + app, + enums::{Color, Event, FrameType, Key}, + prelude::*, +}; + +pub fn button_handle(b: &mut T, ev: Event) -> bool { + match ev { + Event::Focus => { + if app::event_key_down(Key::Enter) { + select(b); + } + true + } + Event::Enter => { + select(b); + true + } + Event::Leave | Event::Unfocus => { + unselect(b); + true + } + Event::KeyDown => match app::event_key() { + Key::Enter => { + select(b); + true + } + _ => false, + }, + Event::KeyUp => match app::event_key() { + Key::Enter => { + b.do_callback(); + unselect(b); + true + } + _ => false, + }, + _ => false, + } +} + +fn select(b: &mut T) { + b.set_color(Color::from_hex(0xE5_F3_FF)); + b.set_frame(FrameType::BorderBox); + b.set_selection_color(Color::from_hex(0xE5_F3_FF)); + b.set_down_frame(FrameType::DownBox); + b.redraw(); +} + +fn unselect(b: &mut T) { + b.set_color(Color::BackGround); + b.set_frame(FrameType::FlatBox); + b.set_selection_color(Color::BackGround); + b.set_down_frame(FrameType::FlatBox); + b.redraw(); +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 68ee9ec..41a18d2 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,2 +1,6 @@ pub mod app; pub mod windows; + +mod focus; +mod logic; +mod rewards; diff --git a/src/ui/rewards/list.rs b/src/ui/rewards/list.rs new file mode 100644 index 0000000..cdc1ff4 --- /dev/null +++ b/src/ui/rewards/list.rs @@ -0,0 +1,12 @@ +use fltk::{enums::FrameType, prelude::*, table::TableRow}; + +pub fn list() -> TableRow { + let mut list = TableRow::default(); + list.set_frame(FrameType::BorderBox); + + if let Some(p) = list.parent() { + list.set_size(0, p.h() - 20); + } + + list +} diff --git a/src/ui/rewards/menubar.rs b/src/ui/rewards/menubar.rs new file mode 100644 index 0000000..c1324eb --- /dev/null +++ b/src/ui/rewards/menubar.rs @@ -0,0 +1,56 @@ +use fltk::{ + button::Button, + enums::{FrameType, Shortcut}, + group::{Pack, PackType}, + image::SvgImage, + prelude::*, +}; + +use crate::ui::app::CONSTANTS; +use crate::ui::logic; + +pub fn menubar() -> Pack { + let mut menubar = Pack::default().with_size(0, CONSTANTS.rewards_menubar_height); + menubar.set_type(PackType::Horizontal); + + let mut add_image = SvgImage::from_data(include_str!("../../../assets/menu/plus.svg")).unwrap(); + add_image.scale(24, 24, true, true); + + let mut add_button = Button::default().with_size(CONSTANTS.rewards_menubar_height, 0); + add_button.set_image(Some(add_image)); + add_button.set_frame(FrameType::FlatBox); + + let mut delete_image = + SvgImage::from_data(include_str!("../../../assets/menu/minus.svg")).unwrap(); + delete_image.scale(24, 24, true, true); + + let mut delete_button = Button::default().with_size(CONSTANTS.rewards_menubar_height, 0); + delete_button.set_image(Some(delete_image)); + delete_button.set_frame(FrameType::FlatBox); + + logic(&mut add_button, &mut delete_button); + + menubar.end(); + menubar +} + +fn logic( + add_button: &mut T, + delete_button: &mut U, +) { + add_button.set_shortcut(Shortcut::from_char('a')); + + add_button.set_callback(|_| { + println!("Add Pressed!"); + }); + + add_button.handle(logic::button_handle); + + delete_button.set_shortcut(Shortcut::from_char('d')); + + delete_button.set_callback(|_| { + println!("Delete Pressed!"); + }); + + delete_button.handle(logic::button_handle); +} diff --git a/src/ui/rewards/mod.rs b/src/ui/rewards/mod.rs new file mode 100644 index 0000000..b75e425 --- /dev/null +++ b/src/ui/rewards/mod.rs @@ -0,0 +1,8 @@ +mod list; +mod menubar; +mod pane; + +pub use pane::pane; + +use list::list; +use menubar::menubar; diff --git a/src/ui/rewards/pane.rs b/src/ui/rewards/pane.rs new file mode 100644 index 0000000..45ed39d --- /dev/null +++ b/src/ui/rewards/pane.rs @@ -0,0 +1,22 @@ +use fltk::{group::Pack, prelude::*}; + +use super::{list, menubar}; +use crate::ui::app::CONSTANTS; + +pub fn pane() -> Pack { + let mut pane = Pack::default().with_pos(10, 10 + CONSTANTS.focus_pane_height + 10); + pane.set_spacing(1); + + if let Some(parent) = pane.parent() { + pane.set_size( + parent.width() - 20, + parent.height() - 10 - 10 - CONSTANTS.focus_pane_height - 10, + ); + } + + let _menubar = menubar(); + let _rewards = list(); + + pane.end(); + pane +} diff --git a/src/ui/windows/main.rs b/src/ui/windows/main.rs index d088f1e..0474ee0 100644 --- a/src/ui/windows/main.rs +++ b/src/ui/windows/main.rs @@ -3,24 +3,25 @@ use fltk::{prelude::*, window::Window}; use super::icon; use crate::channels::Channels; use crate::ui::app::CONSTANTS; +use crate::ui::focus; +use crate::ui::rewards; /// Create the Main Window pub fn main(_channels: &Channels) -> Window { let mut window = Window::new( 100, 100, - CONSTANTS.window_min_width, - CONSTANTS.window_min_height, + CONSTANTS.window_width, + CONSTANTS.window_height, "Shuchu", ); window.set_icon(Some(icon())); - window.size_range( - CONSTANTS.window_min_width, - CONSTANTS.window_min_height, - 0, - 0, - ); - window.make_resizable(true); + + // 1. Focus Pane + let _focus_pane = focus::pane(); + + // 2. Rewards Pane + let _rewards_pane = rewards::pane(); window.end(); window.show(); From 852cbee1a56cbf821570568a60e2e6799ef7d66a Mon Sep 17 00:00:00 2001 From: Pavel Sobolev Date: Wed, 14 Jul 2021 21:12:31 +0300 Subject: [PATCH 03/16] Add the rates pane; resizing / switching between the panes; Left, Right, Tab handles. --- src/ui/focus/arrow.rs | 50 ++++++++++++++++++--- src/ui/focus/coins.rs | 48 +++++++++++++++++--- src/ui/focus/pane.rs | 36 ++++++++++++++- src/ui/focus/timer.rs | 17 ++++++-- src/ui/logic.rs | 59 ++++++++++++++++++++++++- src/ui/mod.rs | 1 + src/ui/rates/list.rs | 67 ++++++++++++++++++++++++++++ src/ui/rates/menubar.rs | 92 +++++++++++++++++++++++++++++++++++++++ src/ui/rates/mod.rs | 8 ++++ src/ui/rates/pane.rs | 23 ++++++++++ src/ui/rewards/list.rs | 63 +++++++++++++++++++++++++-- src/ui/rewards/menubar.rs | 66 +++++++++++++++++++++------- src/ui/rewards/pane.rs | 6 +-- src/ui/windows/main.rs | 12 +++++ 14 files changed, 506 insertions(+), 42 deletions(-) create mode 100644 src/ui/rates/list.rs create mode 100644 src/ui/rates/menubar.rs create mode 100644 src/ui/rates/mod.rs create mode 100644 src/ui/rates/pane.rs diff --git a/src/ui/focus/arrow.rs b/src/ui/focus/arrow.rs index beb2793..e0b2de9 100644 --- a/src/ui/focus/arrow.rs +++ b/src/ui/focus/arrow.rs @@ -1,10 +1,12 @@ use fltk::{ + app, button::Button, - enums::{FrameType, Shortcut}, + enums::{Event, FrameType, Key, Shortcut}, image::SvgImage, prelude::*, }; +use crate::ui::app::CONSTANTS; use crate::ui::logic; pub fn arrow() -> Button { @@ -19,17 +21,15 @@ pub fn arrow() -> Button { arrow_image.scale(48, 40, true, true); arrow.set_image(Some(arrow_image)); - arrow.set_shortcut(Shortcut::from_char('r')); - arrow.set_callback(|_| println!("Rate pressed!")); - arrow.handle(logic::button_handle); + logic(&mut arrow); arrow } fn draw_frame(f: &mut T) { - if let Some(p) = f.parent() { - if let Some(lw) = p.child(0) { - if let Some(rw) = p.child(1) { + if let Some(ref p) = f.parent() { + if let Some(ref lw) = p.child(0) { + if let Some(ref rw) = p.child(1) { let lx = lw.x() + lw.w(); let w = 48 + 15; f.set_pos(lx + (rw.x() - lx - w) / 2, p.y() + 15); @@ -38,3 +38,39 @@ fn draw_frame(f: &mut T) { } } } + +fn logic(a: &mut T) { + a.set_shortcut(Shortcut::from_char('r')); + a.set_callback(|a| { + if let Some(ref p) = a.parent() { + if let Some(ref mut w) = p.parent() { + if let Some(ref mut re_p) = w.child(1) { + if let Some(ref mut ra_p) = w.child(2) { + if re_p.visible() { + re_p.hide(); + ra_p.show(); + } else if ra_p.visible() { + w.resize(w.x(), w.y(), w.w(), 10 + CONSTANTS.focus_pane_height + 10); + ra_p.hide(); + } else { + w.resize(w.x(), w.y(), w.w(), CONSTANTS.window_height); + ra_p.show(); + } + } + } + } + } + }); + a.handle(|a, ev| { + if ev == Event::KeyDown { + match app::event_key() { + Key::Left => logic::fp_handle_left(a, 2), + Key::Right => logic::fp_handle_right(a, 2), + Key::Tab => logic::handle_tab(a), + _ => logic::handle_selection(a, ev), + } + } else { + logic::handle_selection(a, ev) + } + }); +} diff --git a/src/ui/focus/coins.rs b/src/ui/focus/coins.rs index 5040326..ef23021 100644 --- a/src/ui/focus/coins.rs +++ b/src/ui/focus/coins.rs @@ -1,11 +1,13 @@ use fltk::{ + app, button::Button, draw, - enums::{Align, Color, FrameType, LabelType, Shortcut}, + enums::{Align, Color, Event, FrameType, Key, LabelType, Shortcut}, image::SvgImage, prelude::*, }; +use crate::ui::app::CONSTANTS; use crate::ui::logic; pub fn coins() -> Button { @@ -16,11 +18,7 @@ pub fn coins() -> Button { draw_button(&mut coins); coins.draw(draw); - coins.set_shortcut(Shortcut::from_char('c')); - coins.set_callback(|_| { - println!("Coins pressed!"); - }); - coins.handle(logic::button_handle); + logic(&mut coins); coins } @@ -35,7 +33,7 @@ fn draw(b: &mut T) { fn draw_button(b: &mut T) { let (lw, _) = b.measure_label(); - if let Some(p) = b.parent() { + if let Some(ref p) = b.parent() { b.set_pos(p.x() + p.w() - lw - 60, p.y() + 10); b.set_size(lw + 50, p.h() - 20); } @@ -64,3 +62,39 @@ fn draw_image(b: &mut T) { coins_image.scale(24, 24, true, true); coins_image.draw(b.x() + 6, b.y() + 7, 24, 24); } + +fn logic(c: &mut T) { + c.set_shortcut(Shortcut::from_char('c')); + c.set_callback(|c| { + if let Some(ref p) = c.parent() { + if let Some(ref mut w) = p.parent() { + if let Some(ref mut re_p) = w.child(1) { + if let Some(ref mut ra_p) = w.child(2) { + if ra_p.visible() { + ra_p.hide(); + re_p.show(); + } else if re_p.visible() { + w.resize(w.x(), w.y(), w.w(), 10 + CONSTANTS.focus_pane_height + 10); + re_p.hide(); + } else { + w.resize(w.x(), w.y(), w.w(), CONSTANTS.window_height); + re_p.show(); + } + } + } + } + } + }); + c.handle(|c, ev| { + if ev == Event::KeyDown { + match app::event_key() { + Key::Left => logic::fp_handle_left(c, 1), + Key::Right => logic::fp_handle_right(c, 1), + Key::Tab => logic::handle_tab(c), + _ => logic::handle_selection(c, ev), + } + } else { + logic::handle_selection(c, ev) + } + }); +} diff --git a/src/ui/focus/pane.rs b/src/ui/focus/pane.rs index 7c2b8ee..05ae164 100644 --- a/src/ui/focus/pane.rs +++ b/src/ui/focus/pane.rs @@ -1,4 +1,9 @@ -use fltk::{enums::FrameType, group::Group, prelude::*}; +use fltk::{ + app, + enums::{Event, FrameType, Key}, + group::Group, + prelude::*, +}; use super::{arrow, coins, timer}; use crate::ui::app::CONSTANTS; @@ -7,7 +12,7 @@ pub fn pane() -> Group { let mut pane = Group::default().with_pos(10, 10); pane.set_frame(FrameType::BorderBox); - if let Some(parent) = pane.parent() { + if let Some(ref parent) = pane.parent() { pane.set_size(parent.width() - 20, CONSTANTS.focus_pane_height); } @@ -18,5 +23,32 @@ pub fn pane() -> Group { let _arrow = arrow(); pane.end(); + + pane.handle(handle); + pane } + +fn handle(p: &mut T, ev: Event) -> bool { + match ev { + Event::KeyDown => { + if app::event_key() == Key::Tab { + if let Some(ref w) = p.parent() { + if let Some(ref mut re_p) = w.child(1) { + if let Some(ref mut ra_p) = w.child(2) { + if re_p.visible() { + re_p.take_focus().ok(); + } else if ra_p.visible() { + ra_p.take_focus().ok(); + } + } + } + } + true + } else { + false + } + } + _ => false, + } +} diff --git a/src/ui/focus/timer.rs b/src/ui/focus/timer.rs index b2df89b..d475351 100644 --- a/src/ui/focus/timer.rs +++ b/src/ui/focus/timer.rs @@ -2,7 +2,7 @@ use fltk::{ app, button::Button, draw, - enums::{Align, Color, FrameType, LabelType, Shortcut}, + enums::{Align, Color, Event, FrameType, Key, LabelType, Shortcut}, image::SvgImage, prelude::*, }; @@ -35,7 +35,7 @@ fn draw(b: &mut T) { } fn draw_button(b: &mut T) { - if let Some(p) = b.parent() { + if let Some(ref p) = b.parent() { b.set_pos(p.x() + 10, p.y() + 10); b.set_size(110, p.h() - 20); } @@ -114,7 +114,18 @@ fn logic(timer: &mut T) { t.set_label("00:00:00"); true } - _ => logic::button_handle(t, ev), + _ => { + if ev == Event::KeyDown { + match app::event_key() { + Key::Left => logic::fp_handle_left(t, 0), + Key::Right => logic::fp_handle_right(t, 0), + Key::Tab => logic::handle_tab(t), + _ => logic::handle_selection(t, ev), + } + } else { + logic::handle_selection(t, ev) + } + } } }); } diff --git a/src/ui/logic.rs b/src/ui/logic.rs index c333d27..a45ba53 100644 --- a/src/ui/logic.rs +++ b/src/ui/logic.rs @@ -4,7 +4,8 @@ use fltk::{ prelude::*, }; -pub fn button_handle(b: &mut T, ev: Event) -> bool { +/// Handle focus / selection events +pub fn handle_selection(b: &mut T, ev: Event) -> bool { match ev { Event::Focus => { if app::event_key_down(Key::Enter) { @@ -54,3 +55,59 @@ fn unselect(b: &mut T) { b.set_down_frame(FrameType::FlatBox); b.redraw(); } + +/// Handle Left events for the buttons in the Focus pane +pub fn fp_handle_left(b: &mut T, idx: i32) -> bool { + if let Some(ref p) = b.parent() { + if let Some(ref mut cw) = p.child(if idx == 2 { 0 } else { idx + 1 }) { + cw.take_focus().ok(); + } + } + true +} + +/// Handle Right events for the buttons in the Focus pane +pub fn fp_handle_right(b: &mut T, idx: i32) -> bool { + if let Some(ref p) = b.parent() { + if let Some(ref mut cw) = p.child(if idx == 0 { 2 } else { idx - 1 }) { + cw.take_focus().ok(); + } + } + true +} + +/// Handle Left events for the buttons in the Rewards / Rates panes +pub fn rp_handle_left(b: &mut T, idx: i32) -> bool { + if let Some(ref m) = b.parent() { + if let Some(ref mut cw) = m.child(if idx == 0 { m.children() - 1 } else { idx - 1 }) { + cw.take_focus().ok(); + } + } + true +} + +/// Handle Right events for the buttons in the Rewards / Rates panes +pub fn rp_handle_right(b: &mut T, idx: i32) -> bool { + if let Some(ref m) = b.parent() { + if let Some(ref mut cw) = m.child(if idx == (m.children() - 1) { + 0 + } else { + idx + 1 + }) { + cw.take_focus().ok(); + } + } + true +} + +/// Handle Tab events for the buttons in the Focus Pane +pub fn handle_tab(b: &mut T) -> bool { + b.parent().map_or(false, |p| { + p.parent().map_or(false, |w| { + w.child(1).map_or(false, |re_p| { + w.child(2) + .map_or(false, |ra_p| !(re_p.visible() || ra_p.visible())) + }) + }) + }) +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 41a18d2..9401715 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -3,4 +3,5 @@ pub mod windows; mod focus; mod logic; +mod rates; mod rewards; diff --git a/src/ui/rates/list.rs b/src/ui/rates/list.rs new file mode 100644 index 0000000..ca1bf16 --- /dev/null +++ b/src/ui/rates/list.rs @@ -0,0 +1,67 @@ +use fltk::{ + app, + browser::{BrowserType, SelectBrowser}, + enums::{Event, FrameType, Key}, + prelude::*, +}; + +pub fn list() -> SelectBrowser { + let mut list = SelectBrowser::default(); + list.set_frame(FrameType::BorderBox); + list.set_type(BrowserType::Hold); + list.set_text_size(16); + + if let Some(ref p) = list.parent() { + list.set_size(0, p.h() - 20); + } + + list.add("5/m"); + list.add("0.1/s"); + list.select(1); + + logic(&mut list); + + list +} + +fn logic(l: &mut T) { + l.handle(|l, ev| match ev { + Event::Focus => true, + Event::Released => { + if l.value() == 0 { + l.select(l.size()); + } + true + } + Event::KeyDown => match app::event_key() { + Key::Down => { + l.set_type(BrowserType::Select); + let i = l.value(); + if i == l.size() { + l.select(1); + } else { + l.select(i + 1); + } + true + } + Key::Up => { + l.set_type(BrowserType::Select); + let i = l.value(); + if i == 1 { + l.select(l.size()); + } else { + l.select(i - 1); + } + true + } + _ => false, + }, + Event::KeyUp => { + if app::event_key() == Key::Down | Key::Up { + l.set_type(BrowserType::Hold); + } + true + } + _ => false, + }); +} diff --git a/src/ui/rates/menubar.rs b/src/ui/rates/menubar.rs new file mode 100644 index 0000000..ae27392 --- /dev/null +++ b/src/ui/rates/menubar.rs @@ -0,0 +1,92 @@ +use fltk::{ + app, + button::Button, + enums::{Event, FrameType, Key, Shortcut}, + group::{Pack, PackType}, + image::SvgImage, + prelude::*, +}; + +use crate::ui::app::CONSTANTS; +use crate::ui::logic; + +pub fn menubar() -> Pack { + let mut menubar = Pack::default().with_size(0, CONSTANTS.rewards_menubar_height); + menubar.set_type(PackType::Horizontal); + + let mut add_image = SvgImage::from_data(include_str!("../../../assets/menu/plus.svg")).unwrap(); + add_image.scale(24, 24, true, true); + + let mut add_button = Button::default().with_size(CONSTANTS.rewards_menubar_height, 0); + add_button.set_image(Some(add_image)); + add_button.set_frame(FrameType::FlatBox); + + let mut delete_image = + SvgImage::from_data(include_str!("../../../assets/menu/minus.svg")).unwrap(); + delete_image.scale(24, 24, true, true); + + let mut delete_button = Button::default().with_size(CONSTANTS.rewards_menubar_height, 0); + delete_button.set_image(Some(delete_image)); + delete_button.set_frame(FrameType::FlatBox); + + menubar_logic(&mut menubar); + add_button_logic(&mut add_button); + delete_button_logic(&mut delete_button); + + menubar.end(); + menubar +} + +fn menubar_logic(m: &mut T) { + m.handle(|m, ev| match ev { + Event::KeyDown => { + if app::event_key() == Key::Tab { + if let Some(ref p) = m.parent() { + if let Some(ref mut l) = p.child(1) { + l.take_focus().ok(); + } + } + true + } else { + false + } + } + _ => false, + }); +} + +fn add_button_logic(ab: &mut T) { + ab.set_shortcut(Shortcut::from_char('a')); + ab.set_callback(|_| { + println!("Add Pressed!"); + }); + ab.handle(|ab, ev| { + if ev == Event::KeyDown { + match app::event_key() { + Key::Left => logic::rp_handle_left(ab, 0), + Key::Right => logic::rp_handle_right(ab, 0), + _ => logic::handle_selection(ab, ev), + } + } else { + logic::handle_selection(ab, ev) + } + }); +} + +fn delete_button_logic(db: &mut T) { + db.set_shortcut(Shortcut::from_char('d')); + db.set_callback(|_| { + println!("Delete Pressed!"); + }); + db.handle(|db, ev| { + if ev == Event::KeyDown { + match app::event_key() { + Key::Left => logic::rp_handle_left(db, 1), + Key::Right => logic::rp_handle_right(db, 1), + _ => logic::handle_selection(db, ev), + } + } else { + logic::handle_selection(db, ev) + } + }); +} diff --git a/src/ui/rates/mod.rs b/src/ui/rates/mod.rs new file mode 100644 index 0000000..b75e425 --- /dev/null +++ b/src/ui/rates/mod.rs @@ -0,0 +1,8 @@ +mod list; +mod menubar; +mod pane; + +pub use pane::pane; + +use list::list; +use menubar::menubar; diff --git a/src/ui/rates/pane.rs b/src/ui/rates/pane.rs new file mode 100644 index 0000000..cc5ac37 --- /dev/null +++ b/src/ui/rates/pane.rs @@ -0,0 +1,23 @@ +use fltk::{group::Pack, prelude::*}; + +use super::{list, menubar}; +use crate::ui::app::CONSTANTS; + +pub fn pane() -> Pack { + let mut pane = Pack::default().with_pos(10, 10 + CONSTANTS.focus_pane_height + 10); + pane.set_spacing(1); + + if let Some(ref parent) = pane.parent() { + pane.set_size( + parent.width() - 20, + parent.height() - CONSTANTS.focus_pane_height - CONSTANTS.rewards_menubar_height - 10, + ); + } + + let _menubar = menubar(); + let _list = list(); + + pane.end(); + pane.hide(); + pane +} diff --git a/src/ui/rewards/list.rs b/src/ui/rewards/list.rs index cdc1ff4..95b7e4f 100644 --- a/src/ui/rewards/list.rs +++ b/src/ui/rewards/list.rs @@ -1,12 +1,67 @@ -use fltk::{enums::FrameType, prelude::*, table::TableRow}; +use fltk::{ + app, + browser::{BrowserType, SelectBrowser}, + enums::{Event, FrameType, Key}, + prelude::*, +}; -pub fn list() -> TableRow { - let mut list = TableRow::default(); +pub fn list() -> SelectBrowser { + let mut list = SelectBrowser::default(); list.set_frame(FrameType::BorderBox); + list.set_type(BrowserType::Hold); + list.set_text_size(16); - if let Some(p) = list.parent() { + if let Some(ref p) = list.parent() { list.set_size(0, p.h() - 20); } + list.add("(15) A reward."); + list.add("(10) Another reward."); + list.select(1); + + logic(&mut list); + list } + +fn logic(l: &mut T) { + l.handle(|l, ev| match ev { + Event::Focus => true, + Event::Released => { + if l.value() == 0 { + l.select(l.size()); + } + true + } + Event::KeyDown => match app::event_key() { + Key::Down => { + l.set_type(BrowserType::Select); + let i = l.value(); + if i == l.size() { + l.select(1); + } else { + l.select(i + 1); + } + true + } + Key::Up => { + l.set_type(BrowserType::Select); + let i = l.value(); + if i == 1 { + l.select(l.size()); + } else { + l.select(i - 1); + } + true + } + _ => false, + }, + Event::KeyUp => { + if app::event_key() == Key::Down | Key::Up { + l.set_type(BrowserType::Hold); + } + true + } + _ => false, + }); +} diff --git a/src/ui/rewards/menubar.rs b/src/ui/rewards/menubar.rs index c1324eb..ae27392 100644 --- a/src/ui/rewards/menubar.rs +++ b/src/ui/rewards/menubar.rs @@ -1,6 +1,7 @@ use fltk::{ + app, button::Button, - enums::{FrameType, Shortcut}, + enums::{Event, FrameType, Key, Shortcut}, group::{Pack, PackType}, image::SvgImage, prelude::*, @@ -28,29 +29,64 @@ pub fn menubar() -> Pack { delete_button.set_image(Some(delete_image)); delete_button.set_frame(FrameType::FlatBox); - logic(&mut add_button, &mut delete_button); + menubar_logic(&mut menubar); + add_button_logic(&mut add_button); + delete_button_logic(&mut delete_button); menubar.end(); menubar } -fn logic( - add_button: &mut T, - delete_button: &mut U, -) { - add_button.set_shortcut(Shortcut::from_char('a')); +fn menubar_logic(m: &mut T) { + m.handle(|m, ev| match ev { + Event::KeyDown => { + if app::event_key() == Key::Tab { + if let Some(ref p) = m.parent() { + if let Some(ref mut l) = p.child(1) { + l.take_focus().ok(); + } + } + true + } else { + false + } + } + _ => false, + }); +} - add_button.set_callback(|_| { +fn add_button_logic(ab: &mut T) { + ab.set_shortcut(Shortcut::from_char('a')); + ab.set_callback(|_| { println!("Add Pressed!"); }); + ab.handle(|ab, ev| { + if ev == Event::KeyDown { + match app::event_key() { + Key::Left => logic::rp_handle_left(ab, 0), + Key::Right => logic::rp_handle_right(ab, 0), + _ => logic::handle_selection(ab, ev), + } + } else { + logic::handle_selection(ab, ev) + } + }); +} - add_button.handle(logic::button_handle); - - delete_button.set_shortcut(Shortcut::from_char('d')); - - delete_button.set_callback(|_| { +fn delete_button_logic(db: &mut T) { + db.set_shortcut(Shortcut::from_char('d')); + db.set_callback(|_| { println!("Delete Pressed!"); }); - - delete_button.handle(logic::button_handle); + db.handle(|db, ev| { + if ev == Event::KeyDown { + match app::event_key() { + Key::Left => logic::rp_handle_left(db, 1), + Key::Right => logic::rp_handle_right(db, 1), + _ => logic::handle_selection(db, ev), + } + } else { + logic::handle_selection(db, ev) + } + }); } diff --git a/src/ui/rewards/pane.rs b/src/ui/rewards/pane.rs index 45ed39d..c0cf875 100644 --- a/src/ui/rewards/pane.rs +++ b/src/ui/rewards/pane.rs @@ -7,15 +7,15 @@ pub fn pane() -> Pack { let mut pane = Pack::default().with_pos(10, 10 + CONSTANTS.focus_pane_height + 10); pane.set_spacing(1); - if let Some(parent) = pane.parent() { + if let Some(ref parent) = pane.parent() { pane.set_size( parent.width() - 20, - parent.height() - 10 - 10 - CONSTANTS.focus_pane_height - 10, + parent.height() - CONSTANTS.focus_pane_height - CONSTANTS.rewards_menubar_height - 10, ); } let _menubar = menubar(); - let _rewards = list(); + let _list = list(); pane.end(); pane diff --git a/src/ui/windows/main.rs b/src/ui/windows/main.rs index 0474ee0..cdc73d4 100644 --- a/src/ui/windows/main.rs +++ b/src/ui/windows/main.rs @@ -4,6 +4,7 @@ use super::icon; use crate::channels::Channels; use crate::ui::app::CONSTANTS; use crate::ui::focus; +use crate::ui::rates; use crate::ui::rewards; /// Create the Main Window @@ -15,6 +16,12 @@ pub fn main(_channels: &Channels) -> Window { CONSTANTS.window_height, "Shuchu", ); + window.size_range( + CONSTANTS.window_width, + CONSTANTS.window_height, + CONSTANTS.window_width, + CONSTANTS.window_height + 100, + ); window.set_icon(Some(icon())); // 1. Focus Pane @@ -23,7 +30,12 @@ pub fn main(_channels: &Channels) -> Window { // 2. Rewards Pane let _rewards_pane = rewards::pane(); + // 3. Conversion Rates Pane + let _rates_pane = rates::pane(); + + // window.resizable(&rewards_pane); window.end(); + window.show(); window } From d33bab56cf2767aee5ec07db74b6e32fe3554f4e Mon Sep 17 00:00:00 2001 From: Pavel Sobolev Date: Sat, 17 Jul 2021 18:36:26 +0300 Subject: [PATCH 04/16] Completely rewrite Browser widget from scratch. Add the Rewards Edit window. --- .cargo/config.toml | 17 ++- src/channels.rs | 10 +- src/events.rs | 12 +- src/main.rs | 17 ++- src/ui/app.rs | 12 +- src/ui/focus/arrow.rs | 6 +- src/ui/focus/coins.rs | 6 +- src/ui/focus/timer.rs | 4 +- src/ui/logic.rs | 14 +-- src/ui/mod.rs | 1 + src/ui/rates/list.rs | 61 +--------- src/ui/rates/menubar.rs | 8 +- src/ui/rates/pane.rs | 5 +- src/ui/rewards/edit/buttons.rs | 24 ++++ src/ui/rewards/edit/cancel.rs | 25 ++++ src/ui/rewards/edit/coins.rs | 58 ++++++++++ src/ui/rewards/edit/mod.rs | 12 ++ src/ui/rewards/edit/ok.rs | 24 ++++ src/ui/rewards/edit/reward.rs | 48 ++++++++ src/ui/rewards/list.rs | 66 +++-------- src/ui/rewards/menubar.rs | 23 ++-- src/ui/rewards/mod.rs | 2 + src/ui/rewards/pane.rs | 10 +- src/ui/widgets/list.rs | 203 +++++++++++++++++++++++++++++++++ src/ui/widgets/mod.rs | 3 + src/ui/windows/main.rs | 18 +-- src/ui/windows/mod.rs | 2 + src/ui/windows/rewards_edit.rs | 46 ++++++++ 28 files changed, 579 insertions(+), 158 deletions(-) create mode 100644 src/ui/rewards/edit/buttons.rs create mode 100644 src/ui/rewards/edit/cancel.rs create mode 100644 src/ui/rewards/edit/coins.rs create mode 100644 src/ui/rewards/edit/mod.rs create mode 100644 src/ui/rewards/edit/ok.rs create mode 100644 src/ui/rewards/edit/reward.rs create mode 100644 src/ui/widgets/list.rs create mode 100644 src/ui/widgets/mod.rs create mode 100644 src/ui/windows/rewards_edit.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index d38acd0..f762aa6 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,2 +1,17 @@ [alias] -lint = ["clippy", "--", "-D", "clippy::pedantic", "-D", "clippy::cargo", "-A", "clippy::option_if_let_else"] +lint = [ + "clippy", + "--", + "-D", + "clippy::pedantic", + "-D", + "clippy::cargo", + "-A", + "clippy::option_if_let_else", + "-A", + "clippy::cast_possible_truncation", + "-A", + "clippy::cast_possible_wrap", + "-A", + "clippy::cast_sign_loss" +] diff --git a/src/channels.rs b/src/channels.rs index c13eac2..1c66f20 100644 --- a/src/channels.rs +++ b/src/channels.rs @@ -46,7 +46,13 @@ channels_default_impl! { /// A struct providing access to the application's channels #[derive(Clone)] pub struct Channels { - /// Channel 1: Nothing Yet - pub nothing: Channel, + /// Channel 1: From Any Window to Main Window + pub mw: Channel, + /// Channel 2: From Main Window to Rewards Edit Window + pub rewards_edit: Channel, + /// Channel 3: Send Coins To Reward in the Rewards Edit Window + pub rewards_coins: Channel, + /// Channel 3: Send Reward (including Coins) from the Rewards Edit Window to Rewards + pub rewards_reward: Channel, } } diff --git a/src/events.rs b/src/events.rs index 073fa88..65b56cc 100644 --- a/src/events.rs +++ b/src/events.rs @@ -14,4 +14,14 @@ macro_rules! events { () => {}; } -events!(START_TIMER, TICK, STOP_TIMER); +events!( + START_TIMER, + TICK, + STOP_TIMER, + ADD_A_REWARD_OPEN, + ADD_A_REWARD_SEND_COINS, + ADD_A_REWARD_SEND_REWARD, + ADD_A_REWARD_RECEIVE, + ADD_A_REWARD_RESET_COINS, + ADD_A_REWARD_RESET_REWARD +); diff --git a/src/main.rs b/src/main.rs index 18b30a6..b1a4ee7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,9 +19,22 @@ fn main() { // 0. App let app = ui::app::new(); - // 1. Window + // 1. Main Window let _window = ui::windows::main(&channels); + // Hidden Windows + + // 1. Reward Edit Window + let rewards_edit = ui::windows::rewards_edit(&channels); + // Start the event loop - while app.wait() {} + while app.wait() { + // Retranslation of signals between windows + if let Ok(event) = channels.mw.r.try_recv() { + app::handle_main(event).ok(); + }; + if let Ok(event) = channels.rewards_edit.r.try_recv() { + app::handle(event, &rewards_edit).ok(); + } + } } diff --git a/src/ui/app.rs b/src/ui/app.rs index 7bb43e3..736cc61 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -5,20 +5,24 @@ use fltk::{ /// A struct providing access to the application's constants pub struct Constants { - pub window_width: i32, - pub window_height: i32, + pub main_window_width: i32, + pub main_window_height: i32, pub focus_pane_height: i32, pub rewards_menubar_height: i32, + pub rewards_edit_window_width: i32, + pub rewards_edit_window_height: i32, } impl Constants { /// Get the default set of the application's constants const fn default() -> Constants { Constants { - window_width: 340, - window_height: 400, + main_window_width: 340, + main_window_height: 300, focus_pane_height: 60, rewards_menubar_height: 30, + rewards_edit_window_width: 320, + rewards_edit_window_height: 140, } } } diff --git a/src/ui/focus/arrow.rs b/src/ui/focus/arrow.rs index e0b2de9..20e80ab 100644 --- a/src/ui/focus/arrow.rs +++ b/src/ui/focus/arrow.rs @@ -53,7 +53,7 @@ fn logic(a: &mut T) { w.resize(w.x(), w.y(), w.w(), 10 + CONSTANTS.focus_pane_height + 10); ra_p.hide(); } else { - w.resize(w.x(), w.y(), w.w(), CONSTANTS.window_height); + w.resize(w.x(), w.y(), w.w(), CONSTANTS.main_window_height); ra_p.show(); } } @@ -67,10 +67,10 @@ fn logic(a: &mut T) { Key::Left => logic::fp_handle_left(a, 2), Key::Right => logic::fp_handle_right(a, 2), Key::Tab => logic::handle_tab(a), - _ => logic::handle_selection(a, ev), + _ => logic::handle_selection(a, ev, FrameType::FlatBox), } } else { - logic::handle_selection(a, ev) + logic::handle_selection(a, ev, FrameType::FlatBox) } }); } diff --git a/src/ui/focus/coins.rs b/src/ui/focus/coins.rs index ef23021..4755645 100644 --- a/src/ui/focus/coins.rs +++ b/src/ui/focus/coins.rs @@ -77,7 +77,7 @@ fn logic(c: &mut T) { w.resize(w.x(), w.y(), w.w(), 10 + CONSTANTS.focus_pane_height + 10); re_p.hide(); } else { - w.resize(w.x(), w.y(), w.w(), CONSTANTS.window_height); + w.resize(w.x(), w.y(), w.w(), CONSTANTS.main_window_height); re_p.show(); } } @@ -91,10 +91,10 @@ fn logic(c: &mut T) { Key::Left => logic::fp_handle_left(c, 1), Key::Right => logic::fp_handle_right(c, 1), Key::Tab => logic::handle_tab(c), - _ => logic::handle_selection(c, ev), + _ => logic::handle_selection(c, ev, FrameType::FlatBox), } } else { - logic::handle_selection(c, ev) + logic::handle_selection(c, ev, FrameType::FlatBox) } }); } diff --git a/src/ui/focus/timer.rs b/src/ui/focus/timer.rs index d475351..8dbb5a9 100644 --- a/src/ui/focus/timer.rs +++ b/src/ui/focus/timer.rs @@ -120,10 +120,10 @@ fn logic(timer: &mut T) { Key::Left => logic::fp_handle_left(t, 0), Key::Right => logic::fp_handle_right(t, 0), Key::Tab => logic::handle_tab(t), - _ => logic::handle_selection(t, ev), + _ => logic::handle_selection(t, ev, FrameType::FlatBox), } } else { - logic::handle_selection(t, ev) + logic::handle_selection(t, ev, FrameType::FlatBox) } } } diff --git a/src/ui/logic.rs b/src/ui/logic.rs index a45ba53..9b2fb9f 100644 --- a/src/ui/logic.rs +++ b/src/ui/logic.rs @@ -5,7 +5,7 @@ use fltk::{ }; /// Handle focus / selection events -pub fn handle_selection(b: &mut T, ev: Event) -> bool { +pub fn handle_selection(b: &mut T, ev: Event, unselect_box: FrameType) -> bool { match ev { Event::Focus => { if app::event_key_down(Key::Enter) { @@ -17,8 +17,8 @@ pub fn handle_selection(b: &mut T, ev: Event) -> bool { select(b); true } - Event::Leave | Event::Unfocus => { - unselect(b); + Event::Leave | Event::Unfocus | Event::Hide => { + unselect(b, unselect_box); true } Event::KeyDown => match app::event_key() { @@ -31,7 +31,7 @@ pub fn handle_selection(b: &mut T, ev: Event) -> bool { Event::KeyUp => match app::event_key() { Key::Enter => { b.do_callback(); - unselect(b); + unselect(b, unselect_box); true } _ => false, @@ -48,11 +48,11 @@ fn select(b: &mut T) { b.redraw(); } -fn unselect(b: &mut T) { +fn unselect(b: &mut T, unselect_box: FrameType) { b.set_color(Color::BackGround); - b.set_frame(FrameType::FlatBox); + b.set_frame(unselect_box); b.set_selection_color(Color::BackGround); - b.set_down_frame(FrameType::FlatBox); + b.set_down_frame(unselect_box); b.redraw(); } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 9401715..9eede90 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -5,3 +5,4 @@ mod focus; mod logic; mod rates; mod rewards; +mod widgets; diff --git a/src/ui/rates/list.rs b/src/ui/rates/list.rs index ca1bf16..85614fe 100644 --- a/src/ui/rates/list.rs +++ b/src/ui/rates/list.rs @@ -1,15 +1,9 @@ -use fltk::{ - app, - browser::{BrowserType, SelectBrowser}, - enums::{Event, FrameType, Key}, - prelude::*, -}; +use fltk::prelude::*; -pub fn list() -> SelectBrowser { - let mut list = SelectBrowser::default(); - list.set_frame(FrameType::BorderBox); - list.set_type(BrowserType::Hold); - list.set_text_size(16); +use crate::ui::widgets::List; + +pub fn list() -> List { + let mut list = List::default(); if let Some(ref p) = list.parent() { list.set_size(0, p.h() - 20); @@ -17,51 +11,6 @@ pub fn list() -> SelectBrowser { list.add("5/m"); list.add("0.1/s"); - list.select(1); - - logic(&mut list); list } - -fn logic(l: &mut T) { - l.handle(|l, ev| match ev { - Event::Focus => true, - Event::Released => { - if l.value() == 0 { - l.select(l.size()); - } - true - } - Event::KeyDown => match app::event_key() { - Key::Down => { - l.set_type(BrowserType::Select); - let i = l.value(); - if i == l.size() { - l.select(1); - } else { - l.select(i + 1); - } - true - } - Key::Up => { - l.set_type(BrowserType::Select); - let i = l.value(); - if i == 1 { - l.select(l.size()); - } else { - l.select(i - 1); - } - true - } - _ => false, - }, - Event::KeyUp => { - if app::event_key() == Key::Down | Key::Up { - l.set_type(BrowserType::Hold); - } - true - } - _ => false, - }); -} diff --git a/src/ui/rates/menubar.rs b/src/ui/rates/menubar.rs index ae27392..18f05dd 100644 --- a/src/ui/rates/menubar.rs +++ b/src/ui/rates/menubar.rs @@ -65,10 +65,10 @@ fn add_button_logic(ab: &mut T) { match app::event_key() { Key::Left => logic::rp_handle_left(ab, 0), Key::Right => logic::rp_handle_right(ab, 0), - _ => logic::handle_selection(ab, ev), + _ => logic::handle_selection(ab, ev, FrameType::FlatBox), } } else { - logic::handle_selection(ab, ev) + logic::handle_selection(ab, ev, FrameType::FlatBox) } }); } @@ -83,10 +83,10 @@ fn delete_button_logic(db: &mut T) { match app::event_key() { Key::Left => logic::rp_handle_left(db, 1), Key::Right => logic::rp_handle_right(db, 1), - _ => logic::handle_selection(db, ev), + _ => logic::handle_selection(db, ev, FrameType::FlatBox), } } else { - logic::handle_selection(db, ev) + logic::handle_selection(db, ev, FrameType::FlatBox) } }); } diff --git a/src/ui/rates/pane.rs b/src/ui/rates/pane.rs index cc5ac37..850f9af 100644 --- a/src/ui/rates/pane.rs +++ b/src/ui/rates/pane.rs @@ -10,14 +10,15 @@ pub fn pane() -> Pack { if let Some(ref parent) = pane.parent() { pane.set_size( parent.width() - 20, - parent.height() - CONSTANTS.focus_pane_height - CONSTANTS.rewards_menubar_height - 10, + parent.height() - CONSTANTS.focus_pane_height - CONSTANTS.rewards_menubar_height, ); } let _menubar = menubar(); - let _list = list(); + let list = list(); pane.end(); + pane.resizable(list.scroll()); pane.hide(); pane } diff --git a/src/ui/rewards/edit/buttons.rs b/src/ui/rewards/edit/buttons.rs new file mode 100644 index 0000000..08463b3 --- /dev/null +++ b/src/ui/rewards/edit/buttons.rs @@ -0,0 +1,24 @@ +use fltk::{ + group::{Pack, PackType}, + prelude::*, +}; + +use super::{cancel, ok}; +use crate::ui::app::CONSTANTS; + +pub fn buttons() -> Pack { + let mut buttons = Pack::default() + .with_pos( + CONSTANTS.rewards_edit_window_width - 10 - 2 * 80 - 10, + CONSTANTS.rewards_edit_window_height - 20 - 15, + ) + .with_size(2 * 80, 25); + buttons.set_spacing(10); + buttons.set_type(PackType::Horizontal); + + let _cancel = cancel(); + let _ok = ok(); + + buttons.end(); + buttons +} diff --git a/src/ui/rewards/edit/cancel.rs b/src/ui/rewards/edit/cancel.rs new file mode 100644 index 0000000..7eb21a3 --- /dev/null +++ b/src/ui/rewards/edit/cancel.rs @@ -0,0 +1,25 @@ +use fltk::{app, button::Button, enums::FrameType, prelude::*}; + +use crate::events; +use crate::ui::logic; + +pub fn cancel() -> Button { + let mut cancel = Button::default().with_size(80, 0).with_label("Cancel"); + + logic(&mut cancel); + + cancel +} + +fn logic(b: &mut T) { + b.set_callback(|b| { + app::handle_main(events::ADD_A_REWARD_RESET_COINS).ok(); + app::handle_main(events::ADD_A_REWARD_RESET_REWARD).ok(); + if let Some(ref p) = b.parent() { + if let Some(ref mut w) = p.parent() { + w.hide(); + } + } + }); + b.handle(|c, ev| logic::handle_selection(c, ev, FrameType::BorderBox)); +} diff --git a/src/ui/rewards/edit/coins.rs b/src/ui/rewards/edit/coins.rs new file mode 100644 index 0000000..ba91205 --- /dev/null +++ b/src/ui/rewards/edit/coins.rs @@ -0,0 +1,58 @@ +use fltk::{ + app, + enums::{Align, CallbackTrigger, FrameType}, + prelude::*, + valuator::ValueInput, +}; + +use crate::channels::Channels; +use crate::events; +use crate::ui::app::CONSTANTS; + +pub fn coins(channels: &Channels) -> ValueInput { + let mut coins = ValueInput::new( + 10, + 25, + CONSTANTS.rewards_edit_window_width - 20, + 20, + "Coins:", + ); + coins.set_frame(FrameType::BorderBox); + coins.set_align(Align::TopLeft); + coins.set_label_size(16); + coins.set_text_size(16); + coins.set_step(0.0, 5); + coins.set_precision(2); + coins.set_bounds(0.0, 999_999_999.0); + coins.set_range(0.0, 999_999_999.0); + coins.set_soft(false); + coins.set_value(0.1); + coins.set_value(0.0); + + logic(&mut coins, channels); + + coins +} + +fn logic(v: &mut T, channels: &Channels) { + v.set_trigger(CallbackTrigger::Changed); + v.set_callback(|v| { + v.set_value(v.clamp(v.value())); + }); + v.handle({ + let s = channels.rewards_coins.s.clone(); + move |v, ev| match ev.bits() { + events::ADD_A_REWARD_SEND_COINS => { + s.try_send(v.value()).ok(); + app::handle_main(events::ADD_A_REWARD_SEND_REWARD).ok(); + v.set_value(0.0); + true + } + events::ADD_A_REWARD_RESET_COINS => { + v.set_value(0.0); + true + } + _ => false, + } + }) +} diff --git a/src/ui/rewards/edit/mod.rs b/src/ui/rewards/edit/mod.rs new file mode 100644 index 0000000..0128d33 --- /dev/null +++ b/src/ui/rewards/edit/mod.rs @@ -0,0 +1,12 @@ +mod buttons; +mod cancel; +mod coins; +mod ok; +mod reward; + +pub use buttons::buttons; +pub use coins::coins; +pub use reward::reward; + +use cancel::cancel; +use ok::ok; diff --git a/src/ui/rewards/edit/ok.rs b/src/ui/rewards/edit/ok.rs new file mode 100644 index 0000000..4c127eb --- /dev/null +++ b/src/ui/rewards/edit/ok.rs @@ -0,0 +1,24 @@ +use fltk::{app, button::Button, enums::FrameType, prelude::*}; + +use crate::events; +use crate::ui::logic; + +pub fn ok() -> Button { + let mut ok = Button::default().with_size(80, 0).with_label("OK"); + + logic(&mut ok); + + ok +} + +fn logic(b: &mut T) { + b.set_callback(|b| { + app::handle_main(events::ADD_A_REWARD_SEND_COINS).ok(); + if let Some(ref p) = b.parent() { + if let Some(ref mut w) = p.parent() { + w.hide(); + } + } + }); + b.handle(|o, ev| logic::handle_selection(o, ev, FrameType::BorderBox)); +} diff --git a/src/ui/rewards/edit/reward.rs b/src/ui/rewards/edit/reward.rs new file mode 100644 index 0000000..43af225 --- /dev/null +++ b/src/ui/rewards/edit/reward.rs @@ -0,0 +1,48 @@ +use fltk::{ + enums::{Align, FrameType}, + input::Input, + prelude::*, +}; + +use crate::channels::Channels; +use crate::events; +use crate::ui::app::CONSTANTS; + +pub fn reward(channels: &Channels) -> Input { + let mut reward = Input::new( + 10, + 70, + CONSTANTS.rewards_edit_window_width - 20, + 20, + "Reward:", + ); + reward.set_frame(FrameType::BorderBox); + reward.set_align(Align::TopLeft); + reward.set_label_size(16); + reward.set_text_size(16); + + logic(&mut reward, channels); + + reward +} + +fn logic(i: &mut T, channels: &Channels) { + i.handle({ + let r_coins = channels.rewards_coins.r.clone(); + let s_reward = channels.rewards_reward.s.clone(); + let s_mw = channels.mw.s.clone(); + move |i, ev| match ev.bits() { + events::ADD_A_REWARD_SEND_REWARD => r_coins.try_recv().map_or(false, |coins| { + s_reward.try_send(format!("({}) {}", coins, i.value())).ok(); + s_mw.try_send(events::ADD_A_REWARD_RECEIVE).ok(); + i.set_value(""); + true + }), + events::ADD_A_REWARD_RESET_REWARD => { + i.set_value(""); + true + } + _ => false, + } + }); +} diff --git a/src/ui/rewards/list.rs b/src/ui/rewards/list.rs index 95b7e4f..add2682 100644 --- a/src/ui/rewards/list.rs +++ b/src/ui/rewards/list.rs @@ -1,15 +1,11 @@ -use fltk::{ - app, - browser::{BrowserType, SelectBrowser}, - enums::{Event, FrameType, Key}, - prelude::*, -}; +use fltk::prelude::*; -pub fn list() -> SelectBrowser { - let mut list = SelectBrowser::default(); - list.set_frame(FrameType::BorderBox); - list.set_type(BrowserType::Hold); - list.set_text_size(16); +use crate::channels::Channels; +use crate::events; +use crate::ui::widgets::{List, ScrollExt}; + +pub fn list(channels: &Channels) -> List { + let mut list = List::default(); if let Some(ref p) = list.parent() { list.set_size(0, p.h() - 20); @@ -17,51 +13,23 @@ pub fn list() -> SelectBrowser { list.add("(15) A reward."); list.add("(10) Another reward."); - list.select(1); + list.add("(5) One more reward."); - logic(&mut list); + logic(&mut list, channels); list } -fn logic(l: &mut T) { - l.handle(|l, ev| match ev { - Event::Focus => true, - Event::Released => { - if l.value() == 0 { - l.select(l.size()); - } - true - } - Event::KeyDown => match app::event_key() { - Key::Down => { - l.set_type(BrowserType::Select); - let i = l.value(); - if i == l.size() { - l.select(1); - } else { - l.select(i + 1); - } - true - } - Key::Up => { - l.set_type(BrowserType::Select); - let i = l.value(); - if i == 1 { - l.select(l.size()); - } else { - l.select(i - 1); - } +fn logic(l: &mut List, channels: &Channels) { + l.handle({ + let r_reward = channels.rewards_reward.r.clone(); + move |s, items, bits| match bits { + events::ADD_A_REWARD_RECEIVE => r_reward.try_recv().map_or(false, |reward| { + items.push(reward); + s.expand(items.len() as i32); true - } + }), _ => false, - }, - Event::KeyUp => { - if app::event_key() == Key::Down | Key::Up { - l.set_type(BrowserType::Hold); - } - true } - _ => false, }); } diff --git a/src/ui/rewards/menubar.rs b/src/ui/rewards/menubar.rs index ae27392..edce157 100644 --- a/src/ui/rewards/menubar.rs +++ b/src/ui/rewards/menubar.rs @@ -7,10 +7,12 @@ use fltk::{ prelude::*, }; +use crate::channels::Channels; +use crate::events; use crate::ui::app::CONSTANTS; use crate::ui::logic; -pub fn menubar() -> Pack { +pub fn menubar(channels: &Channels) -> Pack { let mut menubar = Pack::default().with_size(0, CONSTANTS.rewards_menubar_height); menubar.set_type(PackType::Horizontal); @@ -30,7 +32,7 @@ pub fn menubar() -> Pack { delete_button.set_frame(FrameType::FlatBox); menubar_logic(&mut menubar); - add_button_logic(&mut add_button); + add_button_logic(&mut add_button, channels); delete_button_logic(&mut delete_button); menubar.end(); @@ -55,20 +57,23 @@ fn menubar_logic(m: &mut T) { }); } -fn add_button_logic(ab: &mut T) { +fn add_button_logic(ab: &mut T, channels: &Channels) { ab.set_shortcut(Shortcut::from_char('a')); - ab.set_callback(|_| { - println!("Add Pressed!"); + ab.set_callback({ + let s = channels.rewards_edit.s.clone(); + move |_| { + s.try_send(events::ADD_A_REWARD_OPEN).ok(); + } }); ab.handle(|ab, ev| { if ev == Event::KeyDown { match app::event_key() { Key::Left => logic::rp_handle_left(ab, 0), Key::Right => logic::rp_handle_right(ab, 0), - _ => logic::handle_selection(ab, ev), + _ => logic::handle_selection(ab, ev, FrameType::FlatBox), } } else { - logic::handle_selection(ab, ev) + logic::handle_selection(ab, ev, FrameType::FlatBox) } }); } @@ -83,10 +88,10 @@ fn delete_button_logic(db: &mut T) { match app::event_key() { Key::Left => logic::rp_handle_left(db, 1), Key::Right => logic::rp_handle_right(db, 1), - _ => logic::handle_selection(db, ev), + _ => logic::handle_selection(db, ev, FrameType::FlatBox), } } else { - logic::handle_selection(db, ev) + logic::handle_selection(db, ev, FrameType::FlatBox) } }); } diff --git a/src/ui/rewards/mod.rs b/src/ui/rewards/mod.rs index b75e425..ff285f9 100644 --- a/src/ui/rewards/mod.rs +++ b/src/ui/rewards/mod.rs @@ -1,3 +1,5 @@ +pub mod edit; + mod list; mod menubar; mod pane; diff --git a/src/ui/rewards/pane.rs b/src/ui/rewards/pane.rs index c0cf875..9bab3b4 100644 --- a/src/ui/rewards/pane.rs +++ b/src/ui/rewards/pane.rs @@ -1,22 +1,24 @@ use fltk::{group::Pack, prelude::*}; use super::{list, menubar}; +use crate::channels::Channels; use crate::ui::app::CONSTANTS; -pub fn pane() -> Pack { +pub fn pane(channels: &Channels) -> Pack { let mut pane = Pack::default().with_pos(10, 10 + CONSTANTS.focus_pane_height + 10); pane.set_spacing(1); if let Some(ref parent) = pane.parent() { pane.set_size( parent.width() - 20, - parent.height() - CONSTANTS.focus_pane_height - CONSTANTS.rewards_menubar_height - 10, + parent.height() - CONSTANTS.focus_pane_height - CONSTANTS.rewards_menubar_height, ); } - let _menubar = menubar(); - let _list = list(); + let _menubar = menubar(channels); + let list = list(channels); + pane.resizable(list.scroll()); pane.end(); pane } diff --git a/src/ui/widgets/list.rs b/src/ui/widgets/list.rs new file mode 100644 index 0000000..0509ea1 --- /dev/null +++ b/src/ui/widgets/list.rs @@ -0,0 +1,203 @@ +use fltk::{ + app, draw, + enums::{Align, Color, Event, FrameType, Key}, + group::{Group, Scroll, ScrollType}, + prelude::*, + widget::Widget, +}; +use std::{cell::RefCell, rc::Rc}; + +type Selected = Rc>; +type Items = Rc>>; + +pub trait ScrollExt: GroupExt { + fn expand(&mut self, n: i32) { + if let Some(ref mut l) = self.child(0) { + l.resize(self.x() + 1, self.y() + 1, self.w() - 2, n * 20); + self.redraw(); + } + } +} + +impl ScrollExt for Scroll {} + +pub struct List { + scroll: Scroll, + list: Widget, + selected: Selected, + items: Items, +} + +impl List { + pub fn default() -> List { + let mut scroll = Scroll::default(); + scroll.set_frame(FrameType::BorderBox); + scroll.set_type(ScrollType::Vertical); + scroll.set_scrollbar_size(17); + + let mut list = Widget::default(); + list.set_color(Color::White); + list.set_selection_color(Color::DarkBlue); + + scroll.end(); + + let mut w = List { + scroll, + list, + selected: Selected::new(RefCell::new(1)), + items: Items::default(), + }; + w.draw(); + w.handle(|_, _, _| false); + w + } + + pub fn scroll(&self) -> &Scroll { + &self.scroll + } + + pub fn parent(&self) -> Option { + self.scroll.parent() + } + + pub fn set_size(&mut self, width: i32, height: i32) { + if width == 0 { + if let Some(p) = self.parent() { + self.scroll.set_size(p.w(), height); + } + } else { + self.scroll.set_size(width, height); + } + } + + pub fn add(&mut self, s: &'static str) { + self.items.borrow_mut().push(String::from(s)); + self.scroll.expand(self.items.borrow().len() as i32); + } + + fn draw(&mut self) { + let selected = Rc::clone(&self.selected); + let items = Rc::clone(&self.items); + self.list.draw(move |l| { + let lw = f64::from(l.w()); + let dw = draw::width("..."); + let color = draw::get_color(); + for (idx, s) in (0..).zip(items.borrow().iter()) { + if idx + 1 == *selected.borrow() { + draw::draw_rect_fill(l.x(), l.y() + idx * 20, l.w(), 20, l.selection_color()); + draw::set_draw_color(Color::White); + draw::set_font(draw::font(), 16); + } else { + draw::set_font(draw::font(), 16); + draw::set_draw_color(Color::Black); + } + + if draw::width(s) < lw { + draw::draw_text2(s, l.x() + 4, l.y() + idx * 20, l.w() - 8, 20, Align::Left); + } else { + let mut n = s.len(); + while draw::width(&s[..n]) + dw > lw - 8.0 { + n -= 1; + } + draw::draw_text2( + &format!("{}...", &s[..n]), + l.x() + 4, + l.y() + idx * 20, + l.w() - 8, + 20, + Align::Left, + ); + } + } + draw::set_draw_color(color); + }); + } + + pub fn handle(&mut self, handle_custom_events: F) + where + F: Fn(&mut Scroll, &mut Vec, i32) -> bool, + { + self.scroll.handle({ + let selected = Rc::clone(&self.selected); + let items = Rc::clone(&self.items); + move |s, ev| match ev { + Event::Focus => true, + Event::Push => handle_push(s, &selected, &items), + Event::KeyDown => handle_keydown(s, &selected, &items), + _ => handle_custom_events(s, &mut *items.borrow_mut(), ev.bits()), + } + }); + } +} + +fn handle_push(s: &mut Scroll, selected: &Selected, items: &Items) -> bool { + s.take_focus().ok(); + + if let Some(l) = s.child(0) { + let mut empty_space_click = true; + let ys = s.yposition(); + let y = app::event_y(); + let i_max = items.borrow().len() as i32; + + if l.h() < s.h() - 20 { + for i in 0..i_max { + if y >= s.y() + i * 20 - ys as i32 && y <= s.y() + (i + 1) * 20 - ys { + if *selected.borrow() != i + 1 { + *selected.borrow_mut() = i + 1; + s.redraw(); + } + empty_space_click = false; + break; + } + } + } else { + let x = app::event_x(); + + if x >= s.x() + s.w() - 17 { + empty_space_click = false; + } else { + for i in 0..i_max { + if y >= s.y() + i * 20 - ys as i32 && y <= s.y() + (i + 1) * 20 - ys { + if *selected.borrow() != i + 1 { + *selected.borrow_mut() = i + 1; + s.redraw(); + } + empty_space_click = false; + break; + } + } + } + } + + if empty_space_click && *selected.borrow() != i_max { + *selected.borrow_mut() = i_max; + s.redraw(); + } + } + + true +} + +fn handle_keydown(s: &mut Scroll, selected: &Selected, items: &Items) -> bool { + match app::event_key() { + Key::Up => { + if *selected.borrow() == 1 { + *selected.borrow_mut() = items.borrow().len() as i32; + } else { + *selected.borrow_mut() -= 1; + } + s.redraw(); + true + } + Key::Down => { + if *selected.borrow() == items.borrow().len() as i32 { + *selected.borrow_mut() = 1; + } else { + *selected.borrow_mut() += 1; + } + s.redraw(); + true + } + _ => false, + } +} diff --git a/src/ui/widgets/mod.rs b/src/ui/widgets/mod.rs new file mode 100644 index 0000000..6fc51ac --- /dev/null +++ b/src/ui/widgets/mod.rs @@ -0,0 +1,3 @@ +mod list; + +pub use list::{List, ScrollExt}; diff --git a/src/ui/windows/main.rs b/src/ui/windows/main.rs index cdc73d4..2ecad78 100644 --- a/src/ui/windows/main.rs +++ b/src/ui/windows/main.rs @@ -8,19 +8,19 @@ use crate::ui::rates; use crate::ui::rewards; /// Create the Main Window -pub fn main(_channels: &Channels) -> Window { +pub fn main(channels: &Channels) -> Window { let mut window = Window::new( 100, 100, - CONSTANTS.window_width, - CONSTANTS.window_height, + CONSTANTS.main_window_width, + CONSTANTS.main_window_height, "Shuchu", ); window.size_range( - CONSTANTS.window_width, - CONSTANTS.window_height, - CONSTANTS.window_width, - CONSTANTS.window_height + 100, + CONSTANTS.main_window_width, + CONSTANTS.main_window_height, + CONSTANTS.main_window_width, + CONSTANTS.main_window_height + 100, ); window.set_icon(Some(icon())); @@ -28,12 +28,12 @@ pub fn main(_channels: &Channels) -> Window { let _focus_pane = focus::pane(); // 2. Rewards Pane - let _rewards_pane = rewards::pane(); + let rewards_pane = rewards::pane(channels); // 3. Conversion Rates Pane let _rates_pane = rates::pane(); - // window.resizable(&rewards_pane); + window.resizable(&rewards_pane); window.end(); window.show(); diff --git a/src/ui/windows/mod.rs b/src/ui/windows/mod.rs index 3505d8f..996d329 100644 --- a/src/ui/windows/mod.rs +++ b/src/ui/windows/mod.rs @@ -1,6 +1,8 @@ mod icon; mod main; +mod rewards_edit; pub use main::main; +pub use rewards_edit::rewards_edit; use icon::icon; diff --git a/src/ui/windows/rewards_edit.rs b/src/ui/windows/rewards_edit.rs new file mode 100644 index 0000000..9b38771 --- /dev/null +++ b/src/ui/windows/rewards_edit.rs @@ -0,0 +1,46 @@ +use fltk::{enums::Event, prelude::*, window::Window}; + +use super::icon; +use crate::channels::Channels; +use crate::events; +use crate::ui::app::CONSTANTS; +use crate::ui::rewards::edit; + +/// Create the Rewards Edit Window +pub fn rewards_edit(channels: &Channels) -> Window { + let mut window = Window::new( + 100, + 100, + CONSTANTS.rewards_edit_window_width, + CONSTANTS.rewards_edit_window_height, + "Add a Reward", + ) + .center_screen(); + window.set_icon(Some(icon())); + window.make_modal(true); + + // 1. Coins + let _coins = edit::coins(channels); + + // 2. Reward + let _reward = edit::reward(channels); + + // 3. Buttons + let _buttons = edit::buttons(); + + window.end(); + + window.handle(handle); + + window +} + +fn handle(w: &mut T, ev: Event) -> bool { + match ev.bits() { + events::ADD_A_REWARD_OPEN => { + w.show(); + true + } + _ => false, + } +} From 5fb50ad4f9c9d2e9c717ea27f3285493c59d2b0d Mon Sep 17 00:00:00 2001 From: Pavel Sobolev Date: Sun, 18 Jul 2021 12:11:46 +0300 Subject: [PATCH 05/16] Update `fltk` to `1.1.1`; use new overloads for timeouts. --- Cargo.lock | 12 ++++++------ Cargo.toml | 2 +- src/ui/focus/timer.rs | 16 ++++------------ 3 files changed, 11 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ab7e490..8666aaf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,9 +10,9 @@ checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" [[package]] name = "cc" -version = "1.0.68" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a72c244c1ff497a746a7e1fb3d14bd08420ecda70c8f25c7112f2781652d787" +checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2" [[package]] name = "cfg-if" @@ -51,9 +51,9 @@ dependencies = [ [[package]] name = "fltk" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fd9596a1677232504499345ad56fbad899093cd281eaf371d41941c638dc753" +checksum = "1d364803b740d10140734eaa925af4ff4b3826e2bad29b18e477aeac3fdf016c" dependencies = [ "bitflags", "fltk-derive", @@ -75,9 +75,9 @@ dependencies = [ [[package]] name = "fltk-sys" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4d8037f9cea0c3ce53fe91c9b73950b559ae58c987647127450bae539014ca0" +checksum = "158abaf04e1d5cdc08bb75cd3683e03be6f6b6e2fa3a0124e1a596c436417bcb" dependencies = [ "cmake", "libc", diff --git a/Cargo.toml b/Cargo.toml index ba3362f..30bf07c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,5 +29,5 @@ codegen-units = 1 panic = 'abort' [dependencies] -fltk = { version = "~1.1.0", features = ["fltk-bundled", "use-ninja"] } +fltk = { version = "=1.1.1", features = ["fltk-bundled", "use-ninja"] } crossbeam-channel = {version = "~0.5.1"} diff --git a/src/ui/focus/timer.rs b/src/ui/focus/timer.rs index 8dbb5a9..cd752bd 100644 --- a/src/ui/focus/timer.rs +++ b/src/ui/focus/timer.rs @@ -6,9 +6,6 @@ use fltk::{ image::SvgImage, prelude::*, }; -use std::sync::atomic::{AtomicBool, Ordering}; - -static REPEAT_TICK: AtomicBool = AtomicBool::new(false); use crate::events; use crate::ui::logic; @@ -75,9 +72,7 @@ fn logic(timer: &mut T) { events::START_TIMER => { if !counting { counting = true; - REPEAT_TICK.store(true, Ordering::Release); - app::add_timeout(1.0, tick); - app::remove_timeout(tick); + app::add_timeout2(1.0, tick); } true } @@ -109,8 +104,7 @@ fn logic(timer: &mut T) { events::STOP_TIMER => { counting = false; seconds = 0; - REPEAT_TICK.store(false, Ordering::Release); - app::remove_timeout(tick); + app::remove_timeout2(tick); t.set_label("00:00:00"); true } @@ -141,8 +135,6 @@ fn stop(b: &mut T) { } fn tick() { - if REPEAT_TICK.load(Ordering::Acquire) { - app::handle_main(events::TICK).ok(); - app::repeat_timeout(1.0, tick); - } + app::handle_main(events::TICK).ok(); + app::repeat_timeout2(1.0, tick); } From 8bb6bb8f8c22bfaf6968ba94cb4040831e52fd7a Mon Sep 17 00:00:00 2001 From: Pavel Sobolev Date: Sun, 18 Jul 2021 13:45:55 +0300 Subject: [PATCH 06/16] Add tooltips to the buttons. --- src/main.rs | 6 +-- src/ui/app.rs | 7 +++- src/ui/focus/arrow.rs | 69 ++++++++++++++++++++-------------- src/ui/focus/coins.rs | 57 ++++++++++++++++------------ src/ui/focus/pane.rs | 15 ++++---- src/ui/focus/timer.rs | 32 ++++++++-------- src/ui/rates/list.rs | 12 +++--- src/ui/rates/menubar.rs | 56 +++++++++++++++++---------- src/ui/rates/pane.rs | 20 +++++----- src/ui/rewards/edit/buttons.rs | 10 ++--- src/ui/rewards/edit/cancel.rs | 6 +-- src/ui/rewards/edit/coins.rs | 28 +++++++------- src/ui/rewards/edit/reward.rs | 14 +++---- src/ui/rewards/list.rs | 16 ++++---- src/ui/rewards/menubar.rs | 56 +++++++++++++++++---------- src/ui/rewards/pane.rs | 18 ++++----- src/ui/widgets/list.rs | 4 +- src/ui/windows/main.rs | 20 +++++----- src/ui/windows/rewards_edit.rs | 18 ++++----- 19 files changed, 264 insertions(+), 200 deletions(-) diff --git a/src/main.rs b/src/main.rs index b1a4ee7..ee345e8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,12 +20,12 @@ fn main() { let app = ui::app::new(); // 1. Main Window - let _window = ui::windows::main(&channels); + let _w = ui::windows::main(&channels); // Hidden Windows // 1. Reward Edit Window - let rewards_edit = ui::windows::rewards_edit(&channels); + let re_w = ui::windows::rewards_edit(&channels); // Start the event loop while app.wait() { @@ -34,7 +34,7 @@ fn main() { app::handle_main(event).ok(); }; if let Ok(event) = channels.rewards_edit.r.try_recv() { - app::handle(event, &rewards_edit).ok(); + app::handle(event, &re_w).ok(); } } } diff --git a/src/ui/app.rs b/src/ui/app.rs index 736cc61..e3c021a 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -1,6 +1,7 @@ use fltk::{ app::{self, App}, - enums::FrameType, + enums::{Color, FrameType}, + misc::Tooltip, }; /// A struct providing access to the application's constants @@ -35,5 +36,9 @@ pub fn new() -> App { let app = App::default(); app::background(255, 255, 255); app::set_frame_type(FrameType::BorderBox); + + Tooltip::set_color(Color::BackGround); + Tooltip::set_hoverdelay(0.0); + app } diff --git a/src/ui/focus/arrow.rs b/src/ui/focus/arrow.rs index 20e80ab..c65927d 100644 --- a/src/ui/focus/arrow.rs +++ b/src/ui/focus/arrow.rs @@ -10,20 +10,20 @@ use crate::ui::app::CONSTANTS; use crate::ui::logic; pub fn arrow() -> Button { - let mut arrow = Button::default(); - arrow.set_frame(FrameType::FlatBox); + let mut a = Button::default(); + a.set_frame(FrameType::FlatBox); + a.set_tooltip("Show the Conversion Rates pane"); - draw_frame(&mut arrow); - arrow.draw(draw_frame); + draw_frame(&mut a); + a.draw(draw_frame); - let mut arrow_image = - SvgImage::from_data(include_str!("../../../assets/menu/arrow.svg")).unwrap(); - arrow_image.scale(48, 40, true, true); - arrow.set_image(Some(arrow_image)); + let mut ai = SvgImage::from_data(include_str!("../../../assets/menu/arrow.svg")).unwrap(); + ai.scale(48, 40, true, true); + a.set_image(Some(ai)); - logic(&mut arrow); + logic(&mut a); - arrow + a } fn draw_frame(f: &mut T) { @@ -43,34 +43,45 @@ fn logic(a: &mut T) { a.set_shortcut(Shortcut::from_char('r')); a.set_callback(|a| { if let Some(ref p) = a.parent() { - if let Some(ref mut w) = p.parent() { - if let Some(ref mut re_p) = w.child(1) { - if let Some(ref mut ra_p) = w.child(2) { - if re_p.visible() { - re_p.hide(); - ra_p.show(); - } else if ra_p.visible() { - w.resize(w.x(), w.y(), w.w(), 10 + CONSTANTS.focus_pane_height + 10); - ra_p.hide(); - } else { - w.resize(w.x(), w.y(), w.w(), CONSTANTS.main_window_height); - ra_p.show(); + if let Some(ref mut c) = p.child(1) { + if let Some(ref mut w) = p.parent() { + if let Some(ref mut re_p) = w.child(1) { + if let Some(ref mut ra_p) = w.child(2) { + if re_p.visible() { + re_p.hide(); + ra_p.show(); + a.set_tooltip("Hide the Conversion Rates pane"); + } else if ra_p.visible() { + w.resize( + w.x(), + w.y(), + w.w(), + 10 + CONSTANTS.focus_pane_height + 10, + ); + ra_p.hide(); + a.set_tooltip("Show the Conversion Rates pane"); + } else { + w.resize(w.x(), w.y(), w.w(), CONSTANTS.main_window_height); + ra_p.show(); + a.set_tooltip("Hide the Conversion Rates pane"); + } + c.set_tooltip("Show the Rewards pane"); } } } } } }); - a.handle(|a, ev| { - if ev == Event::KeyDown { - match app::event_key() { + a.handle({ + let unselect_box = FrameType::FlatBox; + move |a, ev| match ev { + Event::KeyDown => match app::event_key() { Key::Left => logic::fp_handle_left(a, 2), Key::Right => logic::fp_handle_right(a, 2), Key::Tab => logic::handle_tab(a), - _ => logic::handle_selection(a, ev, FrameType::FlatBox), - } - } else { - logic::handle_selection(a, ev, FrameType::FlatBox) + _ => logic::handle_selection(a, ev, unselect_box), + }, + _ => logic::handle_selection(a, ev, unselect_box), } }); } diff --git a/src/ui/focus/coins.rs b/src/ui/focus/coins.rs index 4755645..1469fbe 100644 --- a/src/ui/focus/coins.rs +++ b/src/ui/focus/coins.rs @@ -11,16 +11,17 @@ use crate::ui::app::CONSTANTS; use crate::ui::logic; pub fn coins() -> Button { - let mut coins = Button::default().with_label("99999"); - coins.set_label_type(LabelType::None); - coins.set_frame(FrameType::FlatBox); + let mut c = Button::default().with_label("99999"); + c.set_label_type(LabelType::None); + c.set_frame(FrameType::FlatBox); + c.set_tooltip("Hide the Rewards pane"); - draw_button(&mut coins); - coins.draw(draw); + draw_button(&mut c); + c.draw(draw); - logic(&mut coins); + logic(&mut c); - coins + c } fn draw(b: &mut T) { @@ -57,28 +58,38 @@ fn draw_label(b: &mut T) { } fn draw_image(b: &mut T) { - let mut coins_image = - SvgImage::from_data(include_str!("../../../assets/menu/coins.svg")).unwrap(); - coins_image.scale(24, 24, true, true); - coins_image.draw(b.x() + 6, b.y() + 7, 24, 24); + let mut ci = SvgImage::from_data(include_str!("../../../assets/menu/coins.svg")).unwrap(); + ci.scale(24, 24, true, true); + ci.draw(b.x() + 6, b.y() + 7, 24, 24); } fn logic(c: &mut T) { c.set_shortcut(Shortcut::from_char('c')); c.set_callback(|c| { if let Some(ref p) = c.parent() { - if let Some(ref mut w) = p.parent() { - if let Some(ref mut re_p) = w.child(1) { - if let Some(ref mut ra_p) = w.child(2) { - if ra_p.visible() { - ra_p.hide(); - re_p.show(); - } else if re_p.visible() { - w.resize(w.x(), w.y(), w.w(), 10 + CONSTANTS.focus_pane_height + 10); - re_p.hide(); - } else { - w.resize(w.x(), w.y(), w.w(), CONSTANTS.main_window_height); - re_p.show(); + if let Some(ref mut a) = p.child(2) { + if let Some(ref mut w) = p.parent() { + if let Some(ref mut re_p) = w.child(1) { + if let Some(ref mut ra_p) = w.child(2) { + if ra_p.visible() { + ra_p.hide(); + re_p.show(); + c.set_tooltip("Hide the Rewards pane"); + } else if re_p.visible() { + w.resize( + w.x(), + w.y(), + w.w(), + 10 + CONSTANTS.focus_pane_height + 10, + ); + re_p.hide(); + c.set_tooltip("Show the Rewards pane"); + } else { + w.resize(w.x(), w.y(), w.w(), CONSTANTS.main_window_height); + re_p.show(); + c.set_tooltip("Hide the Rewards pane"); + } + a.set_tooltip("Show the Conversion Rates pane"); } } } diff --git a/src/ui/focus/pane.rs b/src/ui/focus/pane.rs index 05ae164..871bd10 100644 --- a/src/ui/focus/pane.rs +++ b/src/ui/focus/pane.rs @@ -9,24 +9,23 @@ use super::{arrow, coins, timer}; use crate::ui::app::CONSTANTS; pub fn pane() -> Group { - let mut pane = Group::default().with_pos(10, 10); - pane.set_frame(FrameType::BorderBox); + let mut p = Group::default().with_pos(10, 10); + p.set_frame(FrameType::BorderBox); - if let Some(ref parent) = pane.parent() { - pane.set_size(parent.width() - 20, CONSTANTS.focus_pane_height); + if let Some(ref parent) = p.parent() { + p.set_size(parent.width() - 20, CONSTANTS.focus_pane_height); } // The order of the widgets is important - let _timer = timer(); let _coins = coins(); let _arrow = arrow(); - pane.end(); + p.end(); - pane.handle(handle); + p.handle(handle); - pane + p } fn handle(p: &mut T, ev: Event) -> bool { diff --git a/src/ui/focus/timer.rs b/src/ui/focus/timer.rs index cd752bd..f0e0faf 100644 --- a/src/ui/focus/timer.rs +++ b/src/ui/focus/timer.rs @@ -11,16 +11,17 @@ use crate::events; use crate::ui::logic; pub fn timer() -> Button { - let mut timer = Button::default().with_label("00:00:00"); - timer.set_label_type(LabelType::None); - timer.set_frame(FrameType::FlatBox); + let mut t = Button::default().with_label("00:00:00"); + t.set_label_type(LabelType::None); + t.set_frame(FrameType::FlatBox); + t.set_tooltip("Start the timer"); - draw_button(&mut timer); - timer.draw(draw); + draw_button(&mut t); + t.draw(draw); - logic(&mut timer); + logic(&mut t); - timer + t } fn draw(b: &mut T) { @@ -56,16 +57,15 @@ fn draw_label(b: &mut T) { } fn draw_image(b: &mut T) { - let mut coins_image = - SvgImage::from_data(include_str!("../../../assets/menu/sand-clock.svg")).unwrap(); - coins_image.scale(24, 24, true, true); - coins_image.draw(b.x() + 6, b.y() + 7, 24, 24); + let mut ti = SvgImage::from_data(include_str!("../../../assets/menu/sand-clock.svg")).unwrap(); + ti.scale(24, 24, true, true); + ti.draw(b.x() + 6, b.y() + 7, 24, 24); } -fn logic(timer: &mut T) { - timer.set_shortcut(Shortcut::from_char('f')); - timer.set_callback(start); - timer.handle({ +fn logic(t: &mut T) { + t.set_shortcut(Shortcut::from_char('f')); + t.set_callback(start); + t.handle({ let mut counting = false; let mut seconds = 0; move |t, ev| match ev.bits() { @@ -126,11 +126,13 @@ fn logic(timer: &mut T) { fn start(b: &mut T) { app::handle_main(events::START_TIMER).ok(); + b.set_tooltip("Stop the timer"); b.set_callback(stop); } fn stop(b: &mut T) { app::handle_main(events::STOP_TIMER).ok(); + b.set_tooltip("Start the timer"); b.set_callback(start); } diff --git a/src/ui/rates/list.rs b/src/ui/rates/list.rs index 85614fe..aba1724 100644 --- a/src/ui/rates/list.rs +++ b/src/ui/rates/list.rs @@ -3,14 +3,14 @@ use fltk::prelude::*; use crate::ui::widgets::List; pub fn list() -> List { - let mut list = List::default(); + let mut l = List::default(); - if let Some(ref p) = list.parent() { - list.set_size(0, p.h() - 20); + if let Some(ref p) = l.parent() { + l.set_size(0, p.h() - 20); } - list.add("5/m"); - list.add("0.1/s"); + l.add("5/m"); + l.add("0.1/s"); - list + l } diff --git a/src/ui/rates/menubar.rs b/src/ui/rates/menubar.rs index 18f05dd..9b982c5 100644 --- a/src/ui/rates/menubar.rs +++ b/src/ui/rates/menubar.rs @@ -11,30 +11,20 @@ use crate::ui::app::CONSTANTS; use crate::ui::logic; pub fn menubar() -> Pack { - let mut menubar = Pack::default().with_size(0, CONSTANTS.rewards_menubar_height); - menubar.set_type(PackType::Horizontal); + let mut m = Pack::default().with_size(0, CONSTANTS.rewards_menubar_height); + m.set_type(PackType::Horizontal); - let mut add_image = SvgImage::from_data(include_str!("../../../assets/menu/plus.svg")).unwrap(); - add_image.scale(24, 24, true, true); + // 1. Add a Rate + let _ab = add_button(); - let mut add_button = Button::default().with_size(CONSTANTS.rewards_menubar_height, 0); - add_button.set_image(Some(add_image)); - add_button.set_frame(FrameType::FlatBox); + // 2. Delete the Rate + let _db = delete_button(); - let mut delete_image = - SvgImage::from_data(include_str!("../../../assets/menu/minus.svg")).unwrap(); - delete_image.scale(24, 24, true, true); + m.end(); - let mut delete_button = Button::default().with_size(CONSTANTS.rewards_menubar_height, 0); - delete_button.set_image(Some(delete_image)); - delete_button.set_frame(FrameType::FlatBox); + menubar_logic(&mut m); - menubar_logic(&mut menubar); - add_button_logic(&mut add_button); - delete_button_logic(&mut delete_button); - - menubar.end(); - menubar + m } fn menubar_logic(m: &mut T) { @@ -55,6 +45,20 @@ fn menubar_logic(m: &mut T) { }); } +fn add_button() -> Button { + let mut ai = SvgImage::from_data(include_str!("../../../assets/menu/plus.svg")).unwrap(); + ai.scale(24, 24, true, true); + + let mut ab = Button::default().with_size(CONSTANTS.rewards_menubar_height, 0); + ab.set_image(Some(ai)); + ab.set_frame(FrameType::FlatBox); + ab.set_tooltip("Add a Rate"); + + add_button_logic(&mut ab); + + ab +} + fn add_button_logic(ab: &mut T) { ab.set_shortcut(Shortcut::from_char('a')); ab.set_callback(|_| { @@ -73,6 +77,20 @@ fn add_button_logic(ab: &mut T) { }); } +fn delete_button() -> Button { + let mut di = SvgImage::from_data(include_str!("../../../assets/menu/minus.svg")).unwrap(); + di.scale(24, 24, true, true); + + let mut db = Button::default().with_size(CONSTANTS.rewards_menubar_height, 0); + db.set_image(Some(di)); + db.set_frame(FrameType::FlatBox); + db.set_tooltip("Delete the Rate"); + + delete_button_logic(&mut db); + + db +} + fn delete_button_logic(db: &mut T) { db.set_shortcut(Shortcut::from_char('d')); db.set_callback(|_| { diff --git a/src/ui/rates/pane.rs b/src/ui/rates/pane.rs index 850f9af..0943d17 100644 --- a/src/ui/rates/pane.rs +++ b/src/ui/rates/pane.rs @@ -4,21 +4,21 @@ use super::{list, menubar}; use crate::ui::app::CONSTANTS; pub fn pane() -> Pack { - let mut pane = Pack::default().with_pos(10, 10 + CONSTANTS.focus_pane_height + 10); - pane.set_spacing(1); + let mut p = Pack::default().with_pos(10, 10 + CONSTANTS.focus_pane_height + 10); + p.set_spacing(1); - if let Some(ref parent) = pane.parent() { - pane.set_size( - parent.width() - 20, - parent.height() - CONSTANTS.focus_pane_height - CONSTANTS.rewards_menubar_height, + if let Some(ref w) = p.parent() { + p.set_size( + w.width() - 20, + w.height() - CONSTANTS.focus_pane_height - CONSTANTS.rewards_menubar_height, ); } let _menubar = menubar(); let list = list(); - pane.end(); - pane.resizable(list.scroll()); - pane.hide(); - pane + p.end(); + p.resizable(list.scroll()); + p.hide(); + p } diff --git a/src/ui/rewards/edit/buttons.rs b/src/ui/rewards/edit/buttons.rs index 08463b3..6df2724 100644 --- a/src/ui/rewards/edit/buttons.rs +++ b/src/ui/rewards/edit/buttons.rs @@ -7,18 +7,18 @@ use super::{cancel, ok}; use crate::ui::app::CONSTANTS; pub fn buttons() -> Pack { - let mut buttons = Pack::default() + let mut bs = Pack::default() .with_pos( CONSTANTS.rewards_edit_window_width - 10 - 2 * 80 - 10, CONSTANTS.rewards_edit_window_height - 20 - 15, ) .with_size(2 * 80, 25); - buttons.set_spacing(10); - buttons.set_type(PackType::Horizontal); + bs.set_spacing(10); + bs.set_type(PackType::Horizontal); let _cancel = cancel(); let _ok = ok(); - buttons.end(); - buttons + bs.end(); + bs } diff --git a/src/ui/rewards/edit/cancel.rs b/src/ui/rewards/edit/cancel.rs index 7eb21a3..75ec682 100644 --- a/src/ui/rewards/edit/cancel.rs +++ b/src/ui/rewards/edit/cancel.rs @@ -4,11 +4,11 @@ use crate::events; use crate::ui::logic; pub fn cancel() -> Button { - let mut cancel = Button::default().with_size(80, 0).with_label("Cancel"); + let mut c = Button::default().with_size(80, 0).with_label("Cancel"); - logic(&mut cancel); + logic(&mut c); - cancel + c } fn logic(b: &mut T) { diff --git a/src/ui/rewards/edit/coins.rs b/src/ui/rewards/edit/coins.rs index ba91205..df61553 100644 --- a/src/ui/rewards/edit/coins.rs +++ b/src/ui/rewards/edit/coins.rs @@ -10,28 +10,28 @@ use crate::events; use crate::ui::app::CONSTANTS; pub fn coins(channels: &Channels) -> ValueInput { - let mut coins = ValueInput::new( + let mut c = ValueInput::new( 10, 25, CONSTANTS.rewards_edit_window_width - 20, 20, "Coins:", ); - coins.set_frame(FrameType::BorderBox); - coins.set_align(Align::TopLeft); - coins.set_label_size(16); - coins.set_text_size(16); - coins.set_step(0.0, 5); - coins.set_precision(2); - coins.set_bounds(0.0, 999_999_999.0); - coins.set_range(0.0, 999_999_999.0); - coins.set_soft(false); - coins.set_value(0.1); - coins.set_value(0.0); + c.set_frame(FrameType::BorderBox); + c.set_align(Align::TopLeft); + c.set_label_size(16); + c.set_text_size(16); + c.set_step(0.0, 5); + c.set_precision(2); + c.set_bounds(0.0, 999_999_999.0); + c.set_range(0.0, 999_999_999.0); + c.set_soft(false); + c.set_value(0.1); + c.set_value(0.0); - logic(&mut coins, channels); + logic(&mut c, channels); - coins + c } fn logic(v: &mut T, channels: &Channels) { diff --git a/src/ui/rewards/edit/reward.rs b/src/ui/rewards/edit/reward.rs index 43af225..678cf0b 100644 --- a/src/ui/rewards/edit/reward.rs +++ b/src/ui/rewards/edit/reward.rs @@ -9,21 +9,21 @@ use crate::events; use crate::ui::app::CONSTANTS; pub fn reward(channels: &Channels) -> Input { - let mut reward = Input::new( + let mut r = Input::new( 10, 70, CONSTANTS.rewards_edit_window_width - 20, 20, "Reward:", ); - reward.set_frame(FrameType::BorderBox); - reward.set_align(Align::TopLeft); - reward.set_label_size(16); - reward.set_text_size(16); + r.set_frame(FrameType::BorderBox); + r.set_align(Align::TopLeft); + r.set_label_size(16); + r.set_text_size(16); - logic(&mut reward, channels); + logic(&mut r, channels); - reward + r } fn logic(i: &mut T, channels: &Channels) { diff --git a/src/ui/rewards/list.rs b/src/ui/rewards/list.rs index add2682..0b91b77 100644 --- a/src/ui/rewards/list.rs +++ b/src/ui/rewards/list.rs @@ -5,19 +5,19 @@ use crate::events; use crate::ui::widgets::{List, ScrollExt}; pub fn list(channels: &Channels) -> List { - let mut list = List::default(); + let mut l = List::default(); - if let Some(ref p) = list.parent() { - list.set_size(0, p.h() - 20); + if let Some(ref p) = l.parent() { + l.set_size(0, p.h() - 20); } - list.add("(15) A reward."); - list.add("(10) Another reward."); - list.add("(5) One more reward."); + l.add("(15) A reward."); + l.add("(10) Another reward."); + l.add("(5) One more reward."); - logic(&mut list, channels); + logic(&mut l, channels); - list + l } fn logic(l: &mut List, channels: &Channels) { diff --git a/src/ui/rewards/menubar.rs b/src/ui/rewards/menubar.rs index edce157..8b19a9e 100644 --- a/src/ui/rewards/menubar.rs +++ b/src/ui/rewards/menubar.rs @@ -13,30 +13,20 @@ use crate::ui::app::CONSTANTS; use crate::ui::logic; pub fn menubar(channels: &Channels) -> Pack { - let mut menubar = Pack::default().with_size(0, CONSTANTS.rewards_menubar_height); - menubar.set_type(PackType::Horizontal); + let mut m = Pack::default().with_size(0, CONSTANTS.rewards_menubar_height); + m.set_type(PackType::Horizontal); - let mut add_image = SvgImage::from_data(include_str!("../../../assets/menu/plus.svg")).unwrap(); - add_image.scale(24, 24, true, true); + // 1. Add a Reward + let _ab = add_button(channels); - let mut add_button = Button::default().with_size(CONSTANTS.rewards_menubar_height, 0); - add_button.set_image(Some(add_image)); - add_button.set_frame(FrameType::FlatBox); + // 2. Delete the Reward + let _db = delete_button(); - let mut delete_image = - SvgImage::from_data(include_str!("../../../assets/menu/minus.svg")).unwrap(); - delete_image.scale(24, 24, true, true); + m.end(); - let mut delete_button = Button::default().with_size(CONSTANTS.rewards_menubar_height, 0); - delete_button.set_image(Some(delete_image)); - delete_button.set_frame(FrameType::FlatBox); + menubar_logic(&mut m); - menubar_logic(&mut menubar); - add_button_logic(&mut add_button, channels); - delete_button_logic(&mut delete_button); - - menubar.end(); - menubar + m } fn menubar_logic(m: &mut T) { @@ -57,6 +47,20 @@ fn menubar_logic(m: &mut T) { }); } +fn add_button(channels: &Channels) -> Button { + let mut ai = SvgImage::from_data(include_str!("../../../assets/menu/plus.svg")).unwrap(); + ai.scale(24, 24, true, true); + + let mut ab = Button::default().with_size(CONSTANTS.rewards_menubar_height, 0); + ab.set_image(Some(ai)); + ab.set_frame(FrameType::FlatBox); + ab.set_tooltip("Add a Reward"); + + add_button_logic(&mut ab, channels); + + ab +} + fn add_button_logic(ab: &mut T, channels: &Channels) { ab.set_shortcut(Shortcut::from_char('a')); ab.set_callback({ @@ -78,6 +82,20 @@ fn add_button_logic(ab: &mut T, channels: & }); } +fn delete_button() -> Button { + let mut di = SvgImage::from_data(include_str!("../../../assets/menu/minus.svg")).unwrap(); + di.scale(24, 24, true, true); + + let mut db = Button::default().with_size(CONSTANTS.rewards_menubar_height, 0); + db.set_image(Some(di)); + db.set_frame(FrameType::FlatBox); + db.set_tooltip("Delete the Reward"); + + delete_button_logic(&mut db); + + db +} + fn delete_button_logic(db: &mut T) { db.set_shortcut(Shortcut::from_char('d')); db.set_callback(|_| { diff --git a/src/ui/rewards/pane.rs b/src/ui/rewards/pane.rs index 9bab3b4..cfc55a3 100644 --- a/src/ui/rewards/pane.rs +++ b/src/ui/rewards/pane.rs @@ -5,20 +5,20 @@ use crate::channels::Channels; use crate::ui::app::CONSTANTS; pub fn pane(channels: &Channels) -> Pack { - let mut pane = Pack::default().with_pos(10, 10 + CONSTANTS.focus_pane_height + 10); - pane.set_spacing(1); + let mut p = Pack::default().with_pos(10, 10 + CONSTANTS.focus_pane_height + 10); + p.set_spacing(1); - if let Some(ref parent) = pane.parent() { - pane.set_size( - parent.width() - 20, - parent.height() - CONSTANTS.focus_pane_height - CONSTANTS.rewards_menubar_height, + if let Some(ref w) = p.parent() { + p.set_size( + w.width() - 20, + w.height() - CONSTANTS.focus_pane_height - CONSTANTS.rewards_menubar_height, ); } let _menubar = menubar(channels); let list = list(channels); - pane.resizable(list.scroll()); - pane.end(); - pane + p.resizable(list.scroll()); + p.end(); + p } diff --git a/src/ui/widgets/list.rs b/src/ui/widgets/list.rs index 0509ea1..8df0744 100644 --- a/src/ui/widgets/list.rs +++ b/src/ui/widgets/list.rs @@ -123,7 +123,7 @@ impl List { move |s, ev| match ev { Event::Focus => true, Event::Push => handle_push(s, &selected, &items), - Event::KeyDown => handle_keydown(s, &selected, &items), + Event::KeyDown => handle_key_down(s, &selected, &items), _ => handle_custom_events(s, &mut *items.borrow_mut(), ev.bits()), } }); @@ -178,7 +178,7 @@ fn handle_push(s: &mut Scroll, selected: &Selected, items: &Items) -> bool { true } -fn handle_keydown(s: &mut Scroll, selected: &Selected, items: &Items) -> bool { +fn handle_key_down(s: &mut Scroll, selected: &Selected, items: &Items) -> bool { match app::event_key() { Key::Up => { if *selected.borrow() == 1 { diff --git a/src/ui/windows/main.rs b/src/ui/windows/main.rs index 2ecad78..4cf1c89 100644 --- a/src/ui/windows/main.rs +++ b/src/ui/windows/main.rs @@ -9,33 +9,33 @@ use crate::ui::rewards; /// Create the Main Window pub fn main(channels: &Channels) -> Window { - let mut window = Window::new( + let mut w = Window::new( 100, 100, CONSTANTS.main_window_width, CONSTANTS.main_window_height, "Shuchu", ); - window.size_range( + w.size_range( CONSTANTS.main_window_width, CONSTANTS.main_window_height, CONSTANTS.main_window_width, CONSTANTS.main_window_height + 100, ); - window.set_icon(Some(icon())); + w.set_icon(Some(icon())); // 1. Focus Pane - let _focus_pane = focus::pane(); + let _fp = focus::pane(); // 2. Rewards Pane - let rewards_pane = rewards::pane(channels); + let re_p = rewards::pane(channels); // 3. Conversion Rates Pane - let _rates_pane = rates::pane(); + let _ra_p = rates::pane(); - window.resizable(&rewards_pane); - window.end(); + w.resizable(&re_p); + w.end(); - window.show(); - window + w.show(); + w } diff --git a/src/ui/windows/rewards_edit.rs b/src/ui/windows/rewards_edit.rs index 9b38771..7e65a77 100644 --- a/src/ui/windows/rewards_edit.rs +++ b/src/ui/windows/rewards_edit.rs @@ -8,7 +8,7 @@ use crate::ui::rewards::edit; /// Create the Rewards Edit Window pub fn rewards_edit(channels: &Channels) -> Window { - let mut window = Window::new( + let mut w = Window::new( 100, 100, CONSTANTS.rewards_edit_window_width, @@ -16,23 +16,23 @@ pub fn rewards_edit(channels: &Channels) -> Window { "Add a Reward", ) .center_screen(); - window.set_icon(Some(icon())); - window.make_modal(true); + w.set_icon(Some(icon())); + w.make_modal(true); // 1. Coins - let _coins = edit::coins(channels); + let _c = edit::coins(channels); // 2. Reward - let _reward = edit::reward(channels); + let _r = edit::reward(channels); // 3. Buttons - let _buttons = edit::buttons(); + let _bs = edit::buttons(); - window.end(); + w.end(); - window.handle(handle); + w.handle(handle); - window + w } fn handle(w: &mut T, ev: Event) -> bool { From 773433bf0d4d0c41b56d34e05344bdc1503ef44e Mon Sep 17 00:00:00 2001 From: Pavel Sobolev Date: Mon, 19 Jul 2021 18:01:04 +0300 Subject: [PATCH 07/16] Add the Edit and Spend buttons. --- assets/menu/arrow.svg | 4 +- assets/menu/divider.svg | 7 ++ assets/menu/insert-coin.svg | 3 + assets/menu/pencil.svg | 4 ++ src/channels.rs | 12 +++- src/events.rs | 15 ++++- src/ui/focus/arrow.rs | 26 +++++--- src/ui/focus/coins.rs | 47 +++++++++----- src/ui/focus/pane.rs | 5 +- src/ui/focus/timer.rs | 18 ++---- src/ui/rates/list.rs | 1 + src/ui/rewards/edit/cancel.rs | 5 +- src/ui/rewards/edit/coins.rs | 35 ++++++---- src/ui/rewards/edit/ok.rs | 27 +++++--- src/ui/rewards/edit/reward.rs | 45 +++++++++---- src/ui/rewards/list.rs | 66 +++++++++++++++++-- src/ui/rewards/menubar.rs | 115 +++++++++++++++++++++++++++------ src/ui/widgets/list.rs | 31 ++++++--- src/ui/windows/main.rs | 2 +- src/ui/windows/rewards_edit.rs | 38 ++++++++--- 20 files changed, 382 insertions(+), 124 deletions(-) create mode 100644 assets/menu/divider.svg create mode 100644 assets/menu/insert-coin.svg create mode 100644 assets/menu/pencil.svg diff --git a/assets/menu/arrow.svg b/assets/menu/arrow.svg index 92db234..db3b9d1 100644 --- a/assets/menu/arrow.svg +++ b/assets/menu/arrow.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/menu/divider.svg b/assets/menu/divider.svg new file mode 100644 index 0000000..7886532 --- /dev/null +++ b/assets/menu/divider.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/menu/insert-coin.svg b/assets/menu/insert-coin.svg new file mode 100644 index 0000000..6b34f07 --- /dev/null +++ b/assets/menu/insert-coin.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/menu/pencil.svg b/assets/menu/pencil.svg new file mode 100644 index 0000000..c9b31df --- /dev/null +++ b/assets/menu/pencil.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/channels.rs b/src/channels.rs index 1c66f20..ec55eb2 100644 --- a/src/channels.rs +++ b/src/channels.rs @@ -51,8 +51,14 @@ channels_default_impl! { /// Channel 2: From Main Window to Rewards Edit Window pub rewards_edit: Channel, /// Channel 3: Send Coins To Reward in the Rewards Edit Window - pub rewards_coins: Channel, - /// Channel 3: Send Reward (including Coins) from the Rewards Edit Window to Rewards - pub rewards_reward: Channel, + pub rewards_send_coins: Channel, + /// Channel 4: Send an Item from the Rewards Edit Window to Rewards + pub rewards_send_item: Channel, + /// Channel 5: Receive an Item from the Rewards + pub rewards_receive_item: Channel, + /// Channel 6: Receive Coins from an Item + pub rewards_receive_coins: Channel, + /// Channel 7: Receive a Reward from an Item + pub rewards_receive_reward: Channel, } } diff --git a/src/events.rs b/src/events.rs index 65b56cc..b03ef0d 100644 --- a/src/events.rs +++ b/src/events.rs @@ -18,10 +18,21 @@ events!( START_TIMER, TICK, STOP_TIMER, + OK_BUTTON_SET_TO_ADD, ADD_A_REWARD_OPEN, ADD_A_REWARD_SEND_COINS, ADD_A_REWARD_SEND_REWARD, ADD_A_REWARD_RECEIVE, - ADD_A_REWARD_RESET_COINS, - ADD_A_REWARD_RESET_REWARD + OK_BUTTON_SET_TO_EDIT, + EDIT_A_REWARD_SEND_ITEM, + EDIT_A_REWARD_OPEN, + EDIT_A_REWARD_RECEIVE_COINS, + EDIT_A_REWARD_RECEIVE_REWARD, + EDIT_A_REWARD_SEND_COINS, + EDIT_A_REWARD_SEND_REWARD, + EDIT_A_REWARD_RECEIVE, + DELETE_A_REWARD, + SPEND_COINS_SEND_TOTAL, + SPEND_COINS_RECEIVE_TOTAL, + SPEND_COINS_RECEIVE_DIFF ); diff --git a/src/ui/focus/arrow.rs b/src/ui/focus/arrow.rs index c65927d..82d04ce 100644 --- a/src/ui/focus/arrow.rs +++ b/src/ui/focus/arrow.rs @@ -1,6 +1,7 @@ use fltk::{ app, button::Button, + draw, enums::{Event, FrameType, Key, Shortcut}, image::SvgImage, prelude::*, @@ -14,24 +15,27 @@ pub fn arrow() -> Button { a.set_frame(FrameType::FlatBox); a.set_tooltip("Show the Conversion Rates pane"); - draw_frame(&mut a); - a.draw(draw_frame); - - let mut ai = SvgImage::from_data(include_str!("../../../assets/menu/arrow.svg")).unwrap(); - ai.scale(48, 40, true, true); - a.set_image(Some(ai)); + resize_frame(&mut a); + a.draw(draw); logic(&mut a); a } -fn draw_frame(f: &mut T) { +fn draw(f: &mut T) { + resize_frame(f); + draw::push_clip(f.x(), f.y(), f.w(), f.h()); + draw_image(f); + draw::pop_clip(); +} + +fn resize_frame(f: &mut T) { if let Some(ref p) = f.parent() { if let Some(ref lw) = p.child(0) { if let Some(ref rw) = p.child(1) { let lx = lw.x() + lw.w(); - let w = 48 + 15; + let w = 48 + 14; f.set_pos(lx + (rw.x() - lx - w) / 2, p.y() + 15); f.set_size(w, 30); } @@ -39,6 +43,12 @@ fn draw_frame(f: &mut T) { } } +fn draw_image(f: &mut T) { + let mut ai = SvgImage::from_data(include_str!("../../../assets/menu/arrow.svg")).unwrap(); + ai.scale(f.w() - 14, f.h(), true, true); + ai.draw(f.x() + 7, f.y(), f.w() - 14, f.h()); +} + fn logic(a: &mut T) { a.set_shortcut(Shortcut::from_char('r')); a.set_callback(|a| { diff --git a/src/ui/focus/coins.rs b/src/ui/focus/coins.rs index 1469fbe..ba31a31 100644 --- a/src/ui/focus/coins.rs +++ b/src/ui/focus/coins.rs @@ -7,32 +7,34 @@ use fltk::{ prelude::*, }; +use crate::channels::Channels; +use crate::events; use crate::ui::app::CONSTANTS; use crate::ui::logic; -pub fn coins() -> Button { +pub fn coins(channels: &Channels) -> Button { let mut c = Button::default().with_label("99999"); c.set_label_type(LabelType::None); c.set_frame(FrameType::FlatBox); c.set_tooltip("Hide the Rewards pane"); - draw_button(&mut c); + resize_button(&mut c); c.draw(draw); - logic(&mut c); + logic(&mut c, channels); c } fn draw(b: &mut T) { + resize_button(b); draw::push_clip(b.x(), b.y(), b.w(), b.h()); - draw_button(b); draw_label(b); draw_image(b); draw::pop_clip(); } -fn draw_button(b: &mut T) { +fn resize_button(b: &mut T) { let (lw, _) = b.measure_label(); if let Some(ref p) = b.parent() { b.set_pos(p.x() + p.w() - lw - 60, p.y() + 10); @@ -47,8 +49,8 @@ fn draw_label(b: &mut T) { draw::set_draw_color(Color::Black); draw::draw_text2( &b.label(), - b.x() + 30, - b.y(), + b.x() + 33, + b.y() + 1, b.w() - 40, b.h(), Align::Right, @@ -60,10 +62,10 @@ fn draw_label(b: &mut T) { fn draw_image(b: &mut T) { let mut ci = SvgImage::from_data(include_str!("../../../assets/menu/coins.svg")).unwrap(); ci.scale(24, 24, true, true); - ci.draw(b.x() + 6, b.y() + 7, 24, 24); + ci.draw(b.x() + 6, b.y() + 8, 24, 24); } -fn logic(c: &mut T) { +fn logic(c: &mut T, channels: &Channels) { c.set_shortcut(Shortcut::from_char('c')); c.set_callback(|c| { if let Some(ref p) = c.parent() { @@ -96,16 +98,31 @@ fn logic(c: &mut T) { } } }); - c.handle(|c, ev| { - if ev == Event::KeyDown { - match app::event_key() { + c.handle({ + let s_coins = channels.rewards_send_coins.s.clone(); + let r_coins = channels.rewards_receive_coins.r.clone(); + move |c, ev| match ev { + Event::KeyDown => match app::event_key() { Key::Left => logic::fp_handle_left(c, 1), Key::Right => logic::fp_handle_right(c, 1), Key::Tab => logic::handle_tab(c), _ => logic::handle_selection(c, ev, FrameType::FlatBox), - } - } else { - logic::handle_selection(c, ev, FrameType::FlatBox) + }, + _ => match ev.bits() { + events::SPEND_COINS_SEND_TOTAL => c.label().parse::().map_or(false, |coins| { + s_coins.try_send(coins).ok(); + app::handle_main(events::SPEND_COINS_RECEIVE_TOTAL).ok(); + true + }), + events::SPEND_COINS_RECEIVE_DIFF => r_coins.try_recv().map_or(false, |coins| { + c.set_label(&coins.to_string()); + if let Some(ref mut p) = c.parent() { + p.redraw(); + } + true + }), + _ => logic::handle_selection(c, ev, FrameType::FlatBox), + }, } }); } diff --git a/src/ui/focus/pane.rs b/src/ui/focus/pane.rs index 871bd10..37f65f3 100644 --- a/src/ui/focus/pane.rs +++ b/src/ui/focus/pane.rs @@ -6,9 +6,10 @@ use fltk::{ }; use super::{arrow, coins, timer}; +use crate::channels::Channels; use crate::ui::app::CONSTANTS; -pub fn pane() -> Group { +pub fn pane(channels: &Channels) -> Group { let mut p = Group::default().with_pos(10, 10); p.set_frame(FrameType::BorderBox); @@ -18,7 +19,7 @@ pub fn pane() -> Group { // The order of the widgets is important let _timer = timer(); - let _coins = coins(); + let _coins = coins(channels); let _arrow = arrow(); p.end(); diff --git a/src/ui/focus/timer.rs b/src/ui/focus/timer.rs index f0e0faf..34a16e4 100644 --- a/src/ui/focus/timer.rs +++ b/src/ui/focus/timer.rs @@ -16,7 +16,11 @@ pub fn timer() -> Button { t.set_frame(FrameType::FlatBox); t.set_tooltip("Start the timer"); - draw_button(&mut t); + if let Some(ref p) = t.parent() { + t.set_pos(p.x() + 10, p.y() + 10); + t.set_size(110, p.h() - 20); + } + t.draw(draw); logic(&mut t); @@ -26,19 +30,11 @@ pub fn timer() -> Button { fn draw(b: &mut T) { draw::push_clip(b.x(), b.y(), b.w(), b.h()); - draw_button(b); draw_label(b); draw_image(b); draw::pop_clip(); } -fn draw_button(b: &mut T) { - if let Some(ref p) = b.parent() { - b.set_pos(p.x() + 10, p.y() + 10); - b.set_size(110, p.h() - 20); - } -} - fn draw_label(b: &mut T) { let color = draw::get_color(); @@ -47,7 +43,7 @@ fn draw_label(b: &mut T) { draw::draw_text2( &b.label(), b.x() + 32, - b.y(), + b.y() + 1, b.w() - 40, b.h(), Align::Right, @@ -59,7 +55,7 @@ fn draw_label(b: &mut T) { fn draw_image(b: &mut T) { let mut ti = SvgImage::from_data(include_str!("../../../assets/menu/sand-clock.svg")).unwrap(); ti.scale(24, 24, true, true); - ti.draw(b.x() + 6, b.y() + 7, 24, 24); + ti.draw(b.x() + 6, b.y() + 8, 24, 24); } fn logic(t: &mut T) { diff --git a/src/ui/rates/list.rs b/src/ui/rates/list.rs index aba1724..2e9dce9 100644 --- a/src/ui/rates/list.rs +++ b/src/ui/rates/list.rs @@ -11,6 +11,7 @@ pub fn list() -> List { l.add("5/m"); l.add("0.1/s"); + l.select(1); l } diff --git a/src/ui/rewards/edit/cancel.rs b/src/ui/rewards/edit/cancel.rs index 75ec682..be4190d 100644 --- a/src/ui/rewards/edit/cancel.rs +++ b/src/ui/rewards/edit/cancel.rs @@ -1,6 +1,5 @@ -use fltk::{app, button::Button, enums::FrameType, prelude::*}; +use fltk::{button::Button, enums::FrameType, prelude::*}; -use crate::events; use crate::ui::logic; pub fn cancel() -> Button { @@ -13,8 +12,6 @@ pub fn cancel() -> Button { fn logic(b: &mut T) { b.set_callback(|b| { - app::handle_main(events::ADD_A_REWARD_RESET_COINS).ok(); - app::handle_main(events::ADD_A_REWARD_RESET_REWARD).ok(); if let Some(ref p) = b.parent() { if let Some(ref mut w) = p.parent() { w.hide(); diff --git a/src/ui/rewards/edit/coins.rs b/src/ui/rewards/edit/coins.rs index df61553..0128efb 100644 --- a/src/ui/rewards/edit/coins.rs +++ b/src/ui/rewards/edit/coins.rs @@ -1,6 +1,6 @@ use fltk::{ app, - enums::{Align, CallbackTrigger, FrameType}, + enums::{Align, CallbackTrigger, Event, FrameType}, prelude::*, valuator::ValueInput, }; @@ -40,19 +40,32 @@ fn logic(v: &mut T, channels: &Channels) { v.set_value(v.clamp(v.value())); }); v.handle({ - let s = channels.rewards_coins.s.clone(); - move |v, ev| match ev.bits() { - events::ADD_A_REWARD_SEND_COINS => { - s.try_send(v.value()).ok(); - app::handle_main(events::ADD_A_REWARD_SEND_REWARD).ok(); + let s_coins = channels.rewards_send_coins.s.clone(); + let r_coins = channels.rewards_receive_coins.r.clone(); + move |v, ev| match ev { + Event::Hide => { v.set_value(0.0); true } - events::ADD_A_REWARD_RESET_COINS => { - v.set_value(0.0); - true - } - _ => false, + _ => match ev.bits() { + events::ADD_A_REWARD_SEND_COINS => { + s_coins.try_send(v.value()).ok(); + app::handle_main(events::ADD_A_REWARD_SEND_REWARD).ok(); + v.set_value(0.0); + true + } + events::EDIT_A_REWARD_SEND_COINS => { + s_coins.try_send(v.value()).ok(); + app::handle_main(events::EDIT_A_REWARD_SEND_REWARD).ok(); + v.set_value(0.0); + true + } + events::EDIT_A_REWARD_RECEIVE_COINS => r_coins.try_recv().map_or(false, |coins| { + v.set_value(coins); + true + }), + _ => false, + }, } }) } diff --git a/src/ui/rewards/edit/ok.rs b/src/ui/rewards/edit/ok.rs index 4c127eb..de52e97 100644 --- a/src/ui/rewards/edit/ok.rs +++ b/src/ui/rewards/edit/ok.rs @@ -11,14 +11,25 @@ pub fn ok() -> Button { ok } -fn logic(b: &mut T) { - b.set_callback(|b| { - app::handle_main(events::ADD_A_REWARD_SEND_COINS).ok(); - if let Some(ref p) = b.parent() { - if let Some(ref mut w) = p.parent() { - w.hide(); - } +fn logic(b: &mut T) { + b.set_callback(add_callback); + b.handle(|b, ev| match ev.bits() { + events::OK_BUTTON_SET_TO_ADD => { + b.set_callback(add_callback); + true } + events::OK_BUTTON_SET_TO_EDIT => { + b.set_callback(edit_callback); + true + } + _ => logic::handle_selection(b, ev, FrameType::BorderBox), }); - b.handle(|o, ev| logic::handle_selection(o, ev, FrameType::BorderBox)); +} + +fn add_callback(_: &mut T) { + app::handle_main(events::ADD_A_REWARD_SEND_COINS).ok(); +} + +fn edit_callback(_: &mut T) { + app::handle_main(events::EDIT_A_REWARD_SEND_COINS).ok(); } diff --git a/src/ui/rewards/edit/reward.rs b/src/ui/rewards/edit/reward.rs index 678cf0b..8e90c63 100644 --- a/src/ui/rewards/edit/reward.rs +++ b/src/ui/rewards/edit/reward.rs @@ -1,5 +1,5 @@ use fltk::{ - enums::{Align, FrameType}, + enums::{Align, Event, FrameType}, input::Input, prelude::*, }; @@ -28,21 +28,42 @@ pub fn reward(channels: &Channels) -> Input { fn logic(i: &mut T, channels: &Channels) { i.handle({ - let r_coins = channels.rewards_coins.r.clone(); - let s_reward = channels.rewards_reward.s.clone(); + let r_coins = channels.rewards_send_coins.r.clone(); + let r_reward = channels.rewards_receive_reward.r.clone(); + let s_item = channels.rewards_send_item.s.clone(); let s_mw = channels.mw.s.clone(); - move |i, ev| match ev.bits() { - events::ADD_A_REWARD_SEND_REWARD => r_coins.try_recv().map_or(false, |coins| { - s_reward.try_send(format!("({}) {}", coins, i.value())).ok(); - s_mw.try_send(events::ADD_A_REWARD_RECEIVE).ok(); - i.set_value(""); - true - }), - events::ADD_A_REWARD_RESET_REWARD => { + move |i, ev| match ev { + Event::Hide => { i.set_value(""); true } - _ => false, + _ => match ev.bits() { + events::ADD_A_REWARD_SEND_REWARD => r_coins.try_recv().map_or(false, |coins| { + s_item.try_send(format!("({}) {}", coins, i.value())).ok(); + s_mw.try_send(events::ADD_A_REWARD_RECEIVE).ok(); + i.set_value(""); + if let Some(ref mut w) = i.parent() { + w.hide(); + } + true + }), + events::EDIT_A_REWARD_SEND_REWARD => r_coins.try_recv().map_or(false, |coins| { + s_item.try_send(format!("({}) {}", coins, i.value())).ok(); + s_mw.try_send(events::EDIT_A_REWARD_RECEIVE).ok(); + i.set_value(""); + if let Some(ref mut w) = i.parent() { + w.hide(); + } + true + }), + events::EDIT_A_REWARD_RECEIVE_REWARD => { + r_reward.try_recv().map_or(false, |ref reward| { + i.set_value(reward); + true + }) + } + _ => false, + }, } }); } diff --git a/src/ui/rewards/list.rs b/src/ui/rewards/list.rs index 0b91b77..e167901 100644 --- a/src/ui/rewards/list.rs +++ b/src/ui/rewards/list.rs @@ -1,4 +1,4 @@ -use fltk::prelude::*; +use fltk::{app, prelude::*}; use crate::channels::Channels; use crate::events; @@ -22,13 +22,67 @@ pub fn list(channels: &Channels) -> List { fn logic(l: &mut List, channels: &Channels) { l.handle({ - let r_reward = channels.rewards_reward.r.clone(); - move |s, items, bits| match bits { - events::ADD_A_REWARD_RECEIVE => r_reward.try_recv().map_or(false, |reward| { - items.push(reward); - s.expand(items.len() as i32); + let s_re = channels.rewards_edit.s.clone(); + let s_diff = channels.rewards_receive_coins.s.clone(); + let s_item = channels.rewards_receive_item.s.clone(); + let r_coins = channels.rewards_send_coins.r.clone(); + let r_item = channels.rewards_send_item.r.clone(); + move |s, selected, items, bits| match bits { + events::ADD_A_REWARD_RECEIVE => r_item.try_recv().map_or(false, |item| { + items.push(item); + s.resize_list(items.len() as i32); true }), + events::DELETE_A_REWARD => { + if *selected != 0 { + let index = *selected as usize - 1; + if *selected == items.len() as i32 { + *selected -= 1; + } + items.remove(index); + s.resize_list(items.len() as i32); + } + true + } + events::EDIT_A_REWARD_RECEIVE => r_item.try_recv().map_or(false, |item| { + items[*selected as usize - 1] = item; + s.resize_list(items.len() as i32); + true + }), + events::EDIT_A_REWARD_SEND_ITEM => { + if *selected != 0 { + let string = items[*selected as usize - 1].clone(); + s_item.try_send(string).ok(); + s_re.try_send(events::EDIT_A_REWARD_OPEN).ok(); + s_re.try_send(events::OK_BUTTON_SET_TO_EDIT).ok(); + } + true + } + events::SPEND_COINS_RECEIVE_TOTAL => { + if *selected != 0 { + let index = *selected as usize - 1; + if let Ok(coins) = r_coins.try_recv() { + if let Some(rb_i) = items[index].find(')') { + if let Ok(price) = items[index][1..rb_i].parse::() { + if price <= coins { + if *selected == items.len() as i32 { + *selected -= 1; + } + items.remove(index); + s.resize_list(items.len() as i32); + + let diff = coins - price; + s_diff.try_send((diff * 100.0).round() / 100.0).ok(); + app::handle_main(events::SPEND_COINS_RECEIVE_DIFF).ok(); + } else { + println!("Not enough coins!"); + } + } + } + } + } + true + } _ => false, } }); diff --git a/src/ui/rewards/menubar.rs b/src/ui/rewards/menubar.rs index 8b19a9e..d65965f 100644 --- a/src/ui/rewards/menubar.rs +++ b/src/ui/rewards/menubar.rs @@ -2,6 +2,7 @@ use fltk::{ app, button::Button, enums::{Event, FrameType, Key, Shortcut}, + frame::Frame, group::{Pack, PackType}, image::SvgImage, prelude::*, @@ -22,6 +23,15 @@ pub fn menubar(channels: &Channels) -> Pack { // 2. Delete the Reward let _db = delete_button(); + // 3. Edit the Reward + let _eb = edit_button(); + + // 4. Divider + let _d = divider(); + + // 5. Spend Coins for the Reward + let _sb = spend_button(); + m.end(); menubar_logic(&mut m); @@ -67,18 +77,16 @@ fn add_button_logic(ab: &mut T, channels: & let s = channels.rewards_edit.s.clone(); move |_| { s.try_send(events::ADD_A_REWARD_OPEN).ok(); + s.try_send(events::OK_BUTTON_SET_TO_ADD).ok(); } }); - ab.handle(|ab, ev| { - if ev == Event::KeyDown { - match app::event_key() { - Key::Left => logic::rp_handle_left(ab, 0), - Key::Right => logic::rp_handle_right(ab, 0), - _ => logic::handle_selection(ab, ev, FrameType::FlatBox), - } - } else { - logic::handle_selection(ab, ev, FrameType::FlatBox) - } + ab.handle(|ab, ev| match ev { + Event::KeyDown => match app::event_key() { + Key::Left => logic::rp_handle_left(ab, 0), + Key::Right => logic::rp_handle_right(ab, 0), + _ => logic::handle_selection(ab, ev, FrameType::FlatBox), + }, + _ => logic::handle_selection(ab, ev, FrameType::FlatBox), }); } @@ -99,17 +107,84 @@ fn delete_button() -> Button { fn delete_button_logic(db: &mut T) { db.set_shortcut(Shortcut::from_char('d')); db.set_callback(|_| { - println!("Delete Pressed!"); + app::handle_main(events::DELETE_A_REWARD).ok(); }); - db.handle(|db, ev| { - if ev == Event::KeyDown { - match app::event_key() { - Key::Left => logic::rp_handle_left(db, 1), - Key::Right => logic::rp_handle_right(db, 1), - _ => logic::handle_selection(db, ev, FrameType::FlatBox), - } - } else { - logic::handle_selection(db, ev, FrameType::FlatBox) + db.handle(|db, ev| match ev { + Event::KeyDown => match app::event_key() { + Key::Left => logic::rp_handle_left(db, 1), + Key::Right => logic::rp_handle_right(db, 1), + _ => logic::handle_selection(db, ev, FrameType::FlatBox), + }, + _ => logic::handle_selection(db, ev, FrameType::FlatBox), + }); +} + +fn edit_button() -> Button { + let mut ei = SvgImage::from_data(include_str!("../../../assets/menu/pencil.svg")).unwrap(); + ei.scale(24, 24, true, true); + + let mut eb = Button::default().with_size(CONSTANTS.rewards_menubar_height, 0); + eb.set_image(Some(ei)); + eb.set_frame(FrameType::FlatBox); + eb.set_tooltip("Edit the Reward"); + + edit_button_logic(&mut eb); + + eb +} + +fn edit_button_logic(eb: &mut T) { + eb.set_shortcut(Shortcut::from_char('e')); + eb.set_callback({ + move |_| { + app::handle_main(events::EDIT_A_REWARD_SEND_ITEM).ok(); } }); + eb.handle(|eb, ev| match ev { + Event::KeyDown => match app::event_key() { + Key::Left => logic::rp_handle_left(eb, 2), + Key::Right => logic::rp_handle_right(eb, 2), + _ => logic::handle_selection(eb, ev, FrameType::FlatBox), + }, + _ => logic::handle_selection(eb, ev, FrameType::FlatBox), + }); +} + +fn divider() -> Frame { + let mut di = SvgImage::from_data(include_str!("../../../assets/menu/divider.svg")).unwrap(); + di.scale(8, 24, true, true); + + let mut d = Frame::default().with_size(10, 0); + d.set_image(Some(di)); + + d +} + +fn spend_button() -> Button { + let mut si = SvgImage::from_data(include_str!("../../../assets/menu/insert-coin.svg")).unwrap(); + si.scale(24, 24, true, true); + + let mut sb = Button::default().with_size(CONSTANTS.rewards_menubar_height, 0); + sb.set_image(Some(si)); + sb.set_frame(FrameType::FlatBox); + sb.set_tooltip("Spend Coins for the Reward"); + + spend_button_logic(&mut sb); + + sb +} + +fn spend_button_logic(sb: &mut T) { + sb.set_shortcut(Shortcut::from_char('s')); + sb.set_callback(|_| { + app::handle_main(events::SPEND_COINS_SEND_TOTAL).ok(); + }); + sb.handle(|sb, ev| match ev { + Event::KeyDown => match app::event_key() { + Key::Left => logic::rp_handle_left(sb, 2), + Key::Right => logic::rp_handle_right(sb, 2), + _ => logic::handle_selection(sb, ev, FrameType::FlatBox), + }, + _ => logic::handle_selection(sb, ev, FrameType::FlatBox), + }); } diff --git a/src/ui/widgets/list.rs b/src/ui/widgets/list.rs index 8df0744..6684e54 100644 --- a/src/ui/widgets/list.rs +++ b/src/ui/widgets/list.rs @@ -7,11 +7,8 @@ use fltk::{ }; use std::{cell::RefCell, rc::Rc}; -type Selected = Rc>; -type Items = Rc>>; - pub trait ScrollExt: GroupExt { - fn expand(&mut self, n: i32) { + fn resize_list(&mut self, n: i32) { if let Some(ref mut l) = self.child(0) { l.resize(self.x() + 1, self.y() + 1, self.w() - 2, n * 20); self.redraw(); @@ -21,6 +18,9 @@ pub trait ScrollExt: GroupExt { impl ScrollExt for Scroll {} +type Selected = Rc>; +type Items = Rc>>; + pub struct List { scroll: Scroll, list: Widget, @@ -44,11 +44,11 @@ impl List { let mut w = List { scroll, list, - selected: Selected::new(RefCell::new(1)), + selected: Selected::default(), items: Items::default(), }; w.draw(); - w.handle(|_, _, _| false); + w.handle(|_, _, _, _| false); w } @@ -72,7 +72,13 @@ impl List { pub fn add(&mut self, s: &'static str) { self.items.borrow_mut().push(String::from(s)); - self.scroll.expand(self.items.borrow().len() as i32); + self.scroll.resize_list(self.items.borrow().len() as i32); + } + + pub fn select(&mut self, idx: i32) { + if idx >= 0 { + *self.selected.borrow_mut() = idx; + } } fn draw(&mut self) { @@ -115,7 +121,7 @@ impl List { pub fn handle(&mut self, handle_custom_events: F) where - F: Fn(&mut Scroll, &mut Vec, i32) -> bool, + F: Fn(&mut Scroll, &mut i32, &mut Vec, i32) -> bool, { self.scroll.handle({ let selected = Rc::clone(&self.selected); @@ -124,7 +130,12 @@ impl List { Event::Focus => true, Event::Push => handle_push(s, &selected, &items), Event::KeyDown => handle_key_down(s, &selected, &items), - _ => handle_custom_events(s, &mut *items.borrow_mut(), ev.bits()), + _ => handle_custom_events( + s, + &mut *selected.borrow_mut(), + &mut *items.borrow_mut(), + ev.bits(), + ), } }); } @@ -181,7 +192,7 @@ fn handle_push(s: &mut Scroll, selected: &Selected, items: &Items) -> bool { fn handle_key_down(s: &mut Scroll, selected: &Selected, items: &Items) -> bool { match app::event_key() { Key::Up => { - if *selected.borrow() == 1 { + if *selected.borrow() == 0 || *selected.borrow() == 1 { *selected.borrow_mut() = items.borrow().len() as i32; } else { *selected.borrow_mut() -= 1; diff --git a/src/ui/windows/main.rs b/src/ui/windows/main.rs index 4cf1c89..c17dca1 100644 --- a/src/ui/windows/main.rs +++ b/src/ui/windows/main.rs @@ -25,7 +25,7 @@ pub fn main(channels: &Channels) -> Window { w.set_icon(Some(icon())); // 1. Focus Pane - let _fp = focus::pane(); + let _fp = focus::pane(channels); // 2. Rewards Pane let re_p = rewards::pane(channels); diff --git a/src/ui/windows/rewards_edit.rs b/src/ui/windows/rewards_edit.rs index 7e65a77..34b1e99 100644 --- a/src/ui/windows/rewards_edit.rs +++ b/src/ui/windows/rewards_edit.rs @@ -1,4 +1,5 @@ -use fltk::{enums::Event, prelude::*, window::Window}; +use fltk::app; +use fltk::{prelude::*, window::Window}; use super::icon; use crate::channels::Channels; @@ -30,17 +31,36 @@ pub fn rewards_edit(channels: &Channels) -> Window { w.end(); - w.handle(handle); + logic(&mut w, channels); w } -fn handle(w: &mut T, ev: Event) -> bool { - match ev.bits() { - events::ADD_A_REWARD_OPEN => { - w.show(); - true +fn logic(w: &mut T, channels: &Channels) { + w.handle({ + let r_item = channels.rewards_receive_item.r.clone(); + let s_coins = channels.rewards_receive_coins.s.clone(); + let s_reward = channels.rewards_receive_reward.s.clone(); + move |w, ev| match ev.bits() { + events::ADD_A_REWARD_OPEN => { + w.show(); + true + } + events::EDIT_A_REWARD_OPEN => { + w.show(); + if let Ok(item) = r_item.try_recv() { + if let Some(rb_i) = item.find(')') { + if let Ok(coins) = item[1..rb_i].parse::() { + s_coins.try_send(coins).ok(); + app::handle_main(events::EDIT_A_REWARD_RECEIVE_COINS).ok(); + s_reward.try_send(item[(rb_i + 2)..].to_string()).ok(); + app::handle_main(events::EDIT_A_REWARD_RECEIVE_REWARD).ok(); + } + } + } + true + } + _ => false, } - _ => false, - } + }) } From b9a0a6707607aa91d1db92efa603021247a35659 Mon Sep 17 00:00:00 2001 From: Pavel Sobolev Date: Tue, 20 Jul 2021 22:50:20 +0300 Subject: [PATCH 08/16] Make buttons in the rewards pane active / inactive when needed. --- Cargo.lock | 12 +- Cargo.toml | 2 +- src/events.rs | 12 +- src/ui/focus/arrow.rs | 5 +- src/ui/focus/coins.rs | 39 +++-- src/ui/focus/timer.rs | 5 +- src/ui/logic.rs | 89 +++++++++-- src/ui/rates/menubar.rs | 8 +- src/ui/rewards/edit/cancel.rs | 3 +- src/ui/rewards/edit/ok.rs | 3 +- src/ui/rewards/list.rs | 149 +++++++++++-------- src/ui/rewards/menubar.rs | 190 ------------------------ src/ui/rewards/menubar/add_button.rs | 46 ++++++ src/ui/rewards/menubar/delete_button.rs | 65 ++++++++ src/ui/rewards/menubar/divider.rs | 11 ++ src/ui/rewards/menubar/edit_button.rs | 65 ++++++++ src/ui/rewards/menubar/mod.rs | 14 ++ src/ui/rewards/menubar/new.rs | 54 +++++++ src/ui/rewards/menubar/spend_button.rs | 66 ++++++++ src/ui/rewards/mod.rs | 3 - src/ui/rewards/pane.rs | 4 +- src/ui/widgets/list.rs | 90 ++++++----- src/ui/widgets/mod.rs | 2 +- 23 files changed, 596 insertions(+), 341 deletions(-) delete mode 100644 src/ui/rewards/menubar.rs create mode 100644 src/ui/rewards/menubar/add_button.rs create mode 100644 src/ui/rewards/menubar/delete_button.rs create mode 100644 src/ui/rewards/menubar/divider.rs create mode 100644 src/ui/rewards/menubar/edit_button.rs create mode 100644 src/ui/rewards/menubar/mod.rs create mode 100644 src/ui/rewards/menubar/new.rs create mode 100644 src/ui/rewards/menubar/spend_button.rs diff --git a/Cargo.lock b/Cargo.lock index 8666aaf..c8b5ecb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -51,9 +51,9 @@ dependencies = [ [[package]] name = "fltk" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d364803b740d10140734eaa925af4ff4b3826e2bad29b18e477aeac3fdf016c" +checksum = "22226bafa5d3a04f2223aea8083b101204d1b1070fbe7e140e90386026ffce9e" dependencies = [ "bitflags", "fltk-derive", @@ -65,9 +65,9 @@ dependencies = [ [[package]] name = "fltk-derive" -version = "1.1.0" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7272e4cb6beabf0a37a8070f1700795d99fc3612dd7292842fa171e2f67d8b76" +checksum = "4aadf3955b405fda705ae40916d06148a3f0d595b9a0518d4336fc5f8149e1c9" dependencies = [ "quote", "syn", @@ -75,9 +75,9 @@ dependencies = [ [[package]] name = "fltk-sys" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "158abaf04e1d5cdc08bb75cd3683e03be6f6b6e2fa3a0124e1a596c436417bcb" +checksum = "51890625127459c5b5100c889c84bd7a7672c91cd4a7bb0c443ce974322b0a2e" dependencies = [ "cmake", "libc", diff --git a/Cargo.toml b/Cargo.toml index 30bf07c..ca74754 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,5 +29,5 @@ codegen-units = 1 panic = 'abort' [dependencies] -fltk = { version = "=1.1.1", features = ["fltk-bundled", "use-ninja"] } +fltk = { version = "=1.1.2", features = ["fltk-bundled", "use-ninja"] } crossbeam-channel = {version = "~0.5.1"} diff --git a/src/events.rs b/src/events.rs index b03ef0d..f02a0f9 100644 --- a/src/events.rs +++ b/src/events.rs @@ -32,7 +32,13 @@ events!( EDIT_A_REWARD_SEND_REWARD, EDIT_A_REWARD_RECEIVE, DELETE_A_REWARD, - SPEND_COINS_SEND_TOTAL, - SPEND_COINS_RECEIVE_TOTAL, - SPEND_COINS_RECEIVE_DIFF + SPEND_COINS_SEND_PRICE, + SPEND_COINS_RECEIVE_PRICE, + LOCK_THE_DELETE_A_REWARD_BUTTON, + UNLOCK_THE_DELETE_A_REWARD_BUTTON, + LOCK_THE_EDIT_A_REWARD_BUTTON, + UNLOCK_THE_EDIT_A_REWARD_BUTTON, + CHECK_AFFORDABILITY_RECEIVE_PRICE, + LOCK_THE_SPEND_BUTTON, + UNLOCK_THE_SPEND_BUTTON ); diff --git a/src/ui/focus/arrow.rs b/src/ui/focus/arrow.rs index 82d04ce..141e333 100644 --- a/src/ui/focus/arrow.rs +++ b/src/ui/focus/arrow.rs @@ -13,6 +13,7 @@ use crate::ui::logic; pub fn arrow() -> Button { let mut a = Button::default(); a.set_frame(FrameType::FlatBox); + a.set_down_frame(FrameType::DownBox); a.set_tooltip("Show the Conversion Rates pane"); resize_frame(&mut a); @@ -89,9 +90,9 @@ fn logic(a: &mut T) { Key::Left => logic::fp_handle_left(a, 2), Key::Right => logic::fp_handle_right(a, 2), Key::Tab => logic::handle_tab(a), - _ => logic::handle_selection(a, ev, unselect_box), + _ => logic::handle_active_selection(a, ev, unselect_box), }, - _ => logic::handle_selection(a, ev, unselect_box), + _ => logic::handle_active_selection(a, ev, unselect_box), } }); } diff --git a/src/ui/focus/coins.rs b/src/ui/focus/coins.rs index ba31a31..73e7860 100644 --- a/src/ui/focus/coins.rs +++ b/src/ui/focus/coins.rs @@ -16,6 +16,7 @@ pub fn coins(channels: &Channels) -> Button { let mut c = Button::default().with_label("99999"); c.set_label_type(LabelType::None); c.set_frame(FrameType::FlatBox); + c.set_down_frame(FrameType::DownBox); c.set_tooltip("Hide the Rewards pane"); resize_button(&mut c); @@ -99,29 +100,39 @@ fn logic(c: &mut T, channels: &Channels) { } }); c.handle({ - let s_coins = channels.rewards_send_coins.s.clone(); let r_coins = channels.rewards_receive_coins.r.clone(); move |c, ev| match ev { Event::KeyDown => match app::event_key() { Key::Left => logic::fp_handle_left(c, 1), Key::Right => logic::fp_handle_right(c, 1), Key::Tab => logic::handle_tab(c), - _ => logic::handle_selection(c, ev, FrameType::FlatBox), + _ => logic::handle_active_selection(c, ev, FrameType::FlatBox), }, _ => match ev.bits() { - events::SPEND_COINS_SEND_TOTAL => c.label().parse::().map_or(false, |coins| { - s_coins.try_send(coins).ok(); - app::handle_main(events::SPEND_COINS_RECEIVE_TOTAL).ok(); - true - }), - events::SPEND_COINS_RECEIVE_DIFF => r_coins.try_recv().map_or(false, |coins| { - c.set_label(&coins.to_string()); - if let Some(ref mut p) = c.parent() { - p.redraw(); - } - true + events::SPEND_COINS_RECEIVE_PRICE => r_coins.try_recv().map_or(false, |price| { + c.label().parse::().map_or(false, |coins| { + let diff = ((coins - price) * 100.0).round() / 100.0; + c.set_label(&diff.to_string()); + app::handle_main(events::DELETE_A_REWARD).ok(); + if let Some(ref mut p) = c.parent() { + p.redraw(); + } + true + }) }), - _ => logic::handle_selection(c, ev, FrameType::FlatBox), + events::CHECK_AFFORDABILITY_RECEIVE_PRICE => { + r_coins.try_recv().map_or(false, |price| { + c.label().parse::().map_or(false, |coins| { + if price <= coins { + app::handle_main(events::UNLOCK_THE_SPEND_BUTTON).ok(); + } else { + app::handle_main(events::LOCK_THE_SPEND_BUTTON).ok(); + } + true + }) + }) + } + _ => logic::handle_active_selection(c, ev, FrameType::FlatBox), }, } }); diff --git a/src/ui/focus/timer.rs b/src/ui/focus/timer.rs index 34a16e4..f1e51c3 100644 --- a/src/ui/focus/timer.rs +++ b/src/ui/focus/timer.rs @@ -14,6 +14,7 @@ pub fn timer() -> Button { let mut t = Button::default().with_label("00:00:00"); t.set_label_type(LabelType::None); t.set_frame(FrameType::FlatBox); + t.set_down_frame(FrameType::DownBox); t.set_tooltip("Start the timer"); if let Some(ref p) = t.parent() { @@ -110,10 +111,10 @@ fn logic(t: &mut T) { Key::Left => logic::fp_handle_left(t, 0), Key::Right => logic::fp_handle_right(t, 0), Key::Tab => logic::handle_tab(t), - _ => logic::handle_selection(t, ev, FrameType::FlatBox), + _ => logic::handle_active_selection(t, ev, FrameType::FlatBox), } } else { - logic::handle_selection(t, ev, FrameType::FlatBox) + logic::handle_active_selection(t, ev, FrameType::FlatBox) } } } diff --git a/src/ui/logic.rs b/src/ui/logic.rs index 9b2fb9f..e35e816 100644 --- a/src/ui/logic.rs +++ b/src/ui/logic.rs @@ -4,17 +4,24 @@ use fltk::{ prelude::*, }; +const ACTIVE_SELECTION_COLOR: u32 = 0xE5_F3_FF; +const INACTIVE_SELECTION_COLOR: u32 = 0xF5_FA_FE; + /// Handle focus / selection events -pub fn handle_selection(b: &mut T, ev: Event, unselect_box: FrameType) -> bool { +pub fn handle_active_selection( + b: &mut T, + ev: Event, + unselect_box: FrameType, +) -> bool { match ev { Event::Focus => { if app::event_key_down(Key::Enter) { - select(b); + select_active(b); } true } Event::Enter => { - select(b); + select_active(b); true } Event::Leave | Event::Unfocus | Event::Hide => { @@ -23,7 +30,7 @@ pub fn handle_selection(b: &mut T, ev: Event, unselect_box: FrameT } Event::KeyDown => match app::event_key() { Key::Enter => { - select(b); + select_active(b); true } _ => false, @@ -40,22 +47,84 @@ pub fn handle_selection(b: &mut T, ev: Event, unselect_box: FrameT } } -fn select(b: &mut T) { - b.set_color(Color::from_hex(0xE5_F3_FF)); +pub fn handle_inactive_selection( + b: &mut T, + ev: Event, + unselect_box: FrameType, +) -> bool { + match ev { + Event::Focus => { + if app::event_key_down(Key::Enter) { + select_inactive(b); + } + true + } + Event::Enter => { + select_inactive(b); + true + } + Event::Leave | Event::Unfocus | Event::Hide => { + unselect(b, unselect_box); + true + } + Event::KeyDown => match app::event_key() { + Key::Enter => { + select_inactive(b); + true + } + _ => false, + }, + Event::KeyUp => match app::event_key() { + Key::Enter => { + unselect(b, unselect_box); + true + } + _ => false, + }, + _ => false, + } +} + +pub fn handle_selection( + b: &mut T, + ev: Event, + unselect_box: FrameType, + lock: bool, +) -> bool { + if lock { + handle_inactive_selection(b, ev, unselect_box) + } else { + handle_active_selection(b, ev, unselect_box) + } +} + +fn select(b: &mut T, hex: u32) { + b.set_color(Color::from_hex(hex)); b.set_frame(FrameType::BorderBox); - b.set_selection_color(Color::from_hex(0xE5_F3_FF)); - b.set_down_frame(FrameType::DownBox); b.redraw(); } +pub fn select_active(b: &mut T) { + select(b, ACTIVE_SELECTION_COLOR); +} + +pub fn select_inactive(b: &mut T) { + select(b, INACTIVE_SELECTION_COLOR); +} + fn unselect(b: &mut T, unselect_box: FrameType) { b.set_color(Color::BackGround); b.set_frame(unselect_box); - b.set_selection_color(Color::BackGround); - b.set_down_frame(unselect_box); b.redraw(); } +pub fn mouse_hovering_widget(b: &mut T) -> bool { + app::event_x() >= b.x() + && app::event_x() <= b.x() + b.w() + && app::event_y() >= b.y() + && app::event_y() <= b.y() + b.h() +} + /// Handle Left events for the buttons in the Focus pane pub fn fp_handle_left(b: &mut T, idx: i32) -> bool { if let Some(ref p) = b.parent() { diff --git a/src/ui/rates/menubar.rs b/src/ui/rates/menubar.rs index 9b982c5..d594746 100644 --- a/src/ui/rates/menubar.rs +++ b/src/ui/rates/menubar.rs @@ -69,10 +69,10 @@ fn add_button_logic(ab: &mut T) { match app::event_key() { Key::Left => logic::rp_handle_left(ab, 0), Key::Right => logic::rp_handle_right(ab, 0), - _ => logic::handle_selection(ab, ev, FrameType::FlatBox), + _ => logic::handle_active_selection(ab, ev, FrameType::FlatBox), } } else { - logic::handle_selection(ab, ev, FrameType::FlatBox) + logic::handle_active_selection(ab, ev, FrameType::FlatBox) } }); } @@ -101,10 +101,10 @@ fn delete_button_logic(db: &mut T) { match app::event_key() { Key::Left => logic::rp_handle_left(db, 1), Key::Right => logic::rp_handle_right(db, 1), - _ => logic::handle_selection(db, ev, FrameType::FlatBox), + _ => logic::handle_active_selection(db, ev, FrameType::FlatBox), } } else { - logic::handle_selection(db, ev, FrameType::FlatBox) + logic::handle_active_selection(db, ev, FrameType::FlatBox) } }); } diff --git a/src/ui/rewards/edit/cancel.rs b/src/ui/rewards/edit/cancel.rs index be4190d..1a11087 100644 --- a/src/ui/rewards/edit/cancel.rs +++ b/src/ui/rewards/edit/cancel.rs @@ -4,6 +4,7 @@ use crate::ui::logic; pub fn cancel() -> Button { let mut c = Button::default().with_size(80, 0).with_label("Cancel"); + c.set_down_frame(FrameType::DownBox); logic(&mut c); @@ -18,5 +19,5 @@ fn logic(b: &mut T) { } } }); - b.handle(|c, ev| logic::handle_selection(c, ev, FrameType::BorderBox)); + b.handle(|c, ev| logic::handle_active_selection(c, ev, FrameType::BorderBox)); } diff --git a/src/ui/rewards/edit/ok.rs b/src/ui/rewards/edit/ok.rs index de52e97..b9dff34 100644 --- a/src/ui/rewards/edit/ok.rs +++ b/src/ui/rewards/edit/ok.rs @@ -5,6 +5,7 @@ use crate::ui::logic; pub fn ok() -> Button { let mut ok = Button::default().with_size(80, 0).with_label("OK"); + ok.set_down_frame(FrameType::DownBox); logic(&mut ok); @@ -22,7 +23,7 @@ fn logic(b: &mut T) { b.set_callback(edit_callback); true } - _ => logic::handle_selection(b, ev, FrameType::BorderBox), + _ => logic::handle_active_selection(b, ev, FrameType::BorderBox), }); } diff --git a/src/ui/rewards/list.rs b/src/ui/rewards/list.rs index e167901..b892136 100644 --- a/src/ui/rewards/list.rs +++ b/src/ui/rewards/list.rs @@ -1,10 +1,11 @@ -use fltk::{app, prelude::*}; +use crossbeam_channel::Sender; +use fltk::{app, group::Scroll, prelude::*}; use crate::channels::Channels; use crate::events; -use crate::ui::widgets::{List, ScrollExt}; +use crate::ui::widgets::{Items, List, ScrollExt, Selected}; -pub fn list(channels: &Channels) -> List { +pub fn new(channels: &Channels) -> List { let mut l = List::default(); if let Some(ref p) = l.parent() { @@ -21,69 +22,93 @@ pub fn list(channels: &Channels) -> List { } fn logic(l: &mut List, channels: &Channels) { - l.handle({ - let s_re = channels.rewards_edit.s.clone(); - let s_diff = channels.rewards_receive_coins.s.clone(); - let s_item = channels.rewards_receive_item.s.clone(); - let r_coins = channels.rewards_send_coins.r.clone(); - let r_item = channels.rewards_send_item.r.clone(); - move |s, selected, items, bits| match bits { - events::ADD_A_REWARD_RECEIVE => r_item.try_recv().map_or(false, |item| { - items.push(item); - s.resize_list(items.len() as i32); - true - }), - events::DELETE_A_REWARD => { - if *selected != 0 { - let index = *selected as usize - 1; - if *selected == items.len() as i32 { - *selected -= 1; - } - items.remove(index); - s.resize_list(items.len() as i32); - } - true + l.handle( + handle_selection(channels), + handle_selection(channels), + handle_custom_events(channels), + ); +} + +fn handle_selection(channels: &Channels) -> impl Fn(&mut Scroll, &Selected, &Items) { + let s_price = channels.rewards_receive_coins.s.clone(); + move |_, selected, items| { + handle_selection_closure(selected, items, &s_price); + } +} + +fn handle_selection_closure(selected: &Selected, items: &Items, s_price: &Sender) { + if *selected.borrow() > 0 && items.borrow().len() > 0 { + app::handle_main(events::UNLOCK_THE_DELETE_A_REWARD_BUTTON).ok(); + app::handle_main(events::UNLOCK_THE_EDIT_A_REWARD_BUTTON).ok(); + let index = *selected.borrow() as usize - 1; + if let Some(rb_i) = items.borrow()[index].find(')') { + if let Ok(price) = items.borrow()[index][1..rb_i].parse::() { + s_price.try_send(price).ok(); + app::handle_main(events::CHECK_AFFORDABILITY_RECEIVE_PRICE).ok(); } - events::EDIT_A_REWARD_RECEIVE => r_item.try_recv().map_or(false, |item| { - items[*selected as usize - 1] = item; - s.resize_list(items.len() as i32); - true - }), - events::EDIT_A_REWARD_SEND_ITEM => { - if *selected != 0 { - let string = items[*selected as usize - 1].clone(); - s_item.try_send(string).ok(); - s_re.try_send(events::EDIT_A_REWARD_OPEN).ok(); - s_re.try_send(events::OK_BUTTON_SET_TO_EDIT).ok(); + } + } +} + +fn handle_custom_events( + channels: &Channels, +) -> impl Fn(&mut Scroll, &Selected, &Items, i32) -> bool { + let s_re = channels.rewards_edit.s.clone(); + let s_price = channels.rewards_receive_coins.s.clone(); + let s_item = channels.rewards_receive_item.s.clone(); + let r_item = channels.rewards_send_item.r.clone(); + move |s, selected, items, bits| match bits { + events::ADD_A_REWARD_RECEIVE => r_item.try_recv().map_or(false, |item| { + items.borrow_mut().push(item); + s.resize_list(items.borrow().len() as i32); + true + }), + events::DELETE_A_REWARD => { + if *selected.borrow() > 0 { + let index = *selected.borrow() as usize - 1; + if *selected.borrow() == items.borrow().len() as i32 { + *selected.borrow_mut() -= 1; } - true + if items.borrow().len() == 1 { + app::handle_main(events::LOCK_THE_DELETE_A_REWARD_BUTTON).ok(); + app::handle_main(events::LOCK_THE_EDIT_A_REWARD_BUTTON).ok(); + app::handle_main(events::LOCK_THE_SPEND_BUTTON).ok(); + } else { + handle_selection_closure(selected, items, &s_price); + } + items.borrow_mut().remove(index); + s.resize_list(items.borrow().len() as i32); } - events::SPEND_COINS_RECEIVE_TOTAL => { - if *selected != 0 { - let index = *selected as usize - 1; - if let Ok(coins) = r_coins.try_recv() { - if let Some(rb_i) = items[index].find(')') { - if let Ok(price) = items[index][1..rb_i].parse::() { - if price <= coins { - if *selected == items.len() as i32 { - *selected -= 1; - } - items.remove(index); - s.resize_list(items.len() as i32); - - let diff = coins - price; - s_diff.try_send((diff * 100.0).round() / 100.0).ok(); - app::handle_main(events::SPEND_COINS_RECEIVE_DIFF).ok(); - } else { - println!("Not enough coins!"); - } - } - } - } + true + } + events::EDIT_A_REWARD_RECEIVE => r_item.try_recv().map_or(false, |item| { + items.borrow_mut()[*selected.borrow() as usize - 1] = item; + s.resize_list(items.borrow().len() as i32); + true + }), + events::EDIT_A_REWARD_SEND_ITEM => { + if *selected.borrow() > 0 { + let string = items.borrow()[*selected.borrow() as usize - 1].clone(); + s_item.try_send(string).ok(); + s_re.try_send(events::EDIT_A_REWARD_OPEN).ok(); + s_re.try_send(events::OK_BUTTON_SET_TO_EDIT).ok(); + } + true + } + events::SPEND_COINS_SEND_PRICE => { + if *selected.borrow() > 0 { + let index = *selected.borrow() as usize - 1; + let rb_i = items.borrow()[index].find(')').unwrap_or_default(); + let price = items.borrow()[index][1..rb_i] + .parse::() + .unwrap_or(-1.0); + if price >= 0.0 { + s_price.try_send(price).ok(); + app::handle_main(events::SPEND_COINS_RECEIVE_PRICE).ok(); } - true } - _ => false, + true } - }); + _ => false, + } } diff --git a/src/ui/rewards/menubar.rs b/src/ui/rewards/menubar.rs deleted file mode 100644 index d65965f..0000000 --- a/src/ui/rewards/menubar.rs +++ /dev/null @@ -1,190 +0,0 @@ -use fltk::{ - app, - button::Button, - enums::{Event, FrameType, Key, Shortcut}, - frame::Frame, - group::{Pack, PackType}, - image::SvgImage, - prelude::*, -}; - -use crate::channels::Channels; -use crate::events; -use crate::ui::app::CONSTANTS; -use crate::ui::logic; - -pub fn menubar(channels: &Channels) -> Pack { - let mut m = Pack::default().with_size(0, CONSTANTS.rewards_menubar_height); - m.set_type(PackType::Horizontal); - - // 1. Add a Reward - let _ab = add_button(channels); - - // 2. Delete the Reward - let _db = delete_button(); - - // 3. Edit the Reward - let _eb = edit_button(); - - // 4. Divider - let _d = divider(); - - // 5. Spend Coins for the Reward - let _sb = spend_button(); - - m.end(); - - menubar_logic(&mut m); - - m -} - -fn menubar_logic(m: &mut T) { - m.handle(|m, ev| match ev { - Event::KeyDown => { - if app::event_key() == Key::Tab { - if let Some(ref p) = m.parent() { - if let Some(ref mut l) = p.child(1) { - l.take_focus().ok(); - } - } - true - } else { - false - } - } - _ => false, - }); -} - -fn add_button(channels: &Channels) -> Button { - let mut ai = SvgImage::from_data(include_str!("../../../assets/menu/plus.svg")).unwrap(); - ai.scale(24, 24, true, true); - - let mut ab = Button::default().with_size(CONSTANTS.rewards_menubar_height, 0); - ab.set_image(Some(ai)); - ab.set_frame(FrameType::FlatBox); - ab.set_tooltip("Add a Reward"); - - add_button_logic(&mut ab, channels); - - ab -} - -fn add_button_logic(ab: &mut T, channels: &Channels) { - ab.set_shortcut(Shortcut::from_char('a')); - ab.set_callback({ - let s = channels.rewards_edit.s.clone(); - move |_| { - s.try_send(events::ADD_A_REWARD_OPEN).ok(); - s.try_send(events::OK_BUTTON_SET_TO_ADD).ok(); - } - }); - ab.handle(|ab, ev| match ev { - Event::KeyDown => match app::event_key() { - Key::Left => logic::rp_handle_left(ab, 0), - Key::Right => logic::rp_handle_right(ab, 0), - _ => logic::handle_selection(ab, ev, FrameType::FlatBox), - }, - _ => logic::handle_selection(ab, ev, FrameType::FlatBox), - }); -} - -fn delete_button() -> Button { - let mut di = SvgImage::from_data(include_str!("../../../assets/menu/minus.svg")).unwrap(); - di.scale(24, 24, true, true); - - let mut db = Button::default().with_size(CONSTANTS.rewards_menubar_height, 0); - db.set_image(Some(di)); - db.set_frame(FrameType::FlatBox); - db.set_tooltip("Delete the Reward"); - - delete_button_logic(&mut db); - - db -} - -fn delete_button_logic(db: &mut T) { - db.set_shortcut(Shortcut::from_char('d')); - db.set_callback(|_| { - app::handle_main(events::DELETE_A_REWARD).ok(); - }); - db.handle(|db, ev| match ev { - Event::KeyDown => match app::event_key() { - Key::Left => logic::rp_handle_left(db, 1), - Key::Right => logic::rp_handle_right(db, 1), - _ => logic::handle_selection(db, ev, FrameType::FlatBox), - }, - _ => logic::handle_selection(db, ev, FrameType::FlatBox), - }); -} - -fn edit_button() -> Button { - let mut ei = SvgImage::from_data(include_str!("../../../assets/menu/pencil.svg")).unwrap(); - ei.scale(24, 24, true, true); - - let mut eb = Button::default().with_size(CONSTANTS.rewards_menubar_height, 0); - eb.set_image(Some(ei)); - eb.set_frame(FrameType::FlatBox); - eb.set_tooltip("Edit the Reward"); - - edit_button_logic(&mut eb); - - eb -} - -fn edit_button_logic(eb: &mut T) { - eb.set_shortcut(Shortcut::from_char('e')); - eb.set_callback({ - move |_| { - app::handle_main(events::EDIT_A_REWARD_SEND_ITEM).ok(); - } - }); - eb.handle(|eb, ev| match ev { - Event::KeyDown => match app::event_key() { - Key::Left => logic::rp_handle_left(eb, 2), - Key::Right => logic::rp_handle_right(eb, 2), - _ => logic::handle_selection(eb, ev, FrameType::FlatBox), - }, - _ => logic::handle_selection(eb, ev, FrameType::FlatBox), - }); -} - -fn divider() -> Frame { - let mut di = SvgImage::from_data(include_str!("../../../assets/menu/divider.svg")).unwrap(); - di.scale(8, 24, true, true); - - let mut d = Frame::default().with_size(10, 0); - d.set_image(Some(di)); - - d -} - -fn spend_button() -> Button { - let mut si = SvgImage::from_data(include_str!("../../../assets/menu/insert-coin.svg")).unwrap(); - si.scale(24, 24, true, true); - - let mut sb = Button::default().with_size(CONSTANTS.rewards_menubar_height, 0); - sb.set_image(Some(si)); - sb.set_frame(FrameType::FlatBox); - sb.set_tooltip("Spend Coins for the Reward"); - - spend_button_logic(&mut sb); - - sb -} - -fn spend_button_logic(sb: &mut T) { - sb.set_shortcut(Shortcut::from_char('s')); - sb.set_callback(|_| { - app::handle_main(events::SPEND_COINS_SEND_TOTAL).ok(); - }); - sb.handle(|sb, ev| match ev { - Event::KeyDown => match app::event_key() { - Key::Left => logic::rp_handle_left(sb, 2), - Key::Right => logic::rp_handle_right(sb, 2), - _ => logic::handle_selection(sb, ev, FrameType::FlatBox), - }, - _ => logic::handle_selection(sb, ev, FrameType::FlatBox), - }); -} diff --git a/src/ui/rewards/menubar/add_button.rs b/src/ui/rewards/menubar/add_button.rs new file mode 100644 index 0000000..3d3db33 --- /dev/null +++ b/src/ui/rewards/menubar/add_button.rs @@ -0,0 +1,46 @@ +use fltk::{ + app, + button::Button, + enums::{Event, FrameType, Key, Shortcut}, + image::SvgImage, + prelude::*, +}; + +use crate::channels::Channels; +use crate::events; +use crate::ui::app::CONSTANTS; +use crate::ui::logic; + +pub fn add_button(channels: &Channels) -> Button { + let mut ai = SvgImage::from_data(include_str!("../../../../assets/menu/plus.svg")).unwrap(); + ai.scale(24, 24, true, true); + + let mut ab = Button::default().with_size(CONSTANTS.rewards_menubar_height, 0); + ab.set_image(Some(ai)); + ab.set_frame(FrameType::FlatBox); + ab.set_down_frame(FrameType::DownBox); + ab.set_tooltip("Add a Reward"); + + logic(&mut ab, channels); + + ab +} + +fn logic(ab: &mut T, channels: &Channels) { + ab.set_shortcut(Shortcut::from_char('a')); + ab.set_callback({ + let s = channels.rewards_edit.s.clone(); + move |_| { + s.try_send(events::ADD_A_REWARD_OPEN).ok(); + s.try_send(events::OK_BUTTON_SET_TO_ADD).ok(); + } + }); + ab.handle(|ab, ev| match ev { + Event::KeyDown => match app::event_key() { + Key::Left => logic::rp_handle_left(ab, 0), + Key::Right => logic::rp_handle_right(ab, 0), + _ => logic::handle_active_selection(ab, ev, FrameType::FlatBox), + }, + _ => logic::handle_active_selection(ab, ev, FrameType::FlatBox), + }); +} diff --git a/src/ui/rewards/menubar/delete_button.rs b/src/ui/rewards/menubar/delete_button.rs new file mode 100644 index 0000000..de7c6d8 --- /dev/null +++ b/src/ui/rewards/menubar/delete_button.rs @@ -0,0 +1,65 @@ +use fltk::{ + app, + button::Button, + enums::{Event, FrameType, Key, Shortcut}, + image::SvgImage, + prelude::*, +}; + +use crate::events; +use crate::ui::app::CONSTANTS; +use crate::ui::logic; + +pub fn delete_button() -> Button { + let mut di = SvgImage::from_data(include_str!("../../../../assets/menu/minus.svg")).unwrap(); + di.scale(24, 24, true, true); + + let mut db = Button::default().with_size(CONSTANTS.rewards_menubar_height, 0); + db.set_image(Some(di)); + db.set_frame(FrameType::FlatBox); + db.set_down_frame(FrameType::DownBox); + db.set_tooltip("Delete the Reward"); + + logic(&mut db); + + db +} + +fn logic(db: &mut T) { + db.set_shortcut(Shortcut::from_char('d')); + db.set_callback(|_| {}); + db.handle({ + let mut lock = true; + let unselect_box = FrameType::FlatBox; + move |db, ev| match ev.bits() { + events::LOCK_THE_DELETE_A_REWARD_BUTTON => { + lock = true; + db.set_callback(|_| {}); + if logic::mouse_hovering_widget(db) { + logic::select_inactive(db); + } + true + } + events::UNLOCK_THE_DELETE_A_REWARD_BUTTON => { + lock = false; + db.set_callback(callback); + if logic::mouse_hovering_widget(db) { + logic::select_active(db); + } + true + } + _ => match ev { + Event::KeyDown => match app::event_key() { + Key::Left => logic::rp_handle_left(db, 1), + Key::Right => logic::rp_handle_right(db, 1), + _ => logic::handle_selection(db, ev, unselect_box, lock), + }, + _ => logic::handle_selection(db, ev, unselect_box, lock), + }, + } + }); +} + +fn callback(_: &mut T) { + app::handle_main(events::DELETE_A_REWARD).ok(); +} diff --git a/src/ui/rewards/menubar/divider.rs b/src/ui/rewards/menubar/divider.rs new file mode 100644 index 0000000..4bd54c6 --- /dev/null +++ b/src/ui/rewards/menubar/divider.rs @@ -0,0 +1,11 @@ +use fltk::{frame::Frame, image::SvgImage, prelude::*}; + +pub fn divider() -> Frame { + let mut di = SvgImage::from_data(include_str!("../../../../assets/menu/divider.svg")).unwrap(); + di.scale(8, 24, true, true); + + let mut d = Frame::default().with_size(10, 0); + d.set_image(Some(di)); + + d +} diff --git a/src/ui/rewards/menubar/edit_button.rs b/src/ui/rewards/menubar/edit_button.rs new file mode 100644 index 0000000..fa1083f --- /dev/null +++ b/src/ui/rewards/menubar/edit_button.rs @@ -0,0 +1,65 @@ +use fltk::{ + app, + button::Button, + enums::{Event, FrameType, Key, Shortcut}, + image::SvgImage, + prelude::*, +}; + +use crate::events; +use crate::ui::app::CONSTANTS; +use crate::ui::logic; + +pub fn edit_button() -> Button { + let mut ei = SvgImage::from_data(include_str!("../../../../assets/menu/pencil.svg")).unwrap(); + ei.scale(24, 24, true, true); + + let mut eb = Button::default().with_size(CONSTANTS.rewards_menubar_height, 0); + eb.set_image(Some(ei)); + eb.set_frame(FrameType::FlatBox); + eb.set_down_frame(FrameType::DownBox); + eb.set_tooltip("Edit the Reward"); + + logic(&mut eb); + + eb +} + +fn logic(eb: &mut T) { + eb.set_shortcut(Shortcut::from_char('e')); + eb.set_callback(|_| {}); + eb.handle({ + let mut lock = true; + let unselect_box = FrameType::FlatBox; + move |eb, ev| match ev.bits() { + events::LOCK_THE_EDIT_A_REWARD_BUTTON => { + lock = true; + eb.set_callback(|_| {}); + if logic::mouse_hovering_widget(eb) { + logic::select_inactive(eb); + } + true + } + events::UNLOCK_THE_EDIT_A_REWARD_BUTTON => { + lock = false; + eb.set_callback(callback); + if logic::mouse_hovering_widget(eb) { + logic::select_active(eb); + } + true + } + _ => match ev { + Event::KeyDown => match app::event_key() { + Key::Left => logic::rp_handle_left(eb, 2), + Key::Right => logic::rp_handle_right(eb, 3), + _ => logic::handle_selection(eb, ev, unselect_box, lock), + }, + _ => logic::handle_selection(eb, ev, unselect_box, lock), + }, + } + }); +} + +fn callback(_: &mut T) { + app::handle_main(events::EDIT_A_REWARD_SEND_ITEM).ok(); +} diff --git a/src/ui/rewards/menubar/mod.rs b/src/ui/rewards/menubar/mod.rs new file mode 100644 index 0000000..79ee0a6 --- /dev/null +++ b/src/ui/rewards/menubar/mod.rs @@ -0,0 +1,14 @@ +mod add_button; +mod delete_button; +mod divider; +mod edit_button; +mod new; +mod spend_button; + +pub use new::new; + +use add_button::add_button; +use delete_button::delete_button; +use divider::divider; +use edit_button::edit_button; +use spend_button::spend_button; diff --git a/src/ui/rewards/menubar/new.rs b/src/ui/rewards/menubar/new.rs new file mode 100644 index 0000000..5a54c7d --- /dev/null +++ b/src/ui/rewards/menubar/new.rs @@ -0,0 +1,54 @@ +use fltk::{ + app, + enums::{Event, Key}, + group::{Pack, PackType}, + prelude::*, +}; + +use super::{add_button, delete_button, divider, edit_button, spend_button}; +use crate::channels::Channels; +use crate::ui::app::CONSTANTS; + +pub fn new(channels: &Channels) -> Pack { + let mut m = Pack::default().with_size(0, CONSTANTS.rewards_menubar_height); + m.set_type(PackType::Horizontal); + + // 1. Add a Reward + let _ab = add_button(channels); + + // 2. Delete the Reward + let _db = delete_button(); + + // 3. Edit the Reward + let _eb = edit_button(); + + // 4. Divider + let _d = divider(); + + // 5. Spend Coins for the Reward + let _sb = spend_button(); + + m.end(); + + logic(&mut m); + + m +} + +fn logic(m: &mut T) { + m.handle(|m, ev| match ev { + Event::KeyDown => { + if app::event_key() == Key::Tab { + if let Some(ref p) = m.parent() { + if let Some(ref mut l) = p.child(1) { + l.take_focus().ok(); + } + } + true + } else { + false + } + } + _ => false, + }); +} diff --git a/src/ui/rewards/menubar/spend_button.rs b/src/ui/rewards/menubar/spend_button.rs new file mode 100644 index 0000000..b38b115 --- /dev/null +++ b/src/ui/rewards/menubar/spend_button.rs @@ -0,0 +1,66 @@ +use fltk::{ + app, + button::Button, + enums::{Event, FrameType, Key, Shortcut}, + image::SvgImage, + prelude::*, +}; + +use crate::events; +use crate::ui::app::CONSTANTS; +use crate::ui::logic; + +pub fn spend_button() -> Button { + let mut si = + SvgImage::from_data(include_str!("../../../../assets/menu/insert-coin.svg")).unwrap(); + si.scale(24, 24, true, true); + + let mut sb = Button::default().with_size(CONSTANTS.rewards_menubar_height, 0); + sb.set_image(Some(si)); + sb.set_frame(FrameType::FlatBox); + sb.set_down_frame(FrameType::DownBox); + sb.set_tooltip("Spend Coins for the Reward"); + + logic(&mut sb); + + sb +} + +fn logic(sb: &mut T) { + sb.set_shortcut(Shortcut::from_char('s')); + sb.set_callback(|_| {}); + sb.handle({ + let mut lock = true; + let unselect_box = FrameType::FlatBox; + move |sb, ev| match ev.bits() { + events::LOCK_THE_SPEND_BUTTON => { + lock = true; + sb.set_callback(|_| {}); + if logic::mouse_hovering_widget(sb) { + logic::select_inactive(sb); + } + true + } + events::UNLOCK_THE_SPEND_BUTTON => { + lock = false; + sb.set_callback(callback); + if logic::mouse_hovering_widget(sb) { + logic::select_active(sb); + } + true + } + _ => match ev { + Event::KeyDown => match app::event_key() { + Key::Left => logic::rp_handle_left(sb, 3), + Key::Right => logic::rp_handle_right(sb, 4), + _ => logic::handle_selection(sb, ev, unselect_box, lock), + }, + _ => logic::handle_selection(sb, ev, unselect_box, lock), + }, + } + }); +} + +fn callback(_: &mut T) { + app::handle_main(events::SPEND_COINS_SEND_PRICE).ok(); +} diff --git a/src/ui/rewards/mod.rs b/src/ui/rewards/mod.rs index ff285f9..bc8e0f4 100644 --- a/src/ui/rewards/mod.rs +++ b/src/ui/rewards/mod.rs @@ -5,6 +5,3 @@ mod menubar; mod pane; pub use pane::pane; - -use list::list; -use menubar::menubar; diff --git a/src/ui/rewards/pane.rs b/src/ui/rewards/pane.rs index cfc55a3..ad91244 100644 --- a/src/ui/rewards/pane.rs +++ b/src/ui/rewards/pane.rs @@ -15,8 +15,8 @@ pub fn pane(channels: &Channels) -> Pack { ); } - let _menubar = menubar(channels); - let list = list(channels); + let _menubar = menubar::new(channels); + let list = list::new(channels); p.resizable(list.scroll()); p.end(); diff --git a/src/ui/widgets/list.rs b/src/ui/widgets/list.rs index 6684e54..03118af 100644 --- a/src/ui/widgets/list.rs +++ b/src/ui/widgets/list.rs @@ -18,8 +18,8 @@ pub trait ScrollExt: GroupExt { impl ScrollExt for Scroll {} -type Selected = Rc>; -type Items = Rc>>; +pub type Selected = Rc>; +pub type Items = Rc>>; pub struct List { scroll: Scroll, @@ -48,7 +48,7 @@ impl List { items: Items::default(), }; w.draw(); - w.handle(|_, _, _, _| false); + w.handle(|_, _, _| {}, |_, _, _| {}, |_, _, _, _| false); w } @@ -119,29 +119,41 @@ impl List { }); } - pub fn handle(&mut self, handle_custom_events: F) - where - F: Fn(&mut Scroll, &mut i32, &mut Vec, i32) -> bool, + pub fn handle( + &mut self, + handle_push: A, + handle_key_down: B, + handle_custom_events: C, + ) where + A: Fn(&mut Scroll, &Selected, &Items), + B: Fn(&mut Scroll, &Selected, &Items), + C: Fn(&mut Scroll, &Selected, &Items, i32) -> bool, { self.scroll.handle({ let selected = Rc::clone(&self.selected); let items = Rc::clone(&self.items); move |s, ev| match ev { Event::Focus => true, - Event::Push => handle_push(s, &selected, &items), - Event::KeyDown => handle_key_down(s, &selected, &items), - _ => handle_custom_events( - s, - &mut *selected.borrow_mut(), - &mut *items.borrow_mut(), - ev.bits(), - ), + Event::Push => { + handle_push_default(s, &selected, &items); + handle_push(s, &selected, &items); + true + } + Event::KeyDown => { + if handle_key_down_default(s, &selected, &items) { + handle_key_down(s, &selected, &items); + true + } else { + false + } + } + _ => handle_custom_events(s, &selected, &items, ev.bits()), } }); } } -fn handle_push(s: &mut Scroll, selected: &Selected, items: &Items) -> bool { +fn handle_push_default(s: &mut Scroll, selected: &Selected, items: &Items) { s.take_focus().ok(); if let Some(l) = s.child(0) { @@ -150,23 +162,8 @@ fn handle_push(s: &mut Scroll, selected: &Selected, items: &Items) -> bool { let y = app::event_y(); let i_max = items.borrow().len() as i32; - if l.h() < s.h() - 20 { - for i in 0..i_max { - if y >= s.y() + i * 20 - ys as i32 && y <= s.y() + (i + 1) * 20 - ys { - if *selected.borrow() != i + 1 { - *selected.borrow_mut() = i + 1; - s.redraw(); - } - empty_space_click = false; - break; - } - } - } else { - let x = app::event_x(); - - if x >= s.x() + s.w() - 17 { - empty_space_click = false; - } else { + if i_max > 0 { + if l.h() < s.h() - 20 { for i in 0..i_max { if y >= s.y() + i * 20 - ys as i32 && y <= s.y() + (i + 1) * 20 - ys { if *selected.borrow() != i + 1 { @@ -177,19 +174,34 @@ fn handle_push(s: &mut Scroll, selected: &Selected, items: &Items) -> bool { break; } } + } else { + let x = app::event_x(); + + if x >= s.x() + s.w() - 17 { + empty_space_click = false; + } else { + for i in 0..i_max { + if y >= s.y() + i * 20 - ys as i32 && y <= s.y() + (i + 1) * 20 - ys { + if *selected.borrow() != i + 1 { + *selected.borrow_mut() = i + 1; + s.redraw(); + } + empty_space_click = false; + break; + } + } + } } - } - if empty_space_click && *selected.borrow() != i_max { - *selected.borrow_mut() = i_max; - s.redraw(); + if empty_space_click && *selected.borrow() != i_max { + *selected.borrow_mut() = i_max; + s.redraw(); + } } } - - true } -fn handle_key_down(s: &mut Scroll, selected: &Selected, items: &Items) -> bool { +fn handle_key_down_default(s: &mut Scroll, selected: &Selected, items: &Items) -> bool { match app::event_key() { Key::Up => { if *selected.borrow() == 0 || *selected.borrow() == 1 { diff --git a/src/ui/widgets/mod.rs b/src/ui/widgets/mod.rs index 6fc51ac..14fb12d 100644 --- a/src/ui/widgets/mod.rs +++ b/src/ui/widgets/mod.rs @@ -1,3 +1,3 @@ mod list; -pub use list::{List, ScrollExt}; +pub use list::{Items, List, ScrollExt, Selected}; From 3c7cced1927e7025cae12476486597b531b45f9f Mon Sep 17 00:00:00 2001 From: Pavel Sobolev Date: Sun, 25 Jul 2021 12:06:00 +0300 Subject: [PATCH 09/16] Rework the List widget; add tooltips for the items when needed. --- Cargo.lock | 16 +-- Cargo.toml | 2 +- src/ui/app.rs | 2 + src/ui/rates/list.rs | 2 +- src/ui/rewards/list.rs | 94 +++++++------ src/ui/widgets/list.rs | 226 -------------------------------- src/ui/widgets/list/holder.rs | 51 +++++++ src/ui/widgets/list/item.rs | 144 ++++++++++++++++++++ src/ui/widgets/list/items.rs | 73 +++++++++++ src/ui/widgets/list/mod.rs | 12 ++ src/ui/widgets/list/selected.rs | 44 +++++++ src/ui/widgets/list/widget.rs | 158 ++++++++++++++++++++++ src/ui/widgets/mod.rs | 4 +- 13 files changed, 550 insertions(+), 278 deletions(-) delete mode 100644 src/ui/widgets/list.rs create mode 100644 src/ui/widgets/list/holder.rs create mode 100644 src/ui/widgets/list/item.rs create mode 100644 src/ui/widgets/list/items.rs create mode 100644 src/ui/widgets/list/mod.rs create mode 100644 src/ui/widgets/list/selected.rs create mode 100644 src/ui/widgets/list/widget.rs diff --git a/Cargo.lock b/Cargo.lock index c8b5ecb..a97b519 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -51,9 +51,9 @@ dependencies = [ [[package]] name = "fltk" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22226bafa5d3a04f2223aea8083b101204d1b1070fbe7e140e90386026ffce9e" +checksum = "43ad3dcd38b2d09083c9ec0f3cfe640e3eab7015369e44cd441ba1361207e12b" dependencies = [ "bitflags", "fltk-derive", @@ -65,9 +65,9 @@ dependencies = [ [[package]] name = "fltk-derive" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4aadf3955b405fda705ae40916d06148a3f0d595b9a0518d4336fc5f8149e1c9" +checksum = "49579fb48d55e4a498b5c98e59fac28dbc5c92513ee80b52ff1ad9e17a04ae66" dependencies = [ "quote", "syn", @@ -75,9 +75,9 @@ dependencies = [ [[package]] name = "fltk-sys" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51890625127459c5b5100c889c84bd7a7672c91cd4a7bb0c443ce974322b0a2e" +checksum = "02b583e67ee6a049c208e1aac37d2ed481f95be8325fe1a14fa4012d1d653f62" dependencies = [ "cmake", "libc", @@ -150,9 +150,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.73" +version = "1.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7" +checksum = "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index ca74754..8c27fbe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,5 +29,5 @@ codegen-units = 1 panic = 'abort' [dependencies] -fltk = { version = "=1.1.2", features = ["fltk-bundled", "use-ninja"] } +fltk = { version = "=1.1.4", features = ["fltk-bundled", "use-ninja"] } crossbeam-channel = {version = "~0.5.1"} diff --git a/src/ui/app.rs b/src/ui/app.rs index e3c021a..4bfdd05 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -12,6 +12,7 @@ pub struct Constants { pub rewards_menubar_height: i32, pub rewards_edit_window_width: i32, pub rewards_edit_window_height: i32, + pub scrollbar_width: i32, } impl Constants { @@ -24,6 +25,7 @@ impl Constants { rewards_menubar_height: 30, rewards_edit_window_width: 320, rewards_edit_window_height: 140, + scrollbar_width: 17, } } } diff --git a/src/ui/rates/list.rs b/src/ui/rates/list.rs index 2e9dce9..462cde0 100644 --- a/src/ui/rates/list.rs +++ b/src/ui/rates/list.rs @@ -1,6 +1,6 @@ use fltk::prelude::*; -use crate::ui::widgets::List; +use crate::ui::widgets::list::List; pub fn list() -> List { let mut l = List::default(); diff --git a/src/ui/rewards/list.rs b/src/ui/rewards/list.rs index b892136..75eaf87 100644 --- a/src/ui/rewards/list.rs +++ b/src/ui/rewards/list.rs @@ -1,9 +1,13 @@ use crossbeam_channel::Sender; -use fltk::{app, group::Scroll, prelude::*}; +use fltk::{ + app, + group::{Pack, Scroll}, + prelude::*, +}; use crate::channels::Channels; use crate::events; -use crate::ui::widgets::{Items, List, ScrollExt, Selected}; +use crate::ui::widgets::list::{Holder, Items, List, Selected}; pub fn new(channels: &Channels) -> List { let mut l = List::default(); @@ -31,64 +35,74 @@ fn logic(l: &mut List, channels: &Channels) { fn handle_selection(channels: &Channels) -> impl Fn(&mut Scroll, &Selected, &Items) { let s_price = channels.rewards_receive_coins.s.clone(); - move |_, selected, items| { - handle_selection_closure(selected, items, &s_price); + move |_, selected, items| handle_selection_all(selected, items, &s_price) +} + +fn handle_selection_all(selected: &Selected, items: &Items, s_price: &Sender) { + if selected.get() > 0 && items.len() > 0 { + handle_selection_unlock_buttons(); + handle_selection_check_affordability(selected, items, s_price); } } -fn handle_selection_closure(selected: &Selected, items: &Items, s_price: &Sender) { - if *selected.borrow() > 0 && items.borrow().len() > 0 { - app::handle_main(events::UNLOCK_THE_DELETE_A_REWARD_BUTTON).ok(); - app::handle_main(events::UNLOCK_THE_EDIT_A_REWARD_BUTTON).ok(); - let index = *selected.borrow() as usize - 1; - if let Some(rb_i) = items.borrow()[index].find(')') { - if let Ok(price) = items.borrow()[index][1..rb_i].parse::() { - s_price.try_send(price).ok(); - app::handle_main(events::CHECK_AFFORDABILITY_RECEIVE_PRICE).ok(); - } +fn handle_selection_unlock_buttons() { + app::handle_main(events::UNLOCK_THE_DELETE_A_REWARD_BUTTON).ok(); + app::handle_main(events::UNLOCK_THE_EDIT_A_REWARD_BUTTON).ok(); +} + +fn handle_selection_lock_buttons() { + app::handle_main(events::LOCK_THE_DELETE_A_REWARD_BUTTON).ok(); + app::handle_main(events::LOCK_THE_EDIT_A_REWARD_BUTTON).ok(); + app::handle_main(events::LOCK_THE_SPEND_BUTTON).ok(); +} + +fn handle_selection_check_affordability(selected: &Selected, items: &Items, s_price: &Sender) { + let index = selected.index(); + if let Some(rb_i) = items.index(index).find(')') { + if let Ok(price) = items.index(index).index(1..rb_i).parse::() { + s_price.try_send(price).ok(); + app::handle_main(events::CHECK_AFFORDABILITY_RECEIVE_PRICE).ok(); } } } fn handle_custom_events( channels: &Channels, -) -> impl Fn(&mut Scroll, &Selected, &Items, i32) -> bool { +) -> impl Fn(&mut Pack, &mut Selected, &mut Items, i32) -> bool { let s_re = channels.rewards_edit.s.clone(); let s_price = channels.rewards_receive_coins.s.clone(); let s_item = channels.rewards_receive_item.s.clone(); let r_item = channels.rewards_send_item.r.clone(); - move |s, selected, items, bits| match bits { - events::ADD_A_REWARD_RECEIVE => r_item.try_recv().map_or(false, |item| { - items.borrow_mut().push(item); - s.resize_list(items.borrow().len() as i32); + move |h, selected, items, bits| match bits { + events::ADD_A_REWARD_RECEIVE => r_item.try_recv().map_or(false, |string| { + Holder::add_to(h, string, items); true }), events::DELETE_A_REWARD => { - if *selected.borrow() > 0 { - let index = *selected.borrow() as usize - 1; - if *selected.borrow() == items.borrow().len() as i32 { - *selected.borrow_mut() -= 1; + if selected.get() > 0 { + let index = selected.index(); + if selected.get() == items.len() { + selected.decrement(); } - if items.borrow().len() == 1 { - app::handle_main(events::LOCK_THE_DELETE_A_REWARD_BUTTON).ok(); - app::handle_main(events::LOCK_THE_EDIT_A_REWARD_BUTTON).ok(); - app::handle_main(events::LOCK_THE_SPEND_BUTTON).ok(); + if items.len() == 1 { + handle_selection_lock_buttons(); } else { - handle_selection_closure(selected, items, &s_price); + handle_selection_all(selected, items, &s_price); } - items.borrow_mut().remove(index); - s.resize_list(items.borrow().len() as i32); + Holder::remove(h, index, items); + items.select(selected.get()); } true } - events::EDIT_A_REWARD_RECEIVE => r_item.try_recv().map_or(false, |item| { - items.borrow_mut()[*selected.borrow() as usize - 1] = item; - s.resize_list(items.borrow().len() as i32); + events::EDIT_A_REWARD_RECEIVE => r_item.try_recv().map_or(false, |string| { + items.index_mut(selected.index()).set(string); + handle_selection_check_affordability(selected, items, &s_price); + h.redraw(); true }), events::EDIT_A_REWARD_SEND_ITEM => { - if *selected.borrow() > 0 { - let string = items.borrow()[*selected.borrow() as usize - 1].clone(); + if selected.get() > 0 { + let string = items.index(selected.index()).clone(); s_item.try_send(string).ok(); s_re.try_send(events::EDIT_A_REWARD_OPEN).ok(); s_re.try_send(events::OK_BUTTON_SET_TO_EDIT).ok(); @@ -96,10 +110,12 @@ fn handle_custom_events( true } events::SPEND_COINS_SEND_PRICE => { - if *selected.borrow() > 0 { - let index = *selected.borrow() as usize - 1; - let rb_i = items.borrow()[index].find(')').unwrap_or_default(); - let price = items.borrow()[index][1..rb_i] + if selected.get() > 0 { + let index = selected.index(); + let rb_i = items.index(index).find(')').unwrap_or_default(); + let price = items + .index(index) + .index(1..rb_i) .parse::() .unwrap_or(-1.0); if price >= 0.0 { diff --git a/src/ui/widgets/list.rs b/src/ui/widgets/list.rs deleted file mode 100644 index 03118af..0000000 --- a/src/ui/widgets/list.rs +++ /dev/null @@ -1,226 +0,0 @@ -use fltk::{ - app, draw, - enums::{Align, Color, Event, FrameType, Key}, - group::{Group, Scroll, ScrollType}, - prelude::*, - widget::Widget, -}; -use std::{cell::RefCell, rc::Rc}; - -pub trait ScrollExt: GroupExt { - fn resize_list(&mut self, n: i32) { - if let Some(ref mut l) = self.child(0) { - l.resize(self.x() + 1, self.y() + 1, self.w() - 2, n * 20); - self.redraw(); - } - } -} - -impl ScrollExt for Scroll {} - -pub type Selected = Rc>; -pub type Items = Rc>>; - -pub struct List { - scroll: Scroll, - list: Widget, - selected: Selected, - items: Items, -} - -impl List { - pub fn default() -> List { - let mut scroll = Scroll::default(); - scroll.set_frame(FrameType::BorderBox); - scroll.set_type(ScrollType::Vertical); - scroll.set_scrollbar_size(17); - - let mut list = Widget::default(); - list.set_color(Color::White); - list.set_selection_color(Color::DarkBlue); - - scroll.end(); - - let mut w = List { - scroll, - list, - selected: Selected::default(), - items: Items::default(), - }; - w.draw(); - w.handle(|_, _, _| {}, |_, _, _| {}, |_, _, _, _| false); - w - } - - pub fn scroll(&self) -> &Scroll { - &self.scroll - } - - pub fn parent(&self) -> Option { - self.scroll.parent() - } - - pub fn set_size(&mut self, width: i32, height: i32) { - if width == 0 { - if let Some(p) = self.parent() { - self.scroll.set_size(p.w(), height); - } - } else { - self.scroll.set_size(width, height); - } - } - - pub fn add(&mut self, s: &'static str) { - self.items.borrow_mut().push(String::from(s)); - self.scroll.resize_list(self.items.borrow().len() as i32); - } - - pub fn select(&mut self, idx: i32) { - if idx >= 0 { - *self.selected.borrow_mut() = idx; - } - } - - fn draw(&mut self) { - let selected = Rc::clone(&self.selected); - let items = Rc::clone(&self.items); - self.list.draw(move |l| { - let lw = f64::from(l.w()); - let dw = draw::width("..."); - let color = draw::get_color(); - for (idx, s) in (0..).zip(items.borrow().iter()) { - if idx + 1 == *selected.borrow() { - draw::draw_rect_fill(l.x(), l.y() + idx * 20, l.w(), 20, l.selection_color()); - draw::set_draw_color(Color::White); - draw::set_font(draw::font(), 16); - } else { - draw::set_font(draw::font(), 16); - draw::set_draw_color(Color::Black); - } - - if draw::width(s) < lw { - draw::draw_text2(s, l.x() + 4, l.y() + idx * 20, l.w() - 8, 20, Align::Left); - } else { - let mut n = s.len(); - while draw::width(&s[..n]) + dw > lw - 8.0 { - n -= 1; - } - draw::draw_text2( - &format!("{}...", &s[..n]), - l.x() + 4, - l.y() + idx * 20, - l.w() - 8, - 20, - Align::Left, - ); - } - } - draw::set_draw_color(color); - }); - } - - pub fn handle( - &mut self, - handle_push: A, - handle_key_down: B, - handle_custom_events: C, - ) where - A: Fn(&mut Scroll, &Selected, &Items), - B: Fn(&mut Scroll, &Selected, &Items), - C: Fn(&mut Scroll, &Selected, &Items, i32) -> bool, - { - self.scroll.handle({ - let selected = Rc::clone(&self.selected); - let items = Rc::clone(&self.items); - move |s, ev| match ev { - Event::Focus => true, - Event::Push => { - handle_push_default(s, &selected, &items); - handle_push(s, &selected, &items); - true - } - Event::KeyDown => { - if handle_key_down_default(s, &selected, &items) { - handle_key_down(s, &selected, &items); - true - } else { - false - } - } - _ => handle_custom_events(s, &selected, &items, ev.bits()), - } - }); - } -} - -fn handle_push_default(s: &mut Scroll, selected: &Selected, items: &Items) { - s.take_focus().ok(); - - if let Some(l) = s.child(0) { - let mut empty_space_click = true; - let ys = s.yposition(); - let y = app::event_y(); - let i_max = items.borrow().len() as i32; - - if i_max > 0 { - if l.h() < s.h() - 20 { - for i in 0..i_max { - if y >= s.y() + i * 20 - ys as i32 && y <= s.y() + (i + 1) * 20 - ys { - if *selected.borrow() != i + 1 { - *selected.borrow_mut() = i + 1; - s.redraw(); - } - empty_space_click = false; - break; - } - } - } else { - let x = app::event_x(); - - if x >= s.x() + s.w() - 17 { - empty_space_click = false; - } else { - for i in 0..i_max { - if y >= s.y() + i * 20 - ys as i32 && y <= s.y() + (i + 1) * 20 - ys { - if *selected.borrow() != i + 1 { - *selected.borrow_mut() = i + 1; - s.redraw(); - } - empty_space_click = false; - break; - } - } - } - } - - if empty_space_click && *selected.borrow() != i_max { - *selected.borrow_mut() = i_max; - s.redraw(); - } - } - } -} - -fn handle_key_down_default(s: &mut Scroll, selected: &Selected, items: &Items) -> bool { - match app::event_key() { - Key::Up => { - if *selected.borrow() == 0 || *selected.borrow() == 1 { - *selected.borrow_mut() = items.borrow().len() as i32; - } else { - *selected.borrow_mut() -= 1; - } - s.redraw(); - true - } - Key::Down => { - if *selected.borrow() == items.borrow().len() as i32 { - *selected.borrow_mut() = 1; - } else { - *selected.borrow_mut() += 1; - } - s.redraw(); - true - } - _ => false, - } -} diff --git a/src/ui/widgets/list/holder.rs b/src/ui/widgets/list/holder.rs new file mode 100644 index 0000000..603f423 --- /dev/null +++ b/src/ui/widgets/list/holder.rs @@ -0,0 +1,51 @@ +use fltk::{enums::Event, group::Pack, prelude::*}; + +use super::{Item, Items}; + +pub struct Holder { + holder: Pack, +} + +impl Holder { + pub fn default() -> Self { + Holder { + holder: Pack::default(), + } + } + + pub fn handle bool + 'static>(&mut self, cb: F) { + self.holder.handle(cb) + } + + fn resize(h: &mut Pack, n: usize) { + if let Some(ref mut s) = h.parent() { + h.resize(s.x() + 1, s.y() + 1, s.w() - 2, n as i32 * 20); + } + } + + pub fn add_to(h: &mut Pack, string: String, items: &Items) { + Self::resize(h, items.len() + 1); + + h.begin(); + let item = Item::new(string); + h.end(); + + h.add(item.frame()); + items.push(item); + + h.redraw() + } + + pub fn add(&mut self, string: String, items: &Items) { + Self::add_to(&mut self.holder, string, items) + } + + pub fn remove(h: &mut Pack, index: usize, items: &mut Items) { + h.remove(items.index(index).frame()); + items.remove(index); + Self::resize(h, items.len()); + if let Some(ref mut s) = h.parent() { + s.redraw(); + } + } +} diff --git a/src/ui/widgets/list/item.rs b/src/ui/widgets/list/item.rs new file mode 100644 index 0000000..69e1d24 --- /dev/null +++ b/src/ui/widgets/list/item.rs @@ -0,0 +1,144 @@ +use fltk::{ + draw, + enums::{Align, Color, FrameType}, + frame::Frame, + prelude::*, +}; +use std::{ + cell::{Ref, RefCell, RefMut}, + ops::Range, + rc::Rc, +}; + +use crate::ui::app::CONSTANTS; +const TEXT_PADDING: i32 = 4; + +type ItemString = Rc>; +type ItemIsSelected = Rc>; +type ItemLabel = Rc>; + +pub struct Item { + frame: Frame, + string: ItemString, + selected: ItemIsSelected, + label: ItemLabel, +} + +impl Item { + pub fn new(string: String) -> Self { + let mut frame = Frame::default(); + frame.set_frame(FrameType::FlatBox); + + let selected = ItemIsSelected::default(); + let label = ItemLabel::default(); + + if let Some(ref h) = frame.parent() { + frame.set_size(h.w(), 20); + Self::update_label_for(&mut frame, &label, string.clone()); + frame.draw({ + let selected = Rc::clone(&selected); + let label = Rc::clone(&label); + move |f| { + let color = draw::get_color(); + if *selected.borrow() { + draw::set_draw_color(Color::White); + draw::set_font(draw::font(), 16); + } else { + draw::set_font(draw::font(), 16); + draw::set_draw_color(Color::Black); + } + draw::draw_text2( + &*label.borrow(), + f.x() + TEXT_PADDING, + f.y(), + f.w() - 2 * TEXT_PADDING - CONSTANTS.scrollbar_width, + f.h(), + Align::Left, + ); + draw::set_draw_color(color); + } + }); + } + + Item { + frame, + string: ItemString::new(RefCell::new(string)), + selected, + label, + } + } + + pub fn frame(&self) -> &Frame { + &self.frame + } + + fn string(&self) -> Ref { + self.string.borrow() + } + + fn string_mut(&self) -> RefMut { + self.string.borrow_mut() + } + + pub fn clone(&self) -> String { + self.string().clone() + } + + pub fn selected(&self) -> bool { + *self.selected.borrow() + } + + fn selected_mut(&self) -> RefMut { + self.selected.borrow_mut() + } + + pub fn find(&self, pat: char) -> Option { + self.string().find(pat) + } + + pub fn set(&mut self, string: String) { + *self.string_mut() = string.clone(); + self.update_label(string); + } + + fn update_label(&mut self, string: String) { + Self::update_label_for(&mut self.frame, &self.label, string) + } + + fn update_label_for(f: &mut Frame, l: &ItemLabel, string: String) { + draw::set_font(draw::font(), 16); + + let fw = f64::from(f.w()); + let sw = draw::width(&string); + let dw = draw::width("..."); + let cw = fw - f64::from(TEXT_PADDING) - f64::from(CONSTANTS.scrollbar_width); + + if cw > 0.0 { + if sw < cw { + f.set_tooltip(""); + *l.borrow_mut() = string; + } else { + let mut n = string.len(); + while draw::width(&string[..n]) + dw > cw { + n -= 1; + } + f.set_tooltip(&string); + *l.borrow_mut() = string[..n].to_string() + "..."; + } + } + } + + pub fn index(&self, index: Range) -> Ref { + Ref::map(self.string(), |items| &items[index]) + } + + pub fn select(&mut self) { + *self.selected_mut() = true; + self.frame.set_color(Color::DarkBlue); + } + + pub fn unselect(&mut self) { + *self.selected_mut() = false; + self.frame.set_color(Color::BackGround); + } +} diff --git a/src/ui/widgets/list/items.rs b/src/ui/widgets/list/items.rs new file mode 100644 index 0000000..54765d6 --- /dev/null +++ b/src/ui/widgets/list/items.rs @@ -0,0 +1,73 @@ +use std::{ + cell::{Ref, RefCell, RefMut}, + rc::Rc, +}; + +use super::Item; + +pub type Array = Rc>>; + +pub struct Items { + items: Rc>>, +} + +impl Items { + pub fn default() -> Self { + Items { + items: Array::default(), + } + } + + fn items(&self) -> Ref> { + self.items.borrow() + } + + fn items_mut(&self) -> RefMut> { + self.items.borrow_mut() + } + + pub fn len(&self) -> usize { + self.items().len() + } + + pub fn index(&self, index: usize) -> Ref { + Ref::map(self.items(), |items| &items[index]) + } + + pub fn index_mut(&self, index: usize) -> RefMut { + RefMut::map(self.items_mut(), |items| &mut items[index]) + } + + pub fn push(&self, item: Item) { + self.items_mut().push(item); + } + + pub fn select(&self, idx: usize) { + if idx > 0 { + let idx = idx - 1; + for i in 0..self.len() { + if i == idx { + self.index_mut(i).select(); + } else if self.index(i).selected() { + self.index_mut(i).unselect(); + } + } + } else { + for i in 0..self.len() { + self.index_mut(i).unselect(); + } + } + } + + pub fn remove(&self, index: usize) { + self.items_mut().remove(index); + } +} + +impl Clone for Items { + fn clone(&self) -> Self { + Items { + items: Rc::clone(&self.items), + } + } +} diff --git a/src/ui/widgets/list/mod.rs b/src/ui/widgets/list/mod.rs new file mode 100644 index 0000000..e435bbc --- /dev/null +++ b/src/ui/widgets/list/mod.rs @@ -0,0 +1,12 @@ +mod holder; +mod item; +mod items; +mod selected; +mod widget; + +pub use holder::Holder; +pub use items::Items; +pub use selected::Selected; +pub use widget::List; + +use item::Item; diff --git a/src/ui/widgets/list/selected.rs b/src/ui/widgets/list/selected.rs new file mode 100644 index 0000000..d7b8571 --- /dev/null +++ b/src/ui/widgets/list/selected.rs @@ -0,0 +1,44 @@ +use std::{cell::RefCell, rc::Rc}; + +type Index = usize; +type IndexRc = Rc>; + +pub struct Selected { + selected: IndexRc, +} + +impl Selected { + pub fn default() -> Self { + Selected { + selected: IndexRc::default(), + } + } + + pub fn get(&self) -> Index { + *self.selected.borrow() + } + + pub fn index(&self) -> Index { + self.get() - 1 + } + + pub fn decrement(&mut self) { + self.set(self.get() - 1) + } + + pub fn increment(&mut self) { + self.set(self.get() + 1) + } + + pub fn set(&self, idx: Index) { + *self.selected.borrow_mut() = idx; + } +} + +impl Clone for Selected { + fn clone(&self) -> Self { + Selected { + selected: Rc::clone(&self.selected), + } + } +} diff --git a/src/ui/widgets/list/widget.rs b/src/ui/widgets/list/widget.rs new file mode 100644 index 0000000..8e0bb97 --- /dev/null +++ b/src/ui/widgets/list/widget.rs @@ -0,0 +1,158 @@ +use fltk::{ + app, + enums::{Event, FrameType, Key}, + group::{Group, Pack, Scroll, ScrollType}, + prelude::*, +}; + +use super::{Holder, Items, Selected}; +use crate::ui::app::CONSTANTS; + +pub struct List { + scroll: Scroll, + holder: Holder, + selected: Selected, + items: Items, +} + +impl List { + pub fn default() -> List { + let mut scroll = Scroll::default(); + scroll.set_frame(FrameType::BorderBox); + scroll.set_type(ScrollType::Vertical); + scroll.set_scrollbar_size(CONSTANTS.scrollbar_width); + + let holder = Holder::default(); + + scroll.end(); + + let mut w = List { + scroll, + holder, + selected: Selected::default(), + items: Items::default(), + }; + w.handle(|_, _, _| {}, |_, _, _| {}, |_, _, _, _| false); + w + } + + pub fn scroll(&self) -> &Scroll { + &self.scroll + } + + pub fn parent(&self) -> Option { + self.scroll.parent() + } + + pub fn set_size(&mut self, width: i32, height: i32) { + if width == 0 { + if let Some(p) = self.parent() { + self.scroll.set_size(p.w(), height); + } + } else { + self.scroll.set_size(width, height); + } + } + + pub fn add(&mut self, s: &'static str) { + self.holder.add(s.to_string(), &self.items); + } + + pub fn select_in(selected: &Selected, items: &Items, idx: usize) { + selected.set(idx); + items.select(idx); + } + + pub fn select(&self, idx: usize) { + Self::select_in(&self.selected, &self.items, idx) + } + + pub fn handle( + &mut self, + handle_push: A, + handle_key_down: B, + handle_custom_events: C, + ) where + A: Fn(&mut Scroll, &Selected, &Items), + B: Fn(&mut Scroll, &Selected, &Items), + C: Fn(&mut Pack, &mut Selected, &mut Items, i32) -> bool, + { + self.scroll.handle({ + let mut selected = self.selected.clone(); + let mut items = self.items.clone(); + move |s, ev| match ev { + Event::Focus => true, + Event::Push => { + handle_push_default(s, &mut selected, &mut items); + handle_push(s, &selected, &items); + true + } + Event::KeyDown => { + if handle_key_down_default(s, &mut selected, &mut items) { + handle_key_down(s, &selected, &items); + true + } else { + false + } + } + _ => false, + } + }); + + self.holder.handle({ + let mut selected = self.selected.clone(); + let mut items = self.items.clone(); + move |l, ev| handle_custom_events(l, &mut selected, &mut items, ev.bits()) + }) + } +} + +fn handle_push_default(s: &mut Scroll, selected: &mut Selected, items: &mut Items) { + s.take_focus().ok(); + + if let Some(l) = s.child(0) { + // Exclude scrollbar clicks + if l.h() < s.h() - 20 + || (l.h() >= s.h() - 20 && app::event_x() < s.x() + s.w() - CONSTANTS.scrollbar_width) + { + let idx = ((app::event_y() - s.y() + s.yposition()) / 20) as usize + 1; + + if idx < items.len() { + selected.set(idx); + } else { + selected.set(items.len()); + } + items.select(selected.get()); + + s.redraw(); + } + } +} + +fn handle_key_down_default(s: &mut Scroll, selected: &mut Selected, items: &mut Items) -> bool { + match app::event_key() { + Key::Up => { + if selected.get() == 0 || selected.get() == 1 { + selected.set(items.len()); + } else { + selected.decrement(); + } + items.select(selected.get()); + + s.redraw(); + true + } + Key::Down => { + if selected.get() == items.len() { + selected.set(1); + } else { + selected.increment(); + } + items.select(selected.get()); + s.redraw(); + + true + } + _ => false, + } +} diff --git a/src/ui/widgets/mod.rs b/src/ui/widgets/mod.rs index 14fb12d..d17e233 100644 --- a/src/ui/widgets/mod.rs +++ b/src/ui/widgets/mod.rs @@ -1,3 +1 @@ -mod list; - -pub use list::{Items, List, ScrollExt, Selected}; +pub mod list; From d0bf0188bec5bb7a2cfdfc51fe08b6e904d0d1b5 Mon Sep 17 00:00:00 2001 From: Pavel Sobolev Date: Sun, 25 Jul 2021 12:34:39 +0300 Subject: [PATCH 10/16] Add `Enter` handles for the Coins and Reward in the Rewards Edit window. --- src/events.rs | 1 + src/ui/rewards/edit/coins.rs | 11 ++++++++++- src/ui/rewards/edit/ok.rs | 4 ++++ src/ui/rewards/edit/reward.rs | 12 +++++++++++- 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/events.rs b/src/events.rs index f02a0f9..ecb70fb 100644 --- a/src/events.rs +++ b/src/events.rs @@ -18,6 +18,7 @@ events!( START_TIMER, TICK, STOP_TIMER, + OK_BUTTON_DO_CALLBACK, OK_BUTTON_SET_TO_ADD, ADD_A_REWARD_OPEN, ADD_A_REWARD_SEND_COINS, diff --git a/src/ui/rewards/edit/coins.rs b/src/ui/rewards/edit/coins.rs index 0128efb..f9e2981 100644 --- a/src/ui/rewards/edit/coins.rs +++ b/src/ui/rewards/edit/coins.rs @@ -1,6 +1,6 @@ use fltk::{ app, - enums::{Align, CallbackTrigger, Event, FrameType}, + enums::{Align, CallbackTrigger, Event, FrameType, Key}, prelude::*, valuator::ValueInput, }; @@ -43,6 +43,15 @@ fn logic(v: &mut T, channels: &Channels) { let s_coins = channels.rewards_send_coins.s.clone(); let r_coins = channels.rewards_receive_coins.r.clone(); move |v, ev| match ev { + Event::KeyDown => app::event_key() == Key::Enter, + Event::KeyUp => { + if app::event_key() == Key::Enter { + app::handle_main(events::OK_BUTTON_DO_CALLBACK).ok(); + true + } else { + false + } + } Event::Hide => { v.set_value(0.0); true diff --git a/src/ui/rewards/edit/ok.rs b/src/ui/rewards/edit/ok.rs index b9dff34..b4853dd 100644 --- a/src/ui/rewards/edit/ok.rs +++ b/src/ui/rewards/edit/ok.rs @@ -15,6 +15,10 @@ pub fn ok() -> Button { fn logic(b: &mut T) { b.set_callback(add_callback); b.handle(|b, ev| match ev.bits() { + events::OK_BUTTON_DO_CALLBACK => { + b.do_callback(); + true + } events::OK_BUTTON_SET_TO_ADD => { b.set_callback(add_callback); true diff --git a/src/ui/rewards/edit/reward.rs b/src/ui/rewards/edit/reward.rs index 8e90c63..6d6ce8e 100644 --- a/src/ui/rewards/edit/reward.rs +++ b/src/ui/rewards/edit/reward.rs @@ -1,5 +1,6 @@ use fltk::{ - enums::{Align, Event, FrameType}, + app, + enums::{Align, Event, FrameType, Key}, input::Input, prelude::*, }; @@ -33,6 +34,15 @@ fn logic(i: &mut T, channels: &Channels) { let s_item = channels.rewards_send_item.s.clone(); let s_mw = channels.mw.s.clone(); move |i, ev| match ev { + Event::KeyDown => app::event_key() == Key::Enter, + Event::KeyUp => { + if app::event_key() == Key::Enter { + app::handle_main(events::OK_BUTTON_DO_CALLBACK).ok(); + true + } else { + false + } + } Event::Hide => { i.set_value(""); true From 478c4b116e59b89f02787de3d09824ee4619a07a Mon Sep 17 00:00:00 2001 From: Pavel Sobolev Date: Sun, 25 Jul 2021 17:05:24 +0300 Subject: [PATCH 11/16] Change the size range when hiding / showing the pane. --- .github/workflows/debug-build.yml | 1 + src/events.rs | 2 ++ src/ui/focus/arrow.rs | 11 ++----- src/ui/focus/coins.rs | 10 ++----- src/ui/windows/main.rs | 49 ++++++++++++++++++++++++------- 5 files changed, 47 insertions(+), 26 deletions(-) diff --git a/.github/workflows/debug-build.yml b/.github/workflows/debug-build.yml index 1db097c..551eb02 100644 --- a/.github/workflows/debug-build.yml +++ b/.github/workflows/debug-build.yml @@ -15,6 +15,7 @@ jobs: ( startsWith(matrix.os, 'windows') && 'Windows' ) || ( startsWith(matrix.os, 'mac') && 'macOS' ) }} strategy: + fail-fast: false matrix: # os: [windows-latest, macos-10.15, ubuntu-18.04] os: [windows-latest] diff --git a/src/events.rs b/src/events.rs index ecb70fb..a07f307 100644 --- a/src/events.rs +++ b/src/events.rs @@ -15,6 +15,8 @@ macro_rules! events { } events!( + MAIN_WINDOW_HIDE_THE_PANE, + MAIN_WINDOW_SHOW_THE_PANE, START_TIMER, TICK, STOP_TIMER, diff --git a/src/ui/focus/arrow.rs b/src/ui/focus/arrow.rs index 141e333..7e78089 100644 --- a/src/ui/focus/arrow.rs +++ b/src/ui/focus/arrow.rs @@ -7,7 +7,7 @@ use fltk::{ prelude::*, }; -use crate::ui::app::CONSTANTS; +use crate::events; use crate::ui::logic; pub fn arrow() -> Button { @@ -63,16 +63,11 @@ fn logic(a: &mut T) { ra_p.show(); a.set_tooltip("Hide the Conversion Rates pane"); } else if ra_p.visible() { - w.resize( - w.x(), - w.y(), - w.w(), - 10 + CONSTANTS.focus_pane_height + 10, - ); + app::handle_main(events::MAIN_WINDOW_HIDE_THE_PANE).ok(); ra_p.hide(); a.set_tooltip("Show the Conversion Rates pane"); } else { - w.resize(w.x(), w.y(), w.w(), CONSTANTS.main_window_height); + app::handle_main(events::MAIN_WINDOW_SHOW_THE_PANE).ok(); ra_p.show(); a.set_tooltip("Hide the Conversion Rates pane"); } diff --git a/src/ui/focus/coins.rs b/src/ui/focus/coins.rs index 73e7860..f8a2781 100644 --- a/src/ui/focus/coins.rs +++ b/src/ui/focus/coins.rs @@ -9,7 +9,6 @@ use fltk::{ use crate::channels::Channels; use crate::events; -use crate::ui::app::CONSTANTS; use crate::ui::logic; pub fn coins(channels: &Channels) -> Button { @@ -79,16 +78,11 @@ fn logic(c: &mut T, channels: &Channels) { re_p.show(); c.set_tooltip("Hide the Rewards pane"); } else if re_p.visible() { - w.resize( - w.x(), - w.y(), - w.w(), - 10 + CONSTANTS.focus_pane_height + 10, - ); + app::handle_main(events::MAIN_WINDOW_HIDE_THE_PANE).ok(); re_p.hide(); c.set_tooltip("Show the Rewards pane"); } else { - w.resize(w.x(), w.y(), w.w(), CONSTANTS.main_window_height); + app::handle_main(events::MAIN_WINDOW_SHOW_THE_PANE).ok(); re_p.show(); c.set_tooltip("Hide the Rewards pane"); } diff --git a/src/ui/windows/main.rs b/src/ui/windows/main.rs index c17dca1..7a79f8c 100644 --- a/src/ui/windows/main.rs +++ b/src/ui/windows/main.rs @@ -2,10 +2,8 @@ use fltk::{prelude::*, window::Window}; use super::icon; use crate::channels::Channels; -use crate::ui::app::CONSTANTS; -use crate::ui::focus; -use crate::ui::rates; -use crate::ui::rewards; +use crate::events; +use crate::ui::{app::CONSTANTS, focus, rates, rewards}; /// Create the Main Window pub fn main(channels: &Channels) -> Window { @@ -16,12 +14,7 @@ pub fn main(channels: &Channels) -> Window { CONSTANTS.main_window_height, "Shuchu", ); - w.size_range( - CONSTANTS.main_window_width, - CONSTANTS.main_window_height, - CONSTANTS.main_window_width, - CONSTANTS.main_window_height + 100, - ); + expand(&mut w); w.set_icon(Some(icon())); // 1. Focus Pane @@ -36,6 +29,42 @@ pub fn main(channels: &Channels) -> Window { w.resizable(&re_p); w.end(); + logic(&mut w); + w.show(); w } + +fn logic(w: &mut T) { + w.handle(|w, ev| match ev.bits() { + events::MAIN_WINDOW_HIDE_THE_PANE => { + w.resize(w.x(), w.y(), w.w(), 10 + CONSTANTS.focus_pane_height + 10); + shrink(w); + true + } + events::MAIN_WINDOW_SHOW_THE_PANE => { + w.resize(w.x(), w.y(), w.w(), CONSTANTS.main_window_height); + expand(w); + true + } + _ => false, + }); +} + +fn shrink(w: &mut T) { + w.size_range( + CONSTANTS.main_window_width, + 10 + CONSTANTS.focus_pane_height + 10, + CONSTANTS.main_window_width, + 10 + CONSTANTS.focus_pane_height + 10, + ); +} + +fn expand(w: &mut T) { + w.size_range( + CONSTANTS.main_window_width, + CONSTANTS.main_window_height, + CONSTANTS.main_window_width, + CONSTANTS.main_window_height + 100, + ); +} From fbf21cab4b2ae0c9d61ddc23760b253bc5d439c3 Mon Sep 17 00:00:00 2001 From: Pavel Sobolev Date: Wed, 28 Jul 2021 15:01:23 +0300 Subject: [PATCH 12/16] Make the Up and Down buttons change the scroll position in the list if needed. --- src/ui/app.rs | 4 +++- src/ui/focus/pane.rs | 6 ++++-- src/ui/rates/list.rs | 5 ++++- src/ui/rates/pane.rs | 4 +++- src/ui/rewards/list.rs | 5 ++++- src/ui/rewards/pane.rs | 4 +++- src/ui/widgets/list/item.rs | 10 ++++++++++ src/ui/widgets/list/items.rs | 15 +++++++++++++++ src/ui/widgets/list/widget.rs | 34 +++++++++++++++++++++++++++++++--- 9 files changed, 77 insertions(+), 10 deletions(-) diff --git a/src/ui/app.rs b/src/ui/app.rs index 4bfdd05..75661cb 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -20,7 +20,9 @@ impl Constants { const fn default() -> Constants { Constants { main_window_width: 340, - main_window_height: 300, + // These 3 pixels are the 1px spacing in Rewards / Rates panes and + // 1px top and bottom borders of the Scroll widget in the custom List widget + main_window_height: 300 + 3, focus_pane_height: 60, rewards_menubar_height: 30, rewards_edit_window_width: 320, diff --git a/src/ui/focus/pane.rs b/src/ui/focus/pane.rs index 37f65f3..69fac5a 100644 --- a/src/ui/focus/pane.rs +++ b/src/ui/focus/pane.rs @@ -13,8 +13,10 @@ pub fn pane(channels: &Channels) -> Group { let mut p = Group::default().with_pos(10, 10); p.set_frame(FrameType::BorderBox); - if let Some(ref parent) = p.parent() { - p.set_size(parent.width() - 20, CONSTANTS.focus_pane_height); + // If this pane is a child of the Main Window + if let Some(ref w) = p.parent() { + // Set the size, excluding the margins + p.set_size(w.width() - 20, CONSTANTS.focus_pane_height); } // The order of the widgets is important diff --git a/src/ui/rates/list.rs b/src/ui/rates/list.rs index 462cde0..fdc4ead 100644 --- a/src/ui/rates/list.rs +++ b/src/ui/rates/list.rs @@ -1,12 +1,15 @@ use fltk::prelude::*; +use crate::ui::app::CONSTANTS; use crate::ui::widgets::list::List; pub fn list() -> List { let mut l = List::default(); + // If this list is a child of the Rates pane if let Some(ref p) = l.parent() { - l.set_size(0, p.h() - 20); + // Set the list's height to everything except the Menubar's height + l.set_size(0, p.h() - CONSTANTS.rewards_menubar_height); } l.add("5/m"); diff --git a/src/ui/rates/pane.rs b/src/ui/rates/pane.rs index 0943d17..18ffda3 100644 --- a/src/ui/rates/pane.rs +++ b/src/ui/rates/pane.rs @@ -7,10 +7,12 @@ pub fn pane() -> Pack { let mut p = Pack::default().with_pos(10, 10 + CONSTANTS.focus_pane_height + 10); p.set_spacing(1); + // If this pane is a child of the Main Window if let Some(ref w) = p.parent() { + // Set the size, excluding the pane's margins and the height taken by the Focus Pane p.set_size( w.width() - 20, - w.height() - CONSTANTS.focus_pane_height - CONSTANTS.rewards_menubar_height, + w.height() - 10 - CONSTANTS.focus_pane_height - 10 - 10, ); } diff --git a/src/ui/rewards/list.rs b/src/ui/rewards/list.rs index 75eaf87..444348f 100644 --- a/src/ui/rewards/list.rs +++ b/src/ui/rewards/list.rs @@ -7,13 +7,16 @@ use fltk::{ use crate::channels::Channels; use crate::events; +use crate::ui::app::CONSTANTS; use crate::ui::widgets::list::{Holder, Items, List, Selected}; pub fn new(channels: &Channels) -> List { let mut l = List::default(); + // If this list is a child of the Rewards pane if let Some(ref p) = l.parent() { - l.set_size(0, p.h() - 20); + // Set the list's height to everything except the Menubar's height + l.set_size(0, p.h() - CONSTANTS.rewards_menubar_height); } l.add("(15) A reward."); diff --git a/src/ui/rewards/pane.rs b/src/ui/rewards/pane.rs index ad91244..5c288c3 100644 --- a/src/ui/rewards/pane.rs +++ b/src/ui/rewards/pane.rs @@ -8,10 +8,12 @@ pub fn pane(channels: &Channels) -> Pack { let mut p = Pack::default().with_pos(10, 10 + CONSTANTS.focus_pane_height + 10); p.set_spacing(1); + // If this pane is a child of the Main Window if let Some(ref w) = p.parent() { + // Set the size, excluding the pane's margins and the height taken by the Focus Pane p.set_size( w.width() - 20, - w.height() - CONSTANTS.focus_pane_height - CONSTANTS.rewards_menubar_height, + w.height() - 10 - CONSTANTS.focus_pane_height - 10 - 10, ); } diff --git a/src/ui/widgets/list/item.rs b/src/ui/widgets/list/item.rs index 69e1d24..8554e64 100644 --- a/src/ui/widgets/list/item.rs +++ b/src/ui/widgets/list/item.rs @@ -72,6 +72,16 @@ impl Item { &self.frame } + /// Get the y coordinate of the item + pub fn y(&self) -> i32 { + self.frame.y() + } + + /// Get the height of the item + pub fn h(&self) -> i32 { + self.frame.h() + } + fn string(&self) -> Ref { self.string.borrow() } diff --git a/src/ui/widgets/list/items.rs b/src/ui/widgets/list/items.rs index 54765d6..d910356 100644 --- a/src/ui/widgets/list/items.rs +++ b/src/ui/widgets/list/items.rs @@ -1,3 +1,4 @@ +use fltk::prelude::*; use std::{ cell::{Ref, RefCell, RefMut}, rc::Rc, @@ -59,6 +60,20 @@ impl Items { } } + /// Return `true` if the item with the specified index is partially or completely hidden + pub fn hidden(&self, index: usize) -> bool { + let item = self.index(index); + // Get the Pack in the custom List widget + item.frame().parent().map_or(false, |ref p| { + // Get the Scroll in the custom List widget + p.parent().map_or(false, |ref s| { + // Return `true` if + item.y() < s.y() // The top border is hidden, or + || item.y() + item.h() > s.y() + s.h() // The bottom border is hidden + }) + }) + } + pub fn remove(&self, index: usize) { self.items_mut().remove(index); } diff --git a/src/ui/widgets/list/widget.rs b/src/ui/widgets/list/widget.rs index 8e0bb97..9f00e66 100644 --- a/src/ui/widgets/list/widget.rs +++ b/src/ui/widgets/list/widget.rs @@ -129,28 +129,56 @@ fn handle_push_default(s: &mut Scroll, selected: &mut Selected, items: &mut Item } } +/// Set the default handles for the `Key::Up` and `Key::Down` events fn handle_key_down_default(s: &mut Scroll, selected: &mut Selected, items: &mut Items) -> bool { match app::event_key() { Key::Up => { - if selected.get() == 0 || selected.get() == 1 { + // Pressing Up without any selection or on the top item will + let to = if selected.get() == 0 || selected.get() == 1 { + // Select the last item selected.set(items.len()); + // Set the scroll position to the bottom of the list + // (these two pixels come from the compensation of the Scroll borders) + selected.get() as i32 * 20 - s.h() + 2 + // Pressing Up on any other item will } else { + // Select the previous item selected.decrement(); + // Set the scroll position to the top border of the previous item + selected.index() as i32 * 20 + }; + // If the selected item is partially or completely hidden, change the scroll position + if items.hidden(selected.index()) { + s.scroll_to(0, to); } + // Select the new item, unselecting all others items.select(selected.get()); s.redraw(); true } Key::Down => { - if selected.get() == items.len() { + // Pressing Down without on the last item will + let to = if selected.get() == items.len() { + // Select the top item selected.set(1); + // Set the scroll position to the top of the list + 0 } else { + // Select the next item selected.increment(); + // Set the scroll position, so that the current item is on the top + // (these two pixels come from the compensation of the Scroll borders) + selected.get() as i32 * 20 - s.h() + 2 + }; + // If the selected item is partially or completely hidden, change the scroll position + if items.hidden(selected.index()) { + s.scroll_to(0, to); } + // Select the new item, unselecting all others items.select(selected.get()); - s.redraw(); + s.redraw(); true } _ => false, From 6a770ad31ebcfca20ef778d3e6402be933b859db Mon Sep 17 00:00:00 2001 From: Pavel Sobolev Date: Tue, 10 Aug 2021 18:04:49 +0300 Subject: [PATCH 13/16] Don't intercept the focus when using buttons; also: - Fix the list keyboard movement jumping outside the bounds when an item is completely hidden; - Refactor the code base by removing the Constants struct and adding a new module for the constants; - Document everything. --- src/channels.rs | 12 +- src/events.rs | 100 +++++++++++-- src/main.rs | 3 +- src/ui/app.rs | 35 +---- src/ui/constants.rs | 30 ++++ src/ui/focus/arrow.rs | 82 ++++++----- src/ui/focus/coins.rs | 70 ++++++--- src/ui/focus/mod.rs | 9 ++ src/ui/focus/pane.rs | 29 +++- src/ui/focus/timer.rs | 140 +++++++++++------- src/ui/logic.rs | 182 ------------------------ src/ui/logic/get_price.rs | 14 ++ src/ui/logic/get_text.rs | 14 ++ src/ui/logic/handle_button.rs | 34 +++++ src/ui/logic/handle_left.rs | 31 ++++ src/ui/logic/handle_lock.rs | 43 ++++++ src/ui/logic/handle_right.rs | 31 ++++ src/ui/logic/handle_selection.rs | 129 +++++++++++++++++ src/ui/logic/handle_shortcut.rs | 13 ++ src/ui/logic/handle_tab.rs | 18 +++ src/ui/logic/mod.rs | 25 ++++ src/ui/logic/mouse_hovering.rs | 11 ++ src/ui/mod.rs | 5 +- src/ui/rates/list.rs | 12 +- src/ui/rates/menubar.rs | 59 ++++---- src/ui/rates/mod.rs | 7 + src/ui/rates/pane.rs | 15 +- src/ui/rewards/edit/buttons.rs | 13 +- src/ui/rewards/edit/cancel.rs | 16 ++- src/ui/rewards/edit/coins.rs | 55 +++++-- src/ui/rewards/edit/mod.rs | 2 + src/ui/rewards/edit/ok.rs | 32 ++++- src/ui/rewards/edit/reward.rs | 44 ++++-- src/ui/rewards/list.rs | 126 ++++++++++------ src/ui/rewards/menubar/add_button.rs | 35 +++-- src/ui/rewards/menubar/delete_button.rs | 67 ++++----- src/ui/rewards/menubar/divider.rs | 7 + src/ui/rewards/menubar/edit_button.rs | 70 ++++----- src/ui/rewards/menubar/mod.rs | 2 + src/ui/rewards/menubar/new.rs | 11 +- src/ui/rewards/menubar/spend_button.rs | 68 ++++----- src/ui/rewards/mod.rs | 8 ++ src/ui/rewards/pane.rs | 9 +- src/ui/widgets/list/holder.rs | 118 +++++++++++---- src/ui/widgets/list/item.rs | 175 +++++++++++++---------- src/ui/widgets/list/items.rs | 44 +++--- src/ui/widgets/list/mod.rs | 12 +- src/ui/widgets/list/selected.rs | 33 +++-- src/ui/widgets/list/widget.rs | 135 +++++++++++++----- src/ui/widgets/mod.rs | 2 + src/ui/windows/icon.rs | 2 + src/ui/windows/main.rs | 104 ++++++++++---- src/ui/windows/mod.rs | 5 +- src/ui/windows/rewards_edit.rs | 43 ++++-- 54 files changed, 1588 insertions(+), 803 deletions(-) create mode 100644 src/ui/constants.rs delete mode 100644 src/ui/logic.rs create mode 100644 src/ui/logic/get_price.rs create mode 100644 src/ui/logic/get_text.rs create mode 100644 src/ui/logic/handle_button.rs create mode 100644 src/ui/logic/handle_left.rs create mode 100644 src/ui/logic/handle_lock.rs create mode 100644 src/ui/logic/handle_right.rs create mode 100644 src/ui/logic/handle_selection.rs create mode 100644 src/ui/logic/handle_shortcut.rs create mode 100644 src/ui/logic/handle_tab.rs create mode 100644 src/ui/logic/mod.rs create mode 100644 src/ui/logic/mouse_hovering.rs diff --git a/src/channels.rs b/src/channels.rs index ec55eb2..284a39d 100644 --- a/src/channels.rs +++ b/src/channels.rs @@ -1,3 +1,5 @@ +//! This module provides the channels used in the program. + use crossbeam_channel::{self, Receiver, Sender}; macro_rules! channels_default_impl { @@ -30,20 +32,24 @@ macro_rules! channels_default_impl { } } +/// A channel #[derive(Clone)] pub struct Channel { + /// Sender pub s: Sender, + /// Receiver pub r: Receiver, } impl Channel { + /// Initialize a new channel from a tuple of a Sender and a Receiver fn new((s, r): (Sender, Receiver)) -> Channel { Channel { s, r } } } channels_default_impl! { - /// A struct providing access to the application's channels + /// A struct that provides access to the application's channels #[derive(Clone)] pub struct Channels { /// Channel 1: From Any Window to Main Window @@ -52,9 +58,9 @@ channels_default_impl! { pub rewards_edit: Channel, /// Channel 3: Send Coins To Reward in the Rewards Edit Window pub rewards_send_coins: Channel, - /// Channel 4: Send an Item from the Rewards Edit Window to Rewards + /// Channel 4: Send an Item's string from the Rewards Edit Window to Rewards pub rewards_send_item: Channel, - /// Channel 5: Receive an Item from the Rewards + /// Channel 5: Receive an Item's string from the Rewards pub rewards_receive_item: Channel, /// Channel 6: Receive Coins from an Item pub rewards_receive_coins: Channel, diff --git a/src/events.rs b/src/events.rs index a07f307..96b8140 100644 --- a/src/events.rs +++ b/src/events.rs @@ -1,3 +1,7 @@ +//! This module provides custom events that are handled by the program. +//! +//! The numeric constants are generated using a recursive counting macro. + /// Consume the first argument in exchange of unit macro_rules! unit { ($_t:tt $unit:expr) => { @@ -7,41 +11,117 @@ macro_rules! unit { /// Create events using provided identifiers macro_rules! events { - ( $first_event:ident $(,)? $($other_events:ident),* ) => { + ( + $(#[$first_doc:meta])* + $first_event:ident $(,)? + $( + $(#[$other_docs:meta])* + $other_events:ident + ),* + ) => { + $(#[$first_doc])* pub const $first_event: i32 = 1000 + <[()]>::len(&[$(unit!(($other_events) ())),*]) as i32; - events!($($other_events),*); + events!($( + $(#[$other_docs])* + $other_events + ),*); }; () => {}; } events!( + /// Hide the secondary pane (called by the Arrow and Coins buttons in the Focus Pane) MAIN_WINDOW_HIDE_THE_PANE, + /// Show the secondary pane (called by the Arrow and Coins buttons in the Focus Pane) MAIN_WINDOW_SHOW_THE_PANE, + /// Start the timer (called by the Timer button in the Focus Pane) START_TIMER, + /// Tick (meaning, advance the timer by one second) (called by Timer in the Focus Pane) TICK, + /// Stop the timer (called by the Timer button in the Focus Pane) STOP_TIMER, + /// Do the callback of the OK button in the Rewards Edit window (called by the Coins, Reward + /// widgets, and the OK button in that window) OK_BUTTON_DO_CALLBACK, + /// Set the OK button in the Rewards Edit window to the 'Add a reward' mode (called by the + /// Add Button in the Rewards' Menubar) OK_BUTTON_SET_TO_ADD, + /// Set the OK button in the Rewards Edit window to the 'Edit the reward' mode (called by the + /// Edit Button in the Rewards' Menubar) + OK_BUTTON_SET_TO_EDIT, + /// Show the Rewards Edit window for adding a new reward (called by the Add Button in the + /// Rewards' Menubar) ADD_A_REWARD_OPEN, + /// Start a chain of events to create a new reward. Start by sending the input of the + /// Coins widget in the Rewards Edit window to its sibling, the Reward widget (called by + /// the OK button in that window, but that signal may be inherited from the input widgets, too) ADD_A_REWARD_SEND_COINS, + /// Combine the number of coins received from the Coins widget in the Rewards Edit window with + /// input of the Reward widget, thus creating a reward, and send a result to the Rewards' + /// List (called by the Coins widget as part of the chain of the events) ADD_A_REWARD_SEND_REWARD, + /// Receive a reward from the Rewards Edit window (called by the Reward widget in the Rewards + /// Edit window as part of the chain of the events) ADD_A_REWARD_RECEIVE, - OK_BUTTON_SET_TO_EDIT, - EDIT_A_REWARD_SEND_ITEM, - EDIT_A_REWARD_OPEN, - EDIT_A_REWARD_RECEIVE_COINS, - EDIT_A_REWARD_RECEIVE_REWARD, - EDIT_A_REWARD_SEND_COINS, - EDIT_A_REWARD_SEND_REWARD, - EDIT_A_REWARD_RECEIVE, + /// Start a chain of events to open an existing reward in the Rewards Edit window. Send the + /// currently selected item from the List to the Rewards Edit window, so it's ready to get + /// edited (called by the Edit Button in the Rewards' Menubar) + EDIT_THE_REWARD_SEND_ITEM, + /// Show the Rewards Edit window for editing an existing reward (called by the List as part of + /// the chain of the events) + EDIT_THE_REWARD_OPEN, + /// Receive the price of the reward that's being edited (called by the Rewards Edit window as + /// part of the chain of the events) + EDIT_THE_REWARD_RECEIVE_COINS, + /// Receive the text of the reward that's being edited (called by the Rewards Edit window as + /// part of the chain of the events) + EDIT_THE_REWARD_RECEIVE_REWARD, + /// Start a chain of events to edit the existing (currently selected) reward. Start by sending + /// the input of the Coins widget in the Rewards Edit window to its sibling, the Reward widget + /// (called by the OK button in that window, but that signal may be inherited from the input + /// widgets, too) + EDIT_THE_REWARD_SEND_COINS, + /// Combine the number of coins received from the Coins widget in the Rewards Edit window with + /// input of the Reward widget, thus creating a reward, and send a result to the Rewards' + /// List (called by the Coins widget as part of the chain of the events) + EDIT_THE_REWARD_SEND_REWARD, + /// Receive a reward from the Rewards Edit window (called by the Reward widget in the Rewards + /// Edit window as part of the chain of the events) + EDIT_THE_REWARD_RECEIVE, + /// Delete the selected reward (called by the Delete Button in the Rewards' Menubar) DELETE_A_REWARD, + /// Start a chain of events to spend coins on the selected reward. Start by sending the price + /// (called by the Spend Button in the Rewards' Menubar) SPEND_COINS_SEND_PRICE, + /// Receive the price of the selected item (called by the List) SPEND_COINS_RECEIVE_PRICE, + /// Handle the Timer button's shortcut (called by the Main Window) + TIMER_SHORTCUT, + /// Handle the Rates button's shortcut (called by the Main Window) + RATES_SHORTCUT, + /// Handle the Rewards button's shortcut (called by the Main Window) + REWARDS_SHORTCUT, + /// Handle the Add Button's shortcut (called by the Main Window) + ADD_BUTTON_SHORTCUT, + /// Handle the Delete Button's shortcut (called by the Main Window) + DELETE_BUTTON_SHORTCUT, + /// Handle the Edit Button's shortcut (called by the Main Window) + EDIT_BUTTON_SHORTCUT, + /// Handle the Spend Button's shortcut (called by the Main Window) + SPEND_BUTTON_SHORTCUT, + /// Lock the Delete Button in the Rewards' Menubar (called by the List) LOCK_THE_DELETE_A_REWARD_BUTTON, + /// Unlock the Delete Button in the Rewards' Menubar (called by the List) UNLOCK_THE_DELETE_A_REWARD_BUTTON, + /// Lock the Edit Button in the Rewards' Menubar (called by the List) LOCK_THE_EDIT_A_REWARD_BUTTON, + /// Unlock the Delete Button in the Rewards' Menubar (called by the List) UNLOCK_THE_EDIT_A_REWARD_BUTTON, + /// Check the affordability (meaning, if the user has enough coins) of the reward. This leads + /// to the Spend button in the Rewards' Menubar getting locked / unlocked (called by the List) CHECK_AFFORDABILITY_RECEIVE_PRICE, + /// Lock the Spend Button in the Rewards' Menubar (called by the List) LOCK_THE_SPEND_BUTTON, + /// Unlock the Spend Button in the Rewards' Menubar (called by the List) UNLOCK_THE_SPEND_BUTTON ); diff --git a/src/main.rs b/src/main.rs index ee345e8..0a37d3d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ mod ui; use fltk::app; +/// Run the program fn main() { // Application channels let channels = channels::Channels::default(); @@ -29,7 +30,7 @@ fn main() { // Start the event loop while app.wait() { - // Retranslation of signals between windows + // Retranslate the signals between the windows if let Ok(event) = channels.mw.r.try_recv() { app::handle_main(event).ok(); }; diff --git a/src/ui/app.rs b/src/ui/app.rs index 75661cb..50653c8 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -1,41 +1,12 @@ +//! This module provides the app initialization function. + use fltk::{ app::{self, App}, enums::{Color, FrameType}, misc::Tooltip, }; -/// A struct providing access to the application's constants -pub struct Constants { - pub main_window_width: i32, - pub main_window_height: i32, - pub focus_pane_height: i32, - pub rewards_menubar_height: i32, - pub rewards_edit_window_width: i32, - pub rewards_edit_window_height: i32, - pub scrollbar_width: i32, -} - -impl Constants { - /// Get the default set of the application's constants - const fn default() -> Constants { - Constants { - main_window_width: 340, - // These 3 pixels are the 1px spacing in Rewards / Rates panes and - // 1px top and bottom borders of the Scroll widget in the custom List widget - main_window_height: 300 + 3, - focus_pane_height: 60, - rewards_menubar_height: 30, - rewards_edit_window_width: 320, - rewards_edit_window_height: 140, - scrollbar_width: 17, - } - } -} - -/// Default set of the application's constants -pub const CONSTANTS: Constants = Constants::default(); - -/// Create a new App +/// Initialize the app pub fn new() -> App { let app = App::default(); app::background(255, 255, 255); diff --git a/src/ui/constants.rs b/src/ui/constants.rs new file mode 100644 index 0000000..c2442a7 --- /dev/null +++ b/src/ui/constants.rs @@ -0,0 +1,30 @@ +//! This module provides the constants. + +use fltk::enums::FrameType; + +// Numeric constants + +/// Main Window's width +pub const MAIN_WINDOW_WIDTH: i32 = 340; +/// Main Window's height +/// +/// These 3 pixels are the 1px spacing in Rewards / Rates panes and +/// 1px top and bottom borders of the Scroll widget in the custom List widget +pub const MAIN_WINDOW_HEIGHT: i32 = 300 + 3; +/// Focus Pane's height +pub const FOCUS_PANE_HEIGHT: i32 = 60; +/// Rewards' Menubar's height +pub const REWARDS_MENUBAR_HEIGHT: i32 = 30; +/// Rewards Edit's width +pub const REWARDS_EDIT_WINDOW_WIDTH: i32 = 320; +/// Rewards Edit's height +pub const REWARDS_EDIT_WINDOW_HEIGHT: i32 = 140; + +// Default frame types for the buttons + +/// Default frame type for boxed buttons +pub const BOXED_BUTTON_FRAME_TYPE: FrameType = FrameType::BorderBox; +/// Default frame type for flat buttons +pub const FLAT_BUTTON_FRAME_TYPE: FrameType = FrameType::FlatBox; +/// Default down frame type for buttons +pub const DOWN_BUTTON_FRAME_TYPE: FrameType = FrameType::DownBox; diff --git a/src/ui/focus/arrow.rs b/src/ui/focus/arrow.rs index 7e78089..80390f4 100644 --- a/src/ui/focus/arrow.rs +++ b/src/ui/focus/arrow.rs @@ -1,57 +1,74 @@ -use fltk::{ - app, - button::Button, - draw, - enums::{Event, FrameType, Key, Shortcut}, - image::SvgImage, - prelude::*, -}; +//! A button, which you can click on to bring the [Rates](super::super::rates) pane. +//! +//! The arrow represents the conversion process between the time and the coins. + +use fltk::{app, button::Button, draw, image::SvgImage, prelude::*}; use crate::events; +use crate::ui::constants::{DOWN_BUTTON_FRAME_TYPE, FLAT_BUTTON_FRAME_TYPE}; use crate::ui::logic; +/// Initialize the arrow button pub fn arrow() -> Button { let mut a = Button::default(); - a.set_frame(FrameType::FlatBox); - a.set_down_frame(FrameType::DownBox); + a.set_frame(FLAT_BUTTON_FRAME_TYPE); + a.set_down_frame(DOWN_BUTTON_FRAME_TYPE); a.set_tooltip("Show the Conversion Rates pane"); - resize_frame(&mut a); - a.draw(draw); + // These 14px are the padding. Note that the 48 / 30 ratio is the ratio of the SVG + a.set_size(48 + 14, 30); + // Setting the size and position to non-zero values is needed for + // the widget to appear after re-adding it to the pane. Initial position + // should not overlap other widgets, since this will lead to visual artifacts + if let Some(ref p) = a.parent() { + if let Some(ref lw) = p.child(0) { + if let Some(ref rw) = p.child(1) { + replace(&mut a, p, lw, rw); + } + } + } + + a.draw(draw); logic(&mut a); a } +/// Draw the arrow button fn draw(f: &mut T) { - resize_frame(f); - draw::push_clip(f.x(), f.y(), f.w(), f.h()); - draw_image(f); - draw::pop_clip(); -} - -fn resize_frame(f: &mut T) { + // Replace if needed if let Some(ref p) = f.parent() { if let Some(ref lw) = p.child(0) { - if let Some(ref rw) = p.child(1) { - let lx = lw.x() + lw.w(); - let w = 48 + 14; - f.set_pos(lx + (rw.x() - lx - w) / 2, p.y() + 15); - f.set_size(w, 30); + if let Some(ref rw) = p.child(2) { + replace(f, p, lw, rw); } } } + // Setting a clip is a safety measure to limit the region of the drawing + draw::push_clip(f.x(), f.y(), f.w(), f.h()); + draw_image(f); + draw::pop_clip(); } +/// Position the arrow in the middle of the two adjacent widgets +fn replace(f: &mut U, p: &T, lw: &S, rw: &S) { + let lx = lw.x() + lw.w(); + f.set_pos(lx + (rw.x() - lx - f.w()) / 2, p.y() + 15); +} + +/// Draw the image of the arrow fn draw_image(f: &mut T) { let mut ai = SvgImage::from_data(include_str!("../../../assets/menu/arrow.svg")).unwrap(); + // These 14px is the padding ai.scale(f.w() - 14, f.h(), true, true); ai.draw(f.x() + 7, f.y(), f.w() - 14, f.h()); } +/// Set a callback and a handler for the arrow button fn logic(a: &mut T) { - a.set_shortcut(Shortcut::from_char('r')); + // On a callback, show the Rates pane if it's not visible, or, + // otherwise, hide it. Change the tooltip as appropriate, too a.set_callback(|a| { if let Some(ref p) = a.parent() { if let Some(ref mut c) = p.child(1) { @@ -78,16 +95,9 @@ fn logic(a: &mut T) { } } }); - a.handle({ - let unselect_box = FrameType::FlatBox; - move |a, ev| match ev { - Event::KeyDown => match app::event_key() { - Key::Left => logic::fp_handle_left(a, 2), - Key::Right => logic::fp_handle_right(a, 2), - Key::Tab => logic::handle_tab(a), - _ => logic::handle_active_selection(a, ev, unselect_box), - }, - _ => logic::handle_active_selection(a, ev, unselect_box), - } + // Handle the shortcut and focus / selection events + a.handle(move |a, ev| { + logic::handle_shortcut(a, ev.bits(), events::RATES_SHORTCUT) + || logic::handle_fp_button(a, ev, 1) }); } diff --git a/src/ui/focus/coins.rs b/src/ui/focus/coins.rs index f8a2781..a34ce45 100644 --- a/src/ui/focus/coins.rs +++ b/src/ui/focus/coins.rs @@ -1,40 +1,51 @@ +//! A button, which you can click on to bring the [Rewards](super::super::rewards) pane. +//! +//! The widget also keeps the number of coins the user has, and updates it if necessary. + use fltk::{ app, button::Button, draw, - enums::{Align, Color, Event, FrameType, Key, LabelType, Shortcut}, + enums::{Align, Color, LabelType}, image::SvgImage, prelude::*, }; use crate::channels::Channels; use crate::events; +use crate::ui::constants::{DOWN_BUTTON_FRAME_TYPE, FLAT_BUTTON_FRAME_TYPE}; use crate::ui::logic; +/// Initialize the coins button pub fn coins(channels: &Channels) -> Button { + // Setting the label, but in the meantime disabling + // its default drawing allows for the custom drawing let mut c = Button::default().with_label("99999"); c.set_label_type(LabelType::None); - c.set_frame(FrameType::FlatBox); - c.set_down_frame(FrameType::DownBox); + + c.set_frame(FLAT_BUTTON_FRAME_TYPE); + c.set_down_frame(DOWN_BUTTON_FRAME_TYPE); c.set_tooltip("Hide the Rewards pane"); - resize_button(&mut c); + // This widget changes in size depending on the number of (digits in) the coins + resize(&mut c); c.draw(draw); - logic(&mut c, channels); c } +/// Draw the coins button fn draw(b: &mut T) { - resize_button(b); + // Setting a clip is a safety measure to limit the region of the drawing draw::push_clip(b.x(), b.y(), b.w(), b.h()); draw_label(b); draw_image(b); draw::pop_clip(); } -fn resize_button(b: &mut T) { +/// Resize the button depending on the size of its label +fn resize(b: &mut T) { let (lw, _) = b.measure_label(); if let Some(ref p) = b.parent() { b.set_pos(p.x() + p.w() - lw - 60, p.y() + 10); @@ -42,7 +53,9 @@ fn resize_button(b: &mut T) { } } +/// Draw the button's label fn draw_label(b: &mut T) { + // Saving the draw color and reapplying it in the end is a safety measure let color = draw::get_color(); draw::set_font(draw::font(), 16); @@ -59,14 +72,17 @@ fn draw_label(b: &mut T) { draw::set_draw_color(color); } +/// Draw the image of the coins fn draw_image(b: &mut T) { let mut ci = SvgImage::from_data(include_str!("../../../assets/menu/coins.svg")).unwrap(); ci.scale(24, 24, true, true); ci.draw(b.x() + 6, b.y() + 8, 24, 24); } +/// Set a callback and a handler for the coins button fn logic(c: &mut T, channels: &Channels) { - c.set_shortcut(Shortcut::from_char('c')); + // On a callback, show the Rewards pane if it's not visible, or, + // otherwise, hide it. Change the tooltip as appropriate, too c.set_callback(|c| { if let Some(ref p) = c.parent() { if let Some(ref mut a) = p.child(2) { @@ -93,41 +109,59 @@ fn logic(c: &mut T, channels: &Channels) { } } }); + // Handle the custom events, shortcut, and focus / selection events c.handle({ + // Receive coins let r_coins = channels.rewards_receive_coins.r.clone(); - move |c, ev| match ev { - Event::KeyDown => match app::event_key() { - Key::Left => logic::fp_handle_left(c, 1), - Key::Right => logic::fp_handle_right(c, 1), - Key::Tab => logic::handle_tab(c), - _ => logic::handle_active_selection(c, ev, FrameType::FlatBox), - }, - _ => match ev.bits() { + move |c, ev| { + let bits = ev.bits(); + // In case of the + match bits { + // 'Spend the coins' event, receive the price events::SPEND_COINS_RECEIVE_PRICE => r_coins.try_recv().map_or(false, |price| { + // Get the total number of coins c.label().parse::().map_or(false, |coins| { + // Calculate the diff, rounding it to the second digit in mantissa let diff = ((coins - price) * 100.0).round() / 100.0; + // Update the total number of coins c.set_label(&diff.to_string()); + // Send a signal to Rewards to delete the currently selected reward app::handle_main(events::DELETE_A_REWARD).ok(); + // Resize the coins button if the number of digits changed + resize(c); + // Redraw the pane, so that the `arrow`'s + // position gets updated too if needed if let Some(ref mut p) = c.parent() { p.redraw(); } true }) }), + // `Check the affordability` event events::CHECK_AFFORDABILITY_RECEIVE_PRICE => { + // Receive the price r_coins.try_recv().map_or(false, |price| { + // Get the total number of coins c.label().parse::().map_or(false, |coins| { + // If the reward is affordable if price <= coins { + // Unlock the spend button app::handle_main(events::UNLOCK_THE_SPEND_BUTTON).ok(); + // Otherwise, } else { + // Lock it app::handle_main(events::LOCK_THE_SPEND_BUTTON).ok(); } true }) }) } - _ => logic::handle_active_selection(c, ev, FrameType::FlatBox), - }, + // Otherwise, handle the shortcut and focus / selection events + _ => { + logic::handle_shortcut(c, bits, events::REWARDS_SHORTCUT) + || logic::handle_fp_button(c, ev, 2) + } + } } }); } diff --git a/src/ui/focus/mod.rs b/src/ui/focus/mod.rs index 5c9d2e9..9531d39 100644 --- a/src/ui/focus/mod.rs +++ b/src/ui/focus/mod.rs @@ -1,3 +1,12 @@ +//! Focus pane is the primary pane, providing access to the focus / conversion process. +//! +//! It provides ways for the user to: +//! - start the time-to-coins conversion process; +//! - see how long this process is running; +//! - see the total number of coins; +//! - get access to the [Rates](super::rates) pane; +//! - get access to the [Rewards](super::rewards) pane + mod arrow; mod coins; mod pane; diff --git a/src/ui/focus/pane.rs b/src/ui/focus/pane.rs index 69fac5a..a2333aa 100644 --- a/src/ui/focus/pane.rs +++ b/src/ui/focus/pane.rs @@ -1,3 +1,5 @@ +//! The pane itself. It is supposed to be the top-most pane in the main window. + use fltk::{ app, enums::{Event, FrameType, Key}, @@ -7,8 +9,9 @@ use fltk::{ use super::{arrow, coins, timer}; use crate::channels::Channels; -use crate::ui::app::CONSTANTS; +use crate::ui::constants::FOCUS_PANE_HEIGHT; +/// Initialize the Focus pane pub fn pane(channels: &Channels) -> Group { let mut p = Group::default().with_pos(10, 10); p.set_frame(FrameType::BorderBox); @@ -16,25 +19,41 @@ pub fn pane(channels: &Channels) -> Group { // If this pane is a child of the Main Window if let Some(ref w) = p.parent() { // Set the size, excluding the margins - p.set_size(w.width() - 20, CONSTANTS.focus_pane_height); + p.set_size(w.width() - 20, FOCUS_PANE_HEIGHT); } - // The order of the widgets is important + // Initialize the widgets, so that the `arrow` widget is the last one let _timer = timer(); - let _coins = coins(channels); - let _arrow = arrow(); + let coins = coins(channels); + let arrow = arrow(); + // The reason why the pack ends here and removes / adds the items later is that + // the `arrow` widget needs to know about the positions and sizes of the + // other two widgets: `timer` and `coins`, because the `coins`' size depends on + // the number of coins, and the `arrow` widget is supposed to be placed between + // the two. But it is also important to have the `arrow` widget as a second + // child, so it is possible to use the default handle function for the buttons p.end(); + // Reorder the children, so that the `arrow` widget is the second one + p.remove(&arrow); + p.remove(&coins); + p.add(&arrow); + p.add(&coins); + p.handle(handle); p } +/// Handle events for the Focus pane fn handle(p: &mut T, ev: Event) -> bool { match ev { + // In case of a pressed down button Event::KeyDown => { + // If it's a `Tab` key if app::event_key() == Key::Tab { + // Focus on the secondary pane if let Some(ref w) = p.parent() { if let Some(ref mut re_p) = w.child(1) { if let Some(ref mut ra_p) = w.child(2) { diff --git a/src/ui/focus/timer.rs b/src/ui/focus/timer.rs index f1e51c3..8803012 100644 --- a/src/ui/focus/timer.rs +++ b/src/ui/focus/timer.rs @@ -1,23 +1,34 @@ +//! A button, which represents a timer. Clicking on it starts the ticking. +//! +//! While it ticks, the coins get generated. Clicking on it again stops and resets the timer. + use fltk::{ app, button::Button, draw, - enums::{Align, Color, Event, FrameType, Key, LabelType, Shortcut}, + enums::{Align, Color, LabelType}, image::SvgImage, prelude::*, }; use crate::events; +use crate::ui::constants::{DOWN_BUTTON_FRAME_TYPE, FLAT_BUTTON_FRAME_TYPE}; use crate::ui::logic; +/// Initialize the timer pub fn timer() -> Button { + // Setting the label, but in the meantime disabling + // its default drawing allows for the custom drawing let mut t = Button::default().with_label("00:00:00"); t.set_label_type(LabelType::None); - t.set_frame(FrameType::FlatBox); - t.set_down_frame(FrameType::DownBox); + + t.set_frame(FLAT_BUTTON_FRAME_TYPE); + t.set_down_frame(DOWN_BUTTON_FRAME_TYPE); t.set_tooltip("Start the timer"); - if let Some(ref p) = t.parent() { + // The timer is the only one among the three widgets + // in the pane which change neither position nor size + if let Some(p) = t.parent() { t.set_pos(p.x() + 10, p.y() + 10); t.set_size(110, p.h() - 20); } @@ -29,14 +40,18 @@ pub fn timer() -> Button { t } +/// Draw the timer fn draw(b: &mut T) { + // Setting a clip is a safety measure to limit the region of the drawing draw::push_clip(b.x(), b.y(), b.w(), b.h()); draw_label(b); draw_image(b); draw::pop_clip(); } +/// Draw the timer's label fn draw_label(b: &mut T) { + // Saving the draw color and reapplying it in the end is a safety measure let color = draw::get_color(); draw::set_font(draw::font(), 16); @@ -53,86 +68,103 @@ fn draw_label(b: &mut T) { draw::set_draw_color(color); } +/// Draw the timer's icon fn draw_image(b: &mut T) { let mut ti = SvgImage::from_data(include_str!("../../../assets/menu/sand-clock.svg")).unwrap(); ti.scale(24, 24, true, true); ti.draw(b.x() + 6, b.y() + 8, 24, 24); } +/// Set a callback and a handler for the timer fn logic(t: &mut T) { - t.set_shortcut(Shortcut::from_char('f')); + // The default callback is set to start the ticking t.set_callback(start); + // Handle the custom events, shortcut, and focus / selection events t.handle({ + // Is the timer counting? let mut counting = false; + // How many seconds does it have at the moment? let mut seconds = 0; - move |t, ev| match ev.bits() { - events::START_TIMER => { - if !counting { - counting = true; - app::add_timeout2(1.0, tick); - } - true - } - events::TICK => { - seconds += 1; - let hours = seconds / 3600; - let minutes = seconds / 60 % 60; - let seconds = seconds % 60; - t.set_label(&format!( - "{}:{}:{}", - if hours > 9 { - format!("{}", hours) - } else { - format!("0{}", hours) - }, - if minutes > 9 { - format!("{}", minutes) - } else { - format!("0{}", minutes) - }, - if seconds > 9 { - format!("{}", seconds) - } else { - format!("0{}", seconds) - } - )); - true - } - events::STOP_TIMER => { - counting = false; - seconds = 0; - app::remove_timeout2(tick); - t.set_label("00:00:00"); - true - } - _ => { - if ev == Event::KeyDown { - match app::event_key() { - Key::Left => logic::fp_handle_left(t, 0), - Key::Right => logic::fp_handle_right(t, 0), - Key::Tab => logic::handle_tab(t), - _ => logic::handle_active_selection(t, ev, FrameType::FlatBox), + move |t, ev| { + let bits = ev.bits(); + // In case of the + match bits { + // 'Start the timer` event + events::START_TIMER => { + // If it's not counting already + if !counting { + // Start the counting + counting = true; + app::add_timeout2(1.0, tick); } - } else { - logic::handle_active_selection(t, ev, FrameType::FlatBox) + true + } + // 'Tick' event + events::TICK => { + seconds += 1; + + // Calculate the integer parts of the time + let hours = seconds / 3600; + let minutes = seconds / 60 % 60; + let seconds = seconds % 60; + + // Update the label, applying the `00:00:00` format to these numbers + t.set_label(&format!( + "{}:{}:{}", + if hours > 9 { + format!("{}", hours) + } else { + format!("0{}", hours) + }, + if minutes > 9 { + format!("{}", minutes) + } else { + format!("0{}", minutes) + }, + if seconds > 9 { + format!("{}", seconds) + } else { + format!("0{}", seconds) + } + )); + + true + } + // 'Stop the timer' event + events::STOP_TIMER => { + // Stop the counting + counting = false; + seconds = 0; + app::remove_timeout2(tick); + // Reset the label + t.set_label("00:00:00"); + true + } + // Otherwise, handle the shortcut and focus / selection events + _ => { + logic::handle_shortcut(t, bits, events::TIMER_SHORTCUT) + || logic::handle_fp_button(t, ev, 0) } } } }); } +/// Start the ticking fn start(b: &mut T) { app::handle_main(events::START_TIMER).ok(); b.set_tooltip("Stop the timer"); b.set_callback(stop); } +/// Stop the ticking fn stop(b: &mut T) { app::handle_main(events::STOP_TIMER).ok(); b.set_tooltip("Start the timer"); b.set_callback(start); } +/// Tick fn tick() { app::handle_main(events::TICK).ok(); app::repeat_timeout2(1.0, tick); diff --git a/src/ui/logic.rs b/src/ui/logic.rs deleted file mode 100644 index e35e816..0000000 --- a/src/ui/logic.rs +++ /dev/null @@ -1,182 +0,0 @@ -use fltk::{ - app, - enums::{Color, Event, FrameType, Key}, - prelude::*, -}; - -const ACTIVE_SELECTION_COLOR: u32 = 0xE5_F3_FF; -const INACTIVE_SELECTION_COLOR: u32 = 0xF5_FA_FE; - -/// Handle focus / selection events -pub fn handle_active_selection( - b: &mut T, - ev: Event, - unselect_box: FrameType, -) -> bool { - match ev { - Event::Focus => { - if app::event_key_down(Key::Enter) { - select_active(b); - } - true - } - Event::Enter => { - select_active(b); - true - } - Event::Leave | Event::Unfocus | Event::Hide => { - unselect(b, unselect_box); - true - } - Event::KeyDown => match app::event_key() { - Key::Enter => { - select_active(b); - true - } - _ => false, - }, - Event::KeyUp => match app::event_key() { - Key::Enter => { - b.do_callback(); - unselect(b, unselect_box); - true - } - _ => false, - }, - _ => false, - } -} - -pub fn handle_inactive_selection( - b: &mut T, - ev: Event, - unselect_box: FrameType, -) -> bool { - match ev { - Event::Focus => { - if app::event_key_down(Key::Enter) { - select_inactive(b); - } - true - } - Event::Enter => { - select_inactive(b); - true - } - Event::Leave | Event::Unfocus | Event::Hide => { - unselect(b, unselect_box); - true - } - Event::KeyDown => match app::event_key() { - Key::Enter => { - select_inactive(b); - true - } - _ => false, - }, - Event::KeyUp => match app::event_key() { - Key::Enter => { - unselect(b, unselect_box); - true - } - _ => false, - }, - _ => false, - } -} - -pub fn handle_selection( - b: &mut T, - ev: Event, - unselect_box: FrameType, - lock: bool, -) -> bool { - if lock { - handle_inactive_selection(b, ev, unselect_box) - } else { - handle_active_selection(b, ev, unselect_box) - } -} - -fn select(b: &mut T, hex: u32) { - b.set_color(Color::from_hex(hex)); - b.set_frame(FrameType::BorderBox); - b.redraw(); -} - -pub fn select_active(b: &mut T) { - select(b, ACTIVE_SELECTION_COLOR); -} - -pub fn select_inactive(b: &mut T) { - select(b, INACTIVE_SELECTION_COLOR); -} - -fn unselect(b: &mut T, unselect_box: FrameType) { - b.set_color(Color::BackGround); - b.set_frame(unselect_box); - b.redraw(); -} - -pub fn mouse_hovering_widget(b: &mut T) -> bool { - app::event_x() >= b.x() - && app::event_x() <= b.x() + b.w() - && app::event_y() >= b.y() - && app::event_y() <= b.y() + b.h() -} - -/// Handle Left events for the buttons in the Focus pane -pub fn fp_handle_left(b: &mut T, idx: i32) -> bool { - if let Some(ref p) = b.parent() { - if let Some(ref mut cw) = p.child(if idx == 2 { 0 } else { idx + 1 }) { - cw.take_focus().ok(); - } - } - true -} - -/// Handle Right events for the buttons in the Focus pane -pub fn fp_handle_right(b: &mut T, idx: i32) -> bool { - if let Some(ref p) = b.parent() { - if let Some(ref mut cw) = p.child(if idx == 0 { 2 } else { idx - 1 }) { - cw.take_focus().ok(); - } - } - true -} - -/// Handle Left events for the buttons in the Rewards / Rates panes -pub fn rp_handle_left(b: &mut T, idx: i32) -> bool { - if let Some(ref m) = b.parent() { - if let Some(ref mut cw) = m.child(if idx == 0 { m.children() - 1 } else { idx - 1 }) { - cw.take_focus().ok(); - } - } - true -} - -/// Handle Right events for the buttons in the Rewards / Rates panes -pub fn rp_handle_right(b: &mut T, idx: i32) -> bool { - if let Some(ref m) = b.parent() { - if let Some(ref mut cw) = m.child(if idx == (m.children() - 1) { - 0 - } else { - idx + 1 - }) { - cw.take_focus().ok(); - } - } - true -} - -/// Handle Tab events for the buttons in the Focus Pane -pub fn handle_tab(b: &mut T) -> bool { - b.parent().map_or(false, |p| { - p.parent().map_or(false, |w| { - w.child(1).map_or(false, |re_p| { - w.child(2) - .map_or(false, |ra_p| !(re_p.visible() || ra_p.visible())) - }) - }) - }) -} diff --git a/src/ui/logic/get_price.rs b/src/ui/logic/get_price.rs new file mode 100644 index 0000000..7939259 --- /dev/null +++ b/src/ui/logic/get_price.rs @@ -0,0 +1,14 @@ +//! Parse the price in a Rewards' Item's string. + +/// Parse the price in a Rewards' Item's string +pub fn get_price(string: &str) -> Option { + if let Some(rb_i) = string.find(')') { + if let Ok(price) = string[1..rb_i].parse::() { + Some(price) + } else { + None + } + } else { + None + } +} diff --git a/src/ui/logic/get_text.rs b/src/ui/logic/get_text.rs new file mode 100644 index 0000000..48b8469 --- /dev/null +++ b/src/ui/logic/get_text.rs @@ -0,0 +1,14 @@ +//! Get the text from the Rewards' Item's string. + +/// Get the text from the Rewards' Item's string +pub fn get_text(string: &str) -> Option { + if let Some(rb_i) = string.find(')') { + if rb_i + 2 <= string.len() { + Some(string[(rb_i + 2)..].to_string()) + } else { + None + } + } else { + None + } +} diff --git a/src/ui/logic/handle_button.rs b/src/ui/logic/handle_button.rs new file mode 100644 index 0000000..d72a826 --- /dev/null +++ b/src/ui/logic/handle_button.rs @@ -0,0 +1,34 @@ +//! Handle the events for the button. + +use fltk::{ + app, + enums::{Event, Key}, + prelude::*, +}; + +use super::{handle_left, handle_right, handle_selection, handle_tab}; +use crate::ui::constants::FLAT_BUTTON_FRAME_TYPE; + +/// Handle the events for the button +pub fn handle_button(b: &mut T, ev: Event, idx: i32, lock: bool) -> bool { + match ev { + Event::KeyDown => match app::event_key() { + Key::Left => handle_left(b, idx), + Key::Right => handle_right(b, idx), + _ => handle_selection(b, ev, FLAT_BUTTON_FRAME_TYPE, lock), + }, + _ => handle_selection(b, ev, FLAT_BUTTON_FRAME_TYPE, lock), + } +} + +/// Handle the events for the button in the Focus pane +pub fn handle_fp_button(b: &mut T, ev: Event, idx: i32) -> bool { + if ev == Event::KeyDown { + match app::event_key() { + Key::Tab => handle_tab(b), + _ => handle_button(b, ev, idx, false), + } + } else { + handle_button(b, ev, idx, false) + } +} diff --git a/src/ui/logic/handle_left.rs b/src/ui/logic/handle_left.rs new file mode 100644 index 0000000..d6e9445 --- /dev/null +++ b/src/ui/logic/handle_left.rs @@ -0,0 +1,31 @@ +//! Handle [`Key::Left`](fltk::enums::Key::Left) events for the button. + +use fltk::prelude::*; + +/// Handle Left events for the button +pub fn handle_left(b: &mut T, idx: i32) -> bool { + if let Some(ref p) = b.parent() { + let max = p.children() - 1; + let prev_idx = get_prev_idx(idx, max); + if let Some(ref mut cw) = p.child(prev_idx) { + // This one-time check is needed to jump over menu dividers + if cw.has_visible_focus() { + cw.take_focus().ok(); + } else if let Some(ref mut pcw) = p.child(get_prev_idx(prev_idx, max)) { + if pcw.has_visible_focus() { + pcw.take_focus().ok(); + } + } + } + } + true +} + +/// Get the previous index +fn get_prev_idx(idx: i32, max: i32) -> i32 { + if idx == 0 { + max + } else { + idx - 1 + } +} diff --git a/src/ui/logic/handle_lock.rs b/src/ui/logic/handle_lock.rs new file mode 100644 index 0000000..f8a1c9a --- /dev/null +++ b/src/ui/logic/handle_lock.rs @@ -0,0 +1,43 @@ +//! Handle custom Lock / Unlock events for the button. + +use fltk::prelude::*; + +use super::{mouse_hovering, select_active, select_inactive}; + +/// Handle Lock / Unlock events for the button +pub fn handle_lock( + b: &mut T, + lock: &mut bool, + callback: F, + bits: i32, + lock_event: i32, + unlock_event: i32, +) -> bool +where + F: Fn(&mut T), +{ + // In case of a lock event + if bits == lock_event { + // Lock and remove the callback + *lock = true; + b.set_callback(|_| {}); + // Redraw as an inactive button if hovering it + if mouse_hovering(b) { + select_inactive(b); + } + true + // In case of an unlock event + } else if bits == unlock_event { + // Unlock and reset the callback + *lock = false; + b.set_callback(callback); + // Redraw as an active button if hovering it + if mouse_hovering(b) { + select_active(b); + } + true + // Otherwise, do nothing + } else { + false + } +} diff --git a/src/ui/logic/handle_right.rs b/src/ui/logic/handle_right.rs new file mode 100644 index 0000000..00beb24 --- /dev/null +++ b/src/ui/logic/handle_right.rs @@ -0,0 +1,31 @@ +//! Handle [`Key::Right`](fltk::enums::Key::Right) events for the button. + +use fltk::prelude::*; + +/// Handle Right events for the button +pub fn handle_right(b: &mut T, idx: i32) -> bool { + if let Some(ref p) = b.parent() { + let max = p.children() - 1; + let next_idx = get_next_idx(idx, max); + if let Some(ref mut cw) = p.child(next_idx) { + // This one-time check is needed to jump over menu dividers + if cw.has_visible_focus() { + cw.take_focus().ok(); + } else if let Some(ref mut ncw) = p.child(get_next_idx(next_idx, max)) { + if ncw.has_visible_focus() { + ncw.take_focus().ok(); + } + } + } + } + true +} + +/// Get the next index +fn get_next_idx(idx: i32, max: i32) -> i32 { + if idx == max { + 0 + } else { + idx + 1 + } +} diff --git a/src/ui/logic/handle_selection.rs b/src/ui/logic/handle_selection.rs new file mode 100644 index 0000000..ad8cf03 --- /dev/null +++ b/src/ui/logic/handle_selection.rs @@ -0,0 +1,129 @@ +//! Handle [`Focus`](fltk::enums::Event::Focus) and other selection events. + +use fltk::{ + app, + enums::{Color, Event, FrameType, Key}, + prelude::*, +}; + +/// Color of an active button +const ACTIVE_SELECTION_COLOR: u32 = 0xE5_F3_FF; +/// Color of an inactive button +const INACTIVE_SELECTION_COLOR: u32 = 0xF5_FA_FE; + +/// Handle focus / selection events for the active button +fn handle_active_selection( + b: &mut T, + ev: Event, + unselect_frame_type: FrameType, +) -> bool { + match ev { + Event::Focus => { + if app::event_key_down(Key::Enter) { + select_active(b); + } + true + } + Event::Enter => { + select_active(b); + true + } + Event::Leave | Event::Unfocus | Event::Hide => { + unselect(b, unselect_frame_type); + true + } + Event::KeyDown => match app::event_key() { + Key::Enter => { + select_active(b); + true + } + _ => false, + }, + Event::KeyUp => match app::event_key() { + Key::Enter => { + b.do_callback(); + unselect(b, unselect_frame_type); + true + } + _ => false, + }, + _ => false, + } +} + +/// Handle focus / selection events for the inactive button +fn handle_inactive_selection( + b: &mut T, + ev: Event, + unselect_frame_type: FrameType, +) -> bool { + match ev { + Event::Focus => { + if app::event_key_down(Key::Enter) { + select_inactive(b); + } + true + } + Event::Enter => { + select_inactive(b); + true + } + Event::Leave | Event::Unfocus | Event::Hide => { + unselect(b, unselect_frame_type); + true + } + Event::KeyDown => match app::event_key() { + Key::Enter => { + select_inactive(b); + true + } + _ => false, + }, + Event::KeyUp => match app::event_key() { + Key::Enter => { + unselect(b, unselect_frame_type); + true + } + _ => false, + }, + _ => false, + } +} + +/// Handle focus / selection events for the button +pub fn handle_selection( + b: &mut T, + ev: Event, + unselect_frame_type: FrameType, + lock: bool, +) -> bool { + if lock { + handle_inactive_selection(b, ev, unselect_frame_type) + } else { + handle_active_selection(b, ev, unselect_frame_type) + } +} + +/// Select the button with the specified color +fn select(b: &mut T, hex: u32) { + b.set_color(Color::from_hex(hex)); + b.set_frame(FrameType::BorderBox); + b.redraw(); +} + +/// Select the active button +pub fn select_active(b: &mut T) { + select(b, ACTIVE_SELECTION_COLOR); +} + +/// Select the inactive button +pub fn select_inactive(b: &mut T) { + select(b, INACTIVE_SELECTION_COLOR); +} + +/// Unselect the button +fn unselect(b: &mut T, unselect_frame_type: FrameType) { + b.set_color(Color::BackGround); + b.set_frame(unselect_frame_type); + b.redraw(); +} diff --git a/src/ui/logic/handle_shortcut.rs b/src/ui/logic/handle_shortcut.rs new file mode 100644 index 0000000..51bd1b2 --- /dev/null +++ b/src/ui/logic/handle_shortcut.rs @@ -0,0 +1,13 @@ +//! Handle custom Shortcut events for the button. + +use fltk::prelude::*; + +/// Handle custom shortcut events for the button +pub fn handle_shortcut(b: &mut T, bits: i32, shortcut_event: i32) -> bool { + if bits == shortcut_event { + b.do_callback(); + true + } else { + false + } +} diff --git a/src/ui/logic/handle_tab.rs b/src/ui/logic/handle_tab.rs new file mode 100644 index 0000000..2948ea5 --- /dev/null +++ b/src/ui/logic/handle_tab.rs @@ -0,0 +1,18 @@ +//! Handle [`Key::Tab`](fltk::enums::Key::Tab) events for the buttons. + +use fltk::prelude::*; + +/// Handle [`Key::Tab`](fltk::enums::Key::Tab) events for the buttons +pub fn handle_tab(b: &mut T) -> bool { + // The idea here is to block the handling of the `Tab` event (by + // returning `true`) if neither of the secondary panes is visible. + // If that's `false`, the pane's `Tab` handling will be used + b.parent().map_or(false, |p| { + p.parent().map_or(false, |w| { + w.child(1).map_or(false, |re_p| { + w.child(2) + .map_or(false, |ra_p| !(re_p.visible() || ra_p.visible())) + }) + }) + }) +} diff --git a/src/ui/logic/mod.rs b/src/ui/logic/mod.rs new file mode 100644 index 0000000..730641d --- /dev/null +++ b/src/ui/logic/mod.rs @@ -0,0 +1,25 @@ +//! This module provides shared behavior between the whole program. + +mod get_price; +mod get_text; +mod handle_button; +mod handle_left; +mod handle_lock; +mod handle_right; +mod handle_selection; +mod handle_shortcut; +mod handle_tab; +mod mouse_hovering; + +pub use get_price::get_price; +pub use get_text::get_text; +pub use handle_button::{handle_button, handle_fp_button}; +pub use handle_lock::handle_lock; +pub use handle_selection::handle_selection; +pub use handle_shortcut::handle_shortcut; + +use handle_left::handle_left; +use handle_right::handle_right; +use handle_selection::{select_active, select_inactive}; +use handle_tab::handle_tab; +use mouse_hovering::mouse_hovering; diff --git a/src/ui/logic/mouse_hovering.rs b/src/ui/logic/mouse_hovering.rs new file mode 100644 index 0000000..68fd2f6 --- /dev/null +++ b/src/ui/logic/mouse_hovering.rs @@ -0,0 +1,11 @@ +//! Return [`true`] if mouse hovers the widget. + +use fltk::{app, prelude::*}; + +/// Return [`true`] if the mouse hovers the widget +pub fn mouse_hovering(w: &T) -> bool { + app::event_x() >= w.x() + && app::event_x() <= w.x() + w.w() + && app::event_y() >= w.y() + && app::event_y() <= w.y() + w.h() +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 9eede90..acc2b3b 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,8 +1,11 @@ +//! This module provides the program's UI. + pub mod app; +pub mod constants; +pub mod widgets; pub mod windows; mod focus; mod logic; mod rates; mod rewards; -mod widgets; diff --git a/src/ui/rates/list.rs b/src/ui/rates/list.rs index fdc4ead..4080dc5 100644 --- a/src/ui/rates/list.rs +++ b/src/ui/rates/list.rs @@ -1,19 +1,25 @@ +//! Rates' List contains the conversion rates. + use fltk::prelude::*; -use crate::ui::app::CONSTANTS; +use crate::ui::constants::REWARDS_MENUBAR_HEIGHT; use crate::ui::widgets::list::List; +/// Initialize the list pub fn list() -> List { let mut l = List::default(); // If this list is a child of the Rates pane if let Some(ref p) = l.parent() { - // Set the list's height to everything except the Menubar's height - l.set_size(0, p.h() - CONSTANTS.rewards_menubar_height); + // Set the list's height to all of the available parent's height + l.set_size(0, p.h() - REWARDS_MENUBAR_HEIGHT); } + // Add examples (temporary) l.add("5/m"); l.add("0.1/s"); + + // Select the first one. This list is supposed to always have a selected item l.select(1); l diff --git a/src/ui/rates/menubar.rs b/src/ui/rates/menubar.rs index d594746..217961d 100644 --- a/src/ui/rates/menubar.rs +++ b/src/ui/rates/menubar.rs @@ -1,22 +1,29 @@ +//! Rates' Menubar provides ways to add / delete / edit the conversion rates. + use fltk::{ app, button::Button, - enums::{Event, FrameType, Key, Shortcut}, + enums::{Event, Key}, group::{Pack, PackType}, image::SvgImage, prelude::*, }; -use crate::ui::app::CONSTANTS; +use crate::events; +use crate::ui::constants::{ + DOWN_BUTTON_FRAME_TYPE, FLAT_BUTTON_FRAME_TYPE, REWARDS_MENUBAR_HEIGHT, +}; use crate::ui::logic; +/// Initialize the menubar pub fn menubar() -> Pack { - let mut m = Pack::default().with_size(0, CONSTANTS.rewards_menubar_height); + // The menubar is a horizontal pack instead of an + // actual menubar just for the customization's sake + let mut m = Pack::default().with_size(0, REWARDS_MENUBAR_HEIGHT); m.set_type(PackType::Horizontal); // 1. Add a Rate let _ab = add_button(); - // 2. Delete the Rate let _db = delete_button(); @@ -27,10 +34,14 @@ pub fn menubar() -> Pack { m } +/// Set a handler for the menubar fn menubar_logic(m: &mut T) { m.handle(|m, ev| match ev { + // In case of a pressed down button Event::KeyDown => { + // If it's a `Tab` if app::event_key() == Key::Tab { + // Focus on the Rates' list if let Some(ref p) = m.parent() { if let Some(ref mut l) = p.child(1) { l.take_focus().ok(); @@ -45,13 +56,15 @@ fn menubar_logic(m: &mut T) { }); } +/// Initialize the add button fn add_button() -> Button { let mut ai = SvgImage::from_data(include_str!("../../../assets/menu/plus.svg")).unwrap(); ai.scale(24, 24, true, true); - let mut ab = Button::default().with_size(CONSTANTS.rewards_menubar_height, 0); + let mut ab = Button::default().with_size(REWARDS_MENUBAR_HEIGHT, 0); ab.set_image(Some(ai)); - ab.set_frame(FrameType::FlatBox); + ab.set_frame(FLAT_BUTTON_FRAME_TYPE); + ab.set_down_frame(DOWN_BUTTON_FRAME_TYPE); ab.set_tooltip("Add a Rate"); add_button_logic(&mut ab); @@ -59,31 +72,27 @@ fn add_button() -> Button { ab } +/// Set a callback and a handler for the add button fn add_button_logic(ab: &mut T) { - ab.set_shortcut(Shortcut::from_char('a')); ab.set_callback(|_| { println!("Add Pressed!"); }); + // Handle the shortcut and focus / selection events ab.handle(|ab, ev| { - if ev == Event::KeyDown { - match app::event_key() { - Key::Left => logic::rp_handle_left(ab, 0), - Key::Right => logic::rp_handle_right(ab, 0), - _ => logic::handle_active_selection(ab, ev, FrameType::FlatBox), - } - } else { - logic::handle_active_selection(ab, ev, FrameType::FlatBox) - } + logic::handle_shortcut(ab, ev.bits(), events::ADD_BUTTON_SHORTCUT) + || logic::handle_button(ab, ev, 0, false) }); } +/// Initialize the delete button fn delete_button() -> Button { let mut di = SvgImage::from_data(include_str!("../../../assets/menu/minus.svg")).unwrap(); di.scale(24, 24, true, true); - let mut db = Button::default().with_size(CONSTANTS.rewards_menubar_height, 0); + let mut db = Button::default().with_size(REWARDS_MENUBAR_HEIGHT, 0); db.set_image(Some(di)); - db.set_frame(FrameType::FlatBox); + db.set_frame(FLAT_BUTTON_FRAME_TYPE); + db.set_down_frame(DOWN_BUTTON_FRAME_TYPE); db.set_tooltip("Delete the Rate"); delete_button_logic(&mut db); @@ -91,20 +100,14 @@ fn delete_button() -> Button { db } +/// Set a callback and a handler for the delete button fn delete_button_logic(db: &mut T) { - db.set_shortcut(Shortcut::from_char('d')); db.set_callback(|_| { println!("Delete Pressed!"); }); + // Handle the shortcut and focus / selection events db.handle(|db, ev| { - if ev == Event::KeyDown { - match app::event_key() { - Key::Left => logic::rp_handle_left(db, 1), - Key::Right => logic::rp_handle_right(db, 1), - _ => logic::handle_active_selection(db, ev, FrameType::FlatBox), - } - } else { - logic::handle_active_selection(db, ev, FrameType::FlatBox) - } + logic::handle_shortcut(db, ev.bits(), events::DELETE_BUTTON_SHORTCUT) + || logic::handle_button(db, ev, 1, false) }); } diff --git a/src/ui/rates/mod.rs b/src/ui/rates/mod.rs index b75e425..b48c884 100644 --- a/src/ui/rates/mod.rs +++ b/src/ui/rates/mod.rs @@ -1,3 +1,10 @@ +//! Rates Pane is a secondary pane that provides access to conversion rates. +//! +//! It gives ways for the user to: +//! - select an existing conversion rate; +//! - create a new conversion rate; +//! - delete the existing conversion rate + mod list; mod menubar; mod pane; diff --git a/src/ui/rates/pane.rs b/src/ui/rates/pane.rs index 18ffda3..0762ec3 100644 --- a/src/ui/rates/pane.rs +++ b/src/ui/rates/pane.rs @@ -1,10 +1,13 @@ +//! The pane itself. It is supposed to be the second pane in the main window. + use fltk::{group::Pack, prelude::*}; use super::{list, menubar}; -use crate::ui::app::CONSTANTS; +use crate::ui::constants::FOCUS_PANE_HEIGHT; +/// Initialize the pane pub fn pane() -> Pack { - let mut p = Pack::default().with_pos(10, 10 + CONSTANTS.focus_pane_height + 10); + let mut p = Pack::default().with_pos(10, 10 + FOCUS_PANE_HEIGHT + 10); p.set_spacing(1); // If this pane is a child of the Main Window @@ -12,15 +15,21 @@ pub fn pane() -> Pack { // Set the size, excluding the pane's margins and the height taken by the Focus Pane p.set_size( w.width() - 20, - w.height() - 10 - CONSTANTS.focus_pane_height - 10 - 10, + w.height() - 10 - FOCUS_PANE_HEIGHT - 10 - 10, ); } + // Initialize the widgets let _menubar = menubar(); let list = list(); p.end(); + + // The list's Scroll is handling the resizing p.resizable(list.scroll()); + + // This pane is hidden by default. Pressing the arrow button will bring it up p.hide(); + p } diff --git a/src/ui/rewards/edit/buttons.rs b/src/ui/rewards/edit/buttons.rs index 6df2724..fa90050 100644 --- a/src/ui/rewards/edit/buttons.rs +++ b/src/ui/rewards/edit/buttons.rs @@ -1,21 +1,26 @@ +//! The buttons pack contains the [`Ok`](mod@super::ok) and [`Cancel`](mod@super::cancel) buttons. + use fltk::{ group::{Pack, PackType}, prelude::*, }; use super::{cancel, ok}; -use crate::ui::app::CONSTANTS; +use crate::ui::constants::{REWARDS_EDIT_WINDOW_HEIGHT, REWARDS_EDIT_WINDOW_WIDTH}; +/// Initialize the buttons pack pub fn buttons() -> Pack { + // 80px is the width of the buttons, 25px — the height, 10px — the margins of the pack let mut bs = Pack::default() .with_pos( - CONSTANTS.rewards_edit_window_width - 10 - 2 * 80 - 10, - CONSTANTS.rewards_edit_window_height - 20 - 15, + REWARDS_EDIT_WINDOW_WIDTH - 10 - 2 * 80 - 10, + REWARDS_EDIT_WINDOW_HEIGHT - 25 - 10, ) - .with_size(2 * 80, 25); + .with_size(2 * 80 + 10, 25); bs.set_spacing(10); bs.set_type(PackType::Horizontal); + // Initialize the buttons let _cancel = cancel(); let _ok = ok(); diff --git a/src/ui/rewards/edit/cancel.rs b/src/ui/rewards/edit/cancel.rs index 1a11087..320f313 100644 --- a/src/ui/rewards/edit/cancel.rs +++ b/src/ui/rewards/edit/cancel.rs @@ -1,17 +1,26 @@ -use fltk::{button::Button, enums::FrameType, prelude::*}; +//! Cancel button cancels the adding / editing process and hides the window. +//! +//! The inputs are then reset by the widgets' `Hide` event handlers. +use fltk::{button::Button, prelude::*}; + +use crate::ui::constants::{BOXED_BUTTON_FRAME_TYPE, DOWN_BUTTON_FRAME_TYPE}; use crate::ui::logic; +/// Initialize the cancel button pub fn cancel() -> Button { let mut c = Button::default().with_size(80, 0).with_label("Cancel"); - c.set_down_frame(FrameType::DownBox); + c.set_frame(BOXED_BUTTON_FRAME_TYPE); + c.set_down_frame(DOWN_BUTTON_FRAME_TYPE); logic(&mut c); c } +/// Set a callback and a handler for the cancel button fn logic(b: &mut T) { + // Pushing the button will hide the window b.set_callback(|b| { if let Some(ref p) = b.parent() { if let Some(ref mut w) = p.parent() { @@ -19,5 +28,6 @@ fn logic(b: &mut T) { } } }); - b.handle(|c, ev| logic::handle_active_selection(c, ev, FrameType::BorderBox)); + // Handle focus / selection events + b.handle(|c, ev| logic::handle_selection(c, ev, BOXED_BUTTON_FRAME_TYPE, false)); } diff --git a/src/ui/rewards/edit/coins.rs b/src/ui/rewards/edit/coins.rs index f9e2981..f841958 100644 --- a/src/ui/rewards/edit/coins.rs +++ b/src/ui/rewards/edit/coins.rs @@ -1,3 +1,7 @@ +//! Coins valuator sets the number of coins in a reward. +//! +//! Dragging will change the value in the available range. + use fltk::{ app, enums::{Align, CallbackTrigger, Event, FrameType, Key}, @@ -7,16 +11,11 @@ use fltk::{ use crate::channels::Channels; use crate::events; -use crate::ui::app::CONSTANTS; +use crate::ui::constants::REWARDS_EDIT_WINDOW_WIDTH; +/// Initialize the coins valuator pub fn coins(channels: &Channels) -> ValueInput { - let mut c = ValueInput::new( - 10, - 25, - CONSTANTS.rewards_edit_window_width - 20, - 20, - "Coins:", - ); + let mut c = ValueInput::new(10, 25, REWARDS_EDIT_WINDOW_WIDTH - 20, 20, "Coins:"); c.set_frame(FrameType::BorderBox); c.set_align(Align::TopLeft); c.set_label_size(16); @@ -26,6 +25,8 @@ pub fn coins(channels: &Channels) -> ValueInput { c.set_bounds(0.0, 999_999_999.0); c.set_range(0.0, 999_999_999.0); c.set_soft(false); + + // This trick makes the default value display as `0.00` instead of `0` c.set_value(0.1); c.set_value(0.0); @@ -34,47 +35,71 @@ pub fn coins(channels: &Channels) -> ValueInput { c } +/// Set a trigger, a callback and a handler for the coins valuator fn logic(v: &mut T, channels: &Channels) { + // The value is clamped as soon as it changes v.set_trigger(CallbackTrigger::Changed); + // The callback is to clamp the valuator v.set_callback(|v| { v.set_value(v.clamp(v.value())); }); + // Handle standard and custom events v.handle({ + // Send coins let s_coins = channels.rewards_send_coins.s.clone(); + // Receive coins let r_coins = channels.rewards_receive_coins.r.clone(); move |v, ev| match ev { + // Holding `Enter` down is ignored Event::KeyDown => app::event_key() == Key::Enter, + // In case of a released button Event::KeyUp => { + // If it's `Enter` if app::event_key() == Key::Enter { + // Do the `OK` button's callback app::handle_main(events::OK_BUTTON_DO_CALLBACK).ok(); true } else { false } } + // Hiding the widget will reset its value Event::Hide => { v.set_value(0.0); true } + // In case of the _ => match ev.bits() { + // 'Add a reward: send coins' event events::ADD_A_REWARD_SEND_COINS => { + // Get the coins from the valuator s_coins.try_send(v.value()).ok(); + // Pass a signal to the Reward widget in the same window app::handle_main(events::ADD_A_REWARD_SEND_REWARD).ok(); + // Reset the valuator (the window will become hidden after this chain of events) v.set_value(0.0); true } - events::EDIT_A_REWARD_SEND_COINS => { + // 'Edit a reward: send coins' event + events::EDIT_THE_REWARD_SEND_COINS => { + // Get the coins from the valuator s_coins.try_send(v.value()).ok(); - app::handle_main(events::EDIT_A_REWARD_SEND_REWARD).ok(); + // Pass a signal to the Reward widget in the same window + app::handle_main(events::EDIT_THE_REWARD_SEND_REWARD).ok(); + // Reset the valuator (the window will become hidden after this chain of events) v.set_value(0.0); true } - events::EDIT_A_REWARD_RECEIVE_COINS => r_coins.try_recv().map_or(false, |coins| { - v.set_value(coins); - true - }), + // 'Edit a reward: receive coins` event, receive the coins + events::EDIT_THE_REWARD_RECEIVE_COINS => { + r_coins.try_recv().map_or(false, |coins| { + // Set them as a new value of the valuator + v.set_value(coins); + true + }) + } _ => false, }, } - }) + }); } diff --git a/src/ui/rewards/edit/mod.rs b/src/ui/rewards/edit/mod.rs index 0128d33..67a847e 100644 --- a/src/ui/rewards/edit/mod.rs +++ b/src/ui/rewards/edit/mod.rs @@ -1,3 +1,5 @@ +//! This module provides widgets for the [Rewards Edit](mod@crate::ui::windows::rewards_edit) window. + mod buttons; mod cancel; mod coins; diff --git a/src/ui/rewards/edit/ok.rs b/src/ui/rewards/edit/ok.rs index b4853dd..329ad49 100644 --- a/src/ui/rewards/edit/ok.rs +++ b/src/ui/rewards/edit/ok.rs @@ -1,40 +1,62 @@ -use fltk::{app, button::Button, enums::FrameType, prelude::*}; +//! OK button combines the values of the valuators in an item and sends this item to the +//! [Rewards](super)' list. +//! +//! The OK button's callback changes, depending on whether the user wants to add a new reward or +//! edit the existing one. + +use fltk::{app, button::Button, prelude::*}; use crate::events; +use crate::ui::constants::{BOXED_BUTTON_FRAME_TYPE, DOWN_BUTTON_FRAME_TYPE}; use crate::ui::logic; +/// Initialize the OK button pub fn ok() -> Button { let mut ok = Button::default().with_size(80, 0).with_label("OK"); - ok.set_down_frame(FrameType::DownBox); + ok.set_frame(BOXED_BUTTON_FRAME_TYPE); + ok.set_down_frame(DOWN_BUTTON_FRAME_TYPE); logic(&mut ok); ok } +/// Set a callback and a handler for the OK button fn logic(b: &mut T) { + // As a safety measure, the default callback is set to add a reward b.set_callback(add_callback); - b.handle(|b, ev| match ev.bits() { + // Handle the custom events + b.handle(|b, ev| + // In case of the + match ev.bits() { + // 'Do callback' event events::OK_BUTTON_DO_CALLBACK => { b.do_callback(); true } + // 'Set to add` event events::OK_BUTTON_SET_TO_ADD => { + // Set the callback to add a reward b.set_callback(add_callback); true } + // 'Set to edit` event events::OK_BUTTON_SET_TO_EDIT => { + // Set the callback to edit the reward b.set_callback(edit_callback); true } - _ => logic::handle_active_selection(b, ev, FrameType::BorderBox), + // Otherwise, handle the focus / selection events + _ => logic::handle_selection(b, ev, BOXED_BUTTON_FRAME_TYPE, false), }); } +/// 'Add a reward' callback fn add_callback(_: &mut T) { app::handle_main(events::ADD_A_REWARD_SEND_COINS).ok(); } +/// 'Edit the reward' callback fn edit_callback(_: &mut T) { - app::handle_main(events::EDIT_A_REWARD_SEND_COINS).ok(); + app::handle_main(events::EDIT_THE_REWARD_SEND_COINS).ok(); } diff --git a/src/ui/rewards/edit/reward.rs b/src/ui/rewards/edit/reward.rs index 6d6ce8e..62acaa3 100644 --- a/src/ui/rewards/edit/reward.rs +++ b/src/ui/rewards/edit/reward.rs @@ -1,3 +1,5 @@ +//! Reward input sets the reward text in a reward. + use fltk::{ app, enums::{Align, Event, FrameType, Key}, @@ -7,16 +9,11 @@ use fltk::{ use crate::channels::Channels; use crate::events; -use crate::ui::app::CONSTANTS; +use crate::ui::constants::REWARDS_EDIT_WINDOW_WIDTH; +/// Initialize the reward input pub fn reward(channels: &Channels) -> Input { - let mut r = Input::new( - 10, - 70, - CONSTANTS.rewards_edit_window_width - 20, - 20, - "Reward:", - ); + let mut r = Input::new(10, 70, REWARDS_EDIT_WINDOW_WIDTH - 20, 20, "Reward:"); r.set_frame(FrameType::BorderBox); r.set_align(Align::TopLeft); r.set_label_size(16); @@ -27,47 +24,72 @@ pub fn reward(channels: &Channels) -> Input { r } +/// Set a callback and a handler for the reward input fn logic(i: &mut T, channels: &Channels) { + // Handle standard and custom events i.handle({ + // Receive coins let r_coins = channels.rewards_send_coins.r.clone(); + // Receive a reward let r_reward = channels.rewards_receive_reward.r.clone(); + // Send an item let s_item = channels.rewards_send_item.s.clone(); + // Send signals to the main window let s_mw = channels.mw.s.clone(); move |i, ev| match ev { + // Holding `Enter` down is ignored Event::KeyDown => app::event_key() == Key::Enter, + // In case of a released button Event::KeyUp => { + // If it's `Enter` if app::event_key() == Key::Enter { + // Do the `OK` button's callback app::handle_main(events::OK_BUTTON_DO_CALLBACK).ok(); true } else { false } } + // Hiding the widget will reset its value Event::Hide => { i.set_value(""); true } + // In case of the _ => match ev.bits() { + // 'Add a reward: send reward' event, receive the coins from the coins valuator events::ADD_A_REWARD_SEND_REWARD => r_coins.try_recv().map_or(false, |coins| { + // Create an Item String and send it to the Rewards' list s_item.try_send(format!("({}) {}", coins, i.value())).ok(); + // Pass a signal to the Reward's list to receive the item s_mw.try_send(events::ADD_A_REWARD_RECEIVE).ok(); + // Reset the input i.set_value(""); + // Hide the window if let Some(ref mut w) = i.parent() { w.hide(); } true }), - events::EDIT_A_REWARD_SEND_REWARD => r_coins.try_recv().map_or(false, |coins| { + // 'Edit a reward: send reward' event, receive the coins from the coins valuator + events::EDIT_THE_REWARD_SEND_REWARD => r_coins.try_recv().map_or(false, |coins| { + // Create an Item String and send it to the Rewards' list s_item.try_send(format!("({}) {}", coins, i.value())).ok(); - s_mw.try_send(events::EDIT_A_REWARD_RECEIVE).ok(); + // Pass a signal to the Reward's list to receive the item + s_mw.try_send(events::EDIT_THE_REWARD_RECEIVE).ok(); + // Reset the input i.set_value(""); + // Hide the window if let Some(ref mut w) = i.parent() { w.hide(); } true }), - events::EDIT_A_REWARD_RECEIVE_REWARD => { + // 'Edit a reward: receive reward' event + events::EDIT_THE_REWARD_RECEIVE_REWARD => { + // Receive the reward text r_reward.try_recv().map_or(false, |ref reward| { + // Set it as a new value of the input i.set_value(reward); true }) diff --git a/src/ui/rewards/list.rs b/src/ui/rewards/list.rs index 444348f..ed2042e 100644 --- a/src/ui/rewards/list.rs +++ b/src/ui/rewards/list.rs @@ -1,127 +1,171 @@ +//! Reward's List keeps the rewards and changes them if requested. + use crossbeam_channel::Sender; -use fltk::{ - app, - group::{Pack, Scroll}, - prelude::*, -}; +use fltk::{app, enums::Key, group::Scroll, prelude::*}; use crate::channels::Channels; use crate::events; -use crate::ui::app::CONSTANTS; +use crate::ui::constants::REWARDS_MENUBAR_HEIGHT; +use crate::ui::logic; use crate::ui::widgets::list::{Holder, Items, List, Selected}; +/// Initialize the list pub fn new(channels: &Channels) -> List { let mut l = List::default(); // If this list is a child of the Rewards pane if let Some(ref p) = l.parent() { - // Set the list's height to everything except the Menubar's height - l.set_size(0, p.h() - CONSTANTS.rewards_menubar_height); + // Set the list's height to all of the available parent's height + l.set_size(0, p.h() - REWARDS_MENUBAR_HEIGHT); } + // Add examples (temporary) l.add("(15) A reward."); l.add("(10) Another reward."); l.add("(5) One more reward."); - logic(&mut l, channels); - - l -} - -fn logic(l: &mut List, channels: &Channels) { + // Apply custom logic l.handle( - handle_selection(channels), - handle_selection(channels), + handle_selection_closure(channels), + handle_selection_closure(channels), handle_custom_events(channels), ); + + l } -fn handle_selection(channels: &Channels) -> impl Fn(&mut Scroll, &Selected, &Items) { +/// Handle custom selection events in the returned closure +fn handle_selection_closure(channels: &Channels) -> impl Fn(&mut Scroll, &Selected, &Items) { + // Send the price of an item let s_price = channels.rewards_receive_coins.s.clone(); - move |_, selected, items| handle_selection_all(selected, items, &s_price) + move |_, selected, items| handle_selection(selected, items, &s_price) } -fn handle_selection_all(selected: &Selected, items: &Items, s_price: &Sender) { - if selected.get() > 0 && items.len() > 0 { +/// Handle custom selection events +fn handle_selection(selected: &Selected, items: &Items, s_price: &Sender) { + // If there is a selected item, and the event is a click or an Up / Down button + if selected.get() > 0 + && (app::event_is_click() || app::event_key() == Key::Down || app::event_key() == Key::Up) + { + // Unlock the buttons handle_selection_unlock_buttons(); + // Check if the selected reward is affordable handle_selection_check_affordability(selected, items, s_price); } } +/// Unlock the Delete / Edit buttons fn handle_selection_unlock_buttons() { app::handle_main(events::UNLOCK_THE_DELETE_A_REWARD_BUTTON).ok(); app::handle_main(events::UNLOCK_THE_EDIT_A_REWARD_BUTTON).ok(); } +/// Lock the Delete / Edit / Spend buttons fn handle_selection_lock_buttons() { app::handle_main(events::LOCK_THE_DELETE_A_REWARD_BUTTON).ok(); app::handle_main(events::LOCK_THE_EDIT_A_REWARD_BUTTON).ok(); app::handle_main(events::LOCK_THE_SPEND_BUTTON).ok(); } +/// Check if the selected item is affordable fn handle_selection_check_affordability(selected: &Selected, items: &Items, s_price: &Sender) { let index = selected.index(); - if let Some(rb_i) = items.index(index).find(')') { - if let Ok(price) = items.index(index).index(1..rb_i).parse::() { - s_price.try_send(price).ok(); - app::handle_main(events::CHECK_AFFORDABILITY_RECEIVE_PRICE).ok(); - } + // Get the price of the item + if let Some(price) = logic::get_price(&*items.index(index).string()) { + // Send it to the Coins widget for an affordability check + s_price.try_send(price).ok(); + app::handle_main(events::CHECK_AFFORDABILITY_RECEIVE_PRICE).ok(); } } +/// Handle custom events in Rewards fn handle_custom_events( channels: &Channels, -) -> impl Fn(&mut Pack, &mut Selected, &mut Items, i32) -> bool { +) -> impl Fn(&mut Scroll, &Holder, &Selected, &Items, i32) -> bool { + // Send signals to the Rewards Edit window let s_re = channels.rewards_edit.s.clone(); + // Send the price of an item let s_price = channels.rewards_receive_coins.s.clone(); + // Send an item let s_item = channels.rewards_receive_item.s.clone(); + // Receive an item let r_item = channels.rewards_send_item.r.clone(); - move |h, selected, items, bits| match bits { + + // In case of the + move |scroll, holder, selected, items, bits| match bits { + // 'Add a reward' event events::ADD_A_REWARD_RECEIVE => r_item.try_recv().map_or(false, |string| { - Holder::add_to(h, string, items); + // Add a new reward + holder.add(string, scroll, items); true }), + // 'Delete a reward' event events::DELETE_A_REWARD => { + // If there is a selected item if selected.get() > 0 { + // Get its index let index = selected.index(); + // If the selected item is the last one in the list if selected.get() == items.len() { + // Decrement the selection selected.decrement(); } + // If that's the only item in the list if items.len() == 1 { + // Lock the Edit / Delete / Spend buttons handle_selection_lock_buttons(); + // Otherwise, } else { - handle_selection_all(selected, items, &s_price); + // Handle the custom selection + handle_selection(selected, items, &s_price); } - Holder::remove(h, index, items); + // Remove the item from the list + holder.remove(index, scroll, items); + // Update the selection items.select(selected.get()); } true } - events::EDIT_A_REWARD_RECEIVE => r_item.try_recv().map_or(false, |string| { + // 'Edit a reward: receive the item' event, receive the item + events::EDIT_THE_REWARD_RECEIVE => r_item.try_recv().map_or(false, |string| { + // Update the selected item items.index_mut(selected.index()).set(string); + // Check the affordability of the selected item handle_selection_check_affordability(selected, items, &s_price); - h.redraw(); + // Redraw the holder + holder.redraw(); true }), - events::EDIT_A_REWARD_SEND_ITEM => { + // 'Edit a reward: send the item' event + events::EDIT_THE_REWARD_SEND_ITEM => { + // If there is a selected item if selected.get() > 0 { + // Get a copy of its Item String let string = items.index(selected.index()).clone(); + // Send it to the Rewards Edit window s_item.try_send(string).ok(); - s_re.try_send(events::EDIT_A_REWARD_OPEN).ok(); + s_re.try_send(events::EDIT_THE_REWARD_OPEN).ok(); + // Set the Rewards Edit window to the Edit mode s_re.try_send(events::OK_BUTTON_SET_TO_EDIT).ok(); } true } + // 'Spend coins: send price' event events::SPEND_COINS_SEND_PRICE => { + // If there is a selected item if selected.get() > 0 { - let index = selected.index(); - let rb_i = items.index(index).find(')').unwrap_or_default(); - let price = items - .index(index) - .index(1..rb_i) - .parse::() - .unwrap_or(-1.0); + // Get the price of the item (or set it to negative if couldn't parse it) + let price = if let Some(price) = + logic::get_price(&*items.index(selected.index()).string()) + { + price + } else { + -1.0 + }; + // Using this check instead of `let Some()` because + // the list gets mutated later in the chain of events if price >= 0.0 { + // Send it to the Coins widget s_price.try_send(price).ok(); app::handle_main(events::SPEND_COINS_RECEIVE_PRICE).ok(); } diff --git a/src/ui/rewards/menubar/add_button.rs b/src/ui/rewards/menubar/add_button.rs index 3d3db33..c45100c 100644 --- a/src/ui/rewards/menubar/add_button.rs +++ b/src/ui/rewards/menubar/add_button.rs @@ -1,24 +1,23 @@ -use fltk::{ - app, - button::Button, - enums::{Event, FrameType, Key, Shortcut}, - image::SvgImage, - prelude::*, -}; +//! The add button opens the [Rewards Edit](super::super::edit) window, set to add a new item. + +use fltk::{button::Button, image::SvgImage, prelude::*}; use crate::channels::Channels; use crate::events; -use crate::ui::app::CONSTANTS; +use crate::ui::constants::{ + DOWN_BUTTON_FRAME_TYPE, FLAT_BUTTON_FRAME_TYPE, REWARDS_MENUBAR_HEIGHT, +}; use crate::ui::logic; +/// Initialize the add button pub fn add_button(channels: &Channels) -> Button { let mut ai = SvgImage::from_data(include_str!("../../../../assets/menu/plus.svg")).unwrap(); ai.scale(24, 24, true, true); - let mut ab = Button::default().with_size(CONSTANTS.rewards_menubar_height, 0); + let mut ab = Button::default().with_size(REWARDS_MENUBAR_HEIGHT, 0); ab.set_image(Some(ai)); - ab.set_frame(FrameType::FlatBox); - ab.set_down_frame(FrameType::DownBox); + ab.set_frame(FLAT_BUTTON_FRAME_TYPE); + ab.set_down_frame(DOWN_BUTTON_FRAME_TYPE); ab.set_tooltip("Add a Reward"); logic(&mut ab, channels); @@ -26,21 +25,19 @@ pub fn add_button(channels: &Channels) -> Button { ab } +/// Set a callback and a handler for the add button fn logic(ab: &mut T, channels: &Channels) { - ab.set_shortcut(Shortcut::from_char('a')); ab.set_callback({ + // Send signals to the Rewards Edit window let s = channels.rewards_edit.s.clone(); move |_| { s.try_send(events::ADD_A_REWARD_OPEN).ok(); s.try_send(events::OK_BUTTON_SET_TO_ADD).ok(); } }); - ab.handle(|ab, ev| match ev { - Event::KeyDown => match app::event_key() { - Key::Left => logic::rp_handle_left(ab, 0), - Key::Right => logic::rp_handle_right(ab, 0), - _ => logic::handle_active_selection(ab, ev, FrameType::FlatBox), - }, - _ => logic::handle_active_selection(ab, ev, FrameType::FlatBox), + // Handle the shortcut and focus / selection events + ab.handle(|ab, ev| { + logic::handle_shortcut(ab, ev.bits(), events::ADD_BUTTON_SHORTCUT) + || logic::handle_button(ab, ev, 0, false) }); } diff --git a/src/ui/rewards/menubar/delete_button.rs b/src/ui/rewards/menubar/delete_button.rs index de7c6d8..a1319fd 100644 --- a/src/ui/rewards/menubar/delete_button.rs +++ b/src/ui/rewards/menubar/delete_button.rs @@ -1,23 +1,22 @@ -use fltk::{ - app, - button::Button, - enums::{Event, FrameType, Key, Shortcut}, - image::SvgImage, - prelude::*, -}; +//! The delete button deletes the selected item in the [Rewards](super::super)' list. + +use fltk::{app, button::Button, image::SvgImage, prelude::*}; use crate::events; -use crate::ui::app::CONSTANTS; +use crate::ui::constants::{ + DOWN_BUTTON_FRAME_TYPE, FLAT_BUTTON_FRAME_TYPE, REWARDS_MENUBAR_HEIGHT, +}; use crate::ui::logic; +/// Initialize the delete button pub fn delete_button() -> Button { let mut di = SvgImage::from_data(include_str!("../../../../assets/menu/minus.svg")).unwrap(); di.scale(24, 24, true, true); - let mut db = Button::default().with_size(CONSTANTS.rewards_menubar_height, 0); + let mut db = Button::default().with_size(REWARDS_MENUBAR_HEIGHT, 0); db.set_image(Some(di)); - db.set_frame(FrameType::FlatBox); - db.set_down_frame(FrameType::DownBox); + db.set_frame(FLAT_BUTTON_FRAME_TYPE); + db.set_down_frame(DOWN_BUTTON_FRAME_TYPE); db.set_tooltip("Delete the Reward"); logic(&mut db); @@ -25,41 +24,33 @@ pub fn delete_button() -> Button { db } +/// Set a callback and a handler for the delete button fn logic(db: &mut T) { - db.set_shortcut(Shortcut::from_char('d')); + // The callback is disabled by default since there + // is no reward selection at the start of the program db.set_callback(|_| {}); + // Handle the shortcut, lock, and focus / selection events db.handle({ + // This button is locked by default. Selecting an item will unlock it let mut lock = true; - let unselect_box = FrameType::FlatBox; - move |db, ev| match ev.bits() { - events::LOCK_THE_DELETE_A_REWARD_BUTTON => { - lock = true; - db.set_callback(|_| {}); - if logic::mouse_hovering_widget(db) { - logic::select_inactive(db); - } - true - } - events::UNLOCK_THE_DELETE_A_REWARD_BUTTON => { - lock = false; - db.set_callback(callback); - if logic::mouse_hovering_widget(db) { - logic::select_active(db); - } - true - } - _ => match ev { - Event::KeyDown => match app::event_key() { - Key::Left => logic::rp_handle_left(db, 1), - Key::Right => logic::rp_handle_right(db, 1), - _ => logic::handle_selection(db, ev, unselect_box, lock), - }, - _ => logic::handle_selection(db, ev, unselect_box, lock), - }, + move |db, ev| { + let bits = ev.bits(); + logic::handle_shortcut(db, bits, events::DELETE_BUTTON_SHORTCUT) + || logic::handle_lock( + db, + &mut lock, + callback, + bits, + events::LOCK_THE_DELETE_A_REWARD_BUTTON, + events::UNLOCK_THE_DELETE_A_REWARD_BUTTON, + ) + || logic::handle_button(db, ev, 1, lock) } }); } +/// The callback of the delete button when it's active fn callback(_: &mut T) { + // Send a signal to the Rewards' list to delete the selected reward app::handle_main(events::DELETE_A_REWARD).ok(); } diff --git a/src/ui/rewards/menubar/divider.rs b/src/ui/rewards/menubar/divider.rs index 4bd54c6..82de4c6 100644 --- a/src/ui/rewards/menubar/divider.rs +++ b/src/ui/rewards/menubar/divider.rs @@ -1,11 +1,18 @@ +//! Divider divides elements in the menubar. + use fltk::{frame::Frame, image::SvgImage, prelude::*}; +/// Initialize a divider pub fn divider() -> Frame { let mut di = SvgImage::from_data(include_str!("../../../../assets/menu/divider.svg")).unwrap(); + // Note that the ratio is the same as in the SVG di.scale(8, 24, true, true); let mut d = Frame::default().with_size(10, 0); d.set_image(Some(di)); + // Disabling the visible focus allows for `Left` / `Right` navigation to ignore this frame + d.visible_focus(false); + d } diff --git a/src/ui/rewards/menubar/edit_button.rs b/src/ui/rewards/menubar/edit_button.rs index fa1083f..76fa15b 100644 --- a/src/ui/rewards/menubar/edit_button.rs +++ b/src/ui/rewards/menubar/edit_button.rs @@ -1,23 +1,23 @@ -use fltk::{ - app, - button::Button, - enums::{Event, FrameType, Key, Shortcut}, - image::SvgImage, - prelude::*, -}; +//! The edit button opens the [Rewards Edit](super::super::edit) +//! window, set to edit the selected item. + +use fltk::{app, button::Button, image::SvgImage, prelude::*}; use crate::events; -use crate::ui::app::CONSTANTS; +use crate::ui::constants::{ + DOWN_BUTTON_FRAME_TYPE, FLAT_BUTTON_FRAME_TYPE, REWARDS_MENUBAR_HEIGHT, +}; use crate::ui::logic; +/// Initialize the edit button pub fn edit_button() -> Button { let mut ei = SvgImage::from_data(include_str!("../../../../assets/menu/pencil.svg")).unwrap(); ei.scale(24, 24, true, true); - let mut eb = Button::default().with_size(CONSTANTS.rewards_menubar_height, 0); + let mut eb = Button::default().with_size(REWARDS_MENUBAR_HEIGHT, 0); eb.set_image(Some(ei)); - eb.set_frame(FrameType::FlatBox); - eb.set_down_frame(FrameType::DownBox); + eb.set_frame(FLAT_BUTTON_FRAME_TYPE); + eb.set_down_frame(DOWN_BUTTON_FRAME_TYPE); eb.set_tooltip("Edit the Reward"); logic(&mut eb); @@ -25,41 +25,33 @@ pub fn edit_button() -> Button { eb } +/// Set a callback and a handler for the edit button fn logic(eb: &mut T) { - eb.set_shortcut(Shortcut::from_char('e')); + // The callback is disabled by default since there + // is no reward selection at the start of the program eb.set_callback(|_| {}); + // Handle the shortcut, lock, and focus / selection events eb.handle({ + // This button is locked by default. Selecting an item will unlock it let mut lock = true; - let unselect_box = FrameType::FlatBox; - move |eb, ev| match ev.bits() { - events::LOCK_THE_EDIT_A_REWARD_BUTTON => { - lock = true; - eb.set_callback(|_| {}); - if logic::mouse_hovering_widget(eb) { - logic::select_inactive(eb); - } - true - } - events::UNLOCK_THE_EDIT_A_REWARD_BUTTON => { - lock = false; - eb.set_callback(callback); - if logic::mouse_hovering_widget(eb) { - logic::select_active(eb); - } - true - } - _ => match ev { - Event::KeyDown => match app::event_key() { - Key::Left => logic::rp_handle_left(eb, 2), - Key::Right => logic::rp_handle_right(eb, 3), - _ => logic::handle_selection(eb, ev, unselect_box, lock), - }, - _ => logic::handle_selection(eb, ev, unselect_box, lock), - }, + move |eb, ev| { + let bits = ev.bits(); + logic::handle_shortcut(eb, bits, events::EDIT_BUTTON_SHORTCUT) + || logic::handle_lock( + eb, + &mut lock, + callback, + bits, + events::LOCK_THE_EDIT_A_REWARD_BUTTON, + events::UNLOCK_THE_EDIT_A_REWARD_BUTTON, + ) + || logic::handle_button(eb, ev, 2, lock) } }); } +/// The callback of the edit button when it's active fn callback(_: &mut T) { - app::handle_main(events::EDIT_A_REWARD_SEND_ITEM).ok(); + // Start a chain of events to edit the selected reward + app::handle_main(events::EDIT_THE_REWARD_SEND_ITEM).ok(); } diff --git a/src/ui/rewards/menubar/mod.rs b/src/ui/rewards/menubar/mod.rs index 79ee0a6..239b9ea 100644 --- a/src/ui/rewards/menubar/mod.rs +++ b/src/ui/rewards/menubar/mod.rs @@ -1,3 +1,5 @@ +//! Rewards' Menubar provides ways for the user to add / delete / edit / spend money on rewards. + mod add_button; mod delete_button; mod divider; diff --git a/src/ui/rewards/menubar/new.rs b/src/ui/rewards/menubar/new.rs index 5a54c7d..e6df63c 100644 --- a/src/ui/rewards/menubar/new.rs +++ b/src/ui/rewards/menubar/new.rs @@ -1,3 +1,5 @@ +//! The menubar initializer. + use fltk::{ app, enums::{Event, Key}, @@ -7,10 +9,11 @@ use fltk::{ use super::{add_button, delete_button, divider, edit_button, spend_button}; use crate::channels::Channels; -use crate::ui::app::CONSTANTS; +use crate::ui::constants::REWARDS_MENUBAR_HEIGHT; +/// Initialize the menubar pub fn new(channels: &Channels) -> Pack { - let mut m = Pack::default().with_size(0, CONSTANTS.rewards_menubar_height); + let mut m = Pack::default().with_size(0, REWARDS_MENUBAR_HEIGHT); m.set_type(PackType::Horizontal); // 1. Add a Reward @@ -35,10 +38,14 @@ pub fn new(channels: &Channels) -> Pack { m } +/// Set a handler for the menubar fn logic(m: &mut T) { m.handle(|m, ev| match ev { + // In case of a pressed down button Event::KeyDown => { + // If it's `Tab` if app::event_key() == Key::Tab { + // Focus on the Rewards' list if let Some(ref p) = m.parent() { if let Some(ref mut l) = p.child(1) { l.take_focus().ok(); diff --git a/src/ui/rewards/menubar/spend_button.rs b/src/ui/rewards/menubar/spend_button.rs index b38b115..fd42cdb 100644 --- a/src/ui/rewards/menubar/spend_button.rs +++ b/src/ui/rewards/menubar/spend_button.rs @@ -1,24 +1,24 @@ -use fltk::{ - app, - button::Button, - enums::{Event, FrameType, Key, Shortcut}, - image::SvgImage, - prelude::*, -}; +//! The spend button deletes the selected item and +//! subtracts its price from the total number of coins. + +use fltk::{app, button::Button, image::SvgImage, prelude::*}; use crate::events; -use crate::ui::app::CONSTANTS; +use crate::ui::constants::{ + DOWN_BUTTON_FRAME_TYPE, FLAT_BUTTON_FRAME_TYPE, REWARDS_MENUBAR_HEIGHT, +}; use crate::ui::logic; +/// Initialize the spend button pub fn spend_button() -> Button { let mut si = SvgImage::from_data(include_str!("../../../../assets/menu/insert-coin.svg")).unwrap(); si.scale(24, 24, true, true); - let mut sb = Button::default().with_size(CONSTANTS.rewards_menubar_height, 0); + let mut sb = Button::default().with_size(REWARDS_MENUBAR_HEIGHT, 0); sb.set_image(Some(si)); - sb.set_frame(FrameType::FlatBox); - sb.set_down_frame(FrameType::DownBox); + sb.set_frame(FLAT_BUTTON_FRAME_TYPE); + sb.set_down_frame(DOWN_BUTTON_FRAME_TYPE); sb.set_tooltip("Spend Coins for the Reward"); logic(&mut sb); @@ -26,41 +26,33 @@ pub fn spend_button() -> Button { sb } +/// Set a callback and a handler for the spend button fn logic(sb: &mut T) { - sb.set_shortcut(Shortcut::from_char('s')); + // The callback is disabled by default since there + // is no reward selection at the start of the program sb.set_callback(|_| {}); + // Handle the shortcut, lock, and focus / selection events sb.handle({ + // This button is locked by default. Selecting an item will unlock it let mut lock = true; - let unselect_box = FrameType::FlatBox; - move |sb, ev| match ev.bits() { - events::LOCK_THE_SPEND_BUTTON => { - lock = true; - sb.set_callback(|_| {}); - if logic::mouse_hovering_widget(sb) { - logic::select_inactive(sb); - } - true - } - events::UNLOCK_THE_SPEND_BUTTON => { - lock = false; - sb.set_callback(callback); - if logic::mouse_hovering_widget(sb) { - logic::select_active(sb); - } - true - } - _ => match ev { - Event::KeyDown => match app::event_key() { - Key::Left => logic::rp_handle_left(sb, 3), - Key::Right => logic::rp_handle_right(sb, 4), - _ => logic::handle_selection(sb, ev, unselect_box, lock), - }, - _ => logic::handle_selection(sb, ev, unselect_box, lock), - }, + move |sb, ev| { + let bits = ev.bits(); + logic::handle_shortcut(sb, bits, events::SPEND_BUTTON_SHORTCUT) + || logic::handle_lock( + sb, + &mut lock, + callback, + bits, + events::LOCK_THE_SPEND_BUTTON, + events::UNLOCK_THE_SPEND_BUTTON, + ) + || logic::handle_button(sb, ev, 4, lock) } }); } +/// The callback of the spend button when it's active fn callback(_: &mut T) { + // Start a chain of events to spend coins on the selected reward (if it's affordable) app::handle_main(events::SPEND_COINS_SEND_PRICE).ok(); } diff --git a/src/ui/rewards/mod.rs b/src/ui/rewards/mod.rs index bc8e0f4..a2bfdf9 100644 --- a/src/ui/rewards/mod.rs +++ b/src/ui/rewards/mod.rs @@ -1,3 +1,11 @@ +//! Rewards Pane is a secondary pane that provides access to rewards. +//! +//! It gives ways for the user to: +//! - select an existing reward; +//! - create a new reward; +//! - delete the existing reward; +//! - spend coins on the selected reward + pub mod edit; mod list; diff --git a/src/ui/rewards/pane.rs b/src/ui/rewards/pane.rs index 5c288c3..03c3ae8 100644 --- a/src/ui/rewards/pane.rs +++ b/src/ui/rewards/pane.rs @@ -1,11 +1,14 @@ +//! The pane itself. It is supposed to be the second pane in the main window. + use fltk::{group::Pack, prelude::*}; use super::{list, menubar}; use crate::channels::Channels; -use crate::ui::app::CONSTANTS; +use crate::ui::constants::FOCUS_PANE_HEIGHT; +/// Initialize the pane pub fn pane(channels: &Channels) -> Pack { - let mut p = Pack::default().with_pos(10, 10 + CONSTANTS.focus_pane_height + 10); + let mut p = Pack::default().with_pos(10, 10 + FOCUS_PANE_HEIGHT + 10); p.set_spacing(1); // If this pane is a child of the Main Window @@ -13,7 +16,7 @@ pub fn pane(channels: &Channels) -> Pack { // Set the size, excluding the pane's margins and the height taken by the Focus Pane p.set_size( w.width() - 20, - w.height() - 10 - CONSTANTS.focus_pane_height - 10 - 10, + w.height() - 10 - FOCUS_PANE_HEIGHT - 10 - 10, ); } diff --git a/src/ui/widgets/list/holder.rs b/src/ui/widgets/list/holder.rs index 603f423..b2c867b 100644 --- a/src/ui/widgets/list/holder.rs +++ b/src/ui/widgets/list/holder.rs @@ -1,51 +1,121 @@ -use fltk::{enums::Event, group::Pack, prelude::*}; +//! List's Holder is a wrapper around a [`Pack`](fltk::group::Pack) that manages +//! [`Items`](super::Items). -use super::{Item, Items}; +use fltk::{ + group::{Pack, Scroll}, + prelude::*, +}; +use std::{ + cell::{Ref, RefCell, RefMut}, + rc::Rc, +}; +use super::{item::Item, Items, ITEM_HEIGHT}; + +/// A wrapper around a [`Pack`](fltk::group::Pack) pub struct Holder { - holder: Pack, + /// A reference-counting pointer to a mutable [`Pack`](fltk::group::Pack) + holder: Rc>, } impl Holder { + /// Get the default struct pub fn default() -> Self { Holder { - holder: Pack::default(), + holder: Rc::>::default(), } } - pub fn handle bool + 'static>(&mut self, cb: F) { - self.holder.handle(cb) + /// Get an immutable reference to the pack + fn holder(&self) -> Ref { + self.holder.borrow() } - fn resize(h: &mut Pack, n: usize) { - if let Some(ref mut s) = h.parent() { - h.resize(s.x() + 1, s.y() + 1, s.w() - 2, n as i32 * 20); - } + /// Get a mutable reference to the pack + fn holder_mut(&self) -> RefMut { + self.holder.borrow_mut() + } + + /// Get the height of the pack + pub fn h(&self) -> i32 { + self.holder().h() + } + + /// Set the position of the pack and return it back + pub fn with_pos(self, x: i32, y: i32) -> Self { + self.holder_mut().set_pos(x, y); + self + } + + /// Set the size of the pack + pub fn set_size(&self, width: i32, height: i32) { + self.holder_mut().set_size(width, height); + } + + /// Resize the pack's height to `ITEM_HEIGHT * n` + fn resize(&self, n: usize) { + let w = self.holder().w(); + self.holder_mut().set_size(w, n as i32 * ITEM_HEIGHT); + } + + /// Redraw the pack + pub fn redraw(&self) { + self.holder_mut().redraw(); } - pub fn add_to(h: &mut Pack, string: String, items: &Items) { - Self::resize(h, items.len() + 1); + /// Add an item to the List + pub fn add(&self, string: String, scroll: &mut Scroll, items: &Items) { + // Resize the Holder (expand it) + self.resize(items.len() + 1); - h.begin(); + // Create an Item as a child of the Holder + self.holder().begin(); let item = Item::new(string); - h.end(); + self.holder().end(); - h.add(item.frame()); items.push(item); - h.redraw() + // Reset the scroll position to where it was before, redraw the Scroll + scroll.scroll_to(0, scroll.yposition()); + scroll.redraw(); } - pub fn add(&mut self, string: String, items: &Items) { - Self::add_to(&mut self.holder, string, items) - } + /// Remove an item from the List + pub fn remove(&self, index: usize, scroll: &mut Scroll, items: &Items) { + // Determine the next scroll position: + // If the first item is partially visible + let to = if items.index(0).hidden(false) { + // If the last item is completely hidden + if items.index(items.len() - 1).hidden(true) { + // Leave the scroll where it is + scroll.yposition() + // Otherwise, + } else { + // Move the scroll so that the last item is visible at the very bottom + (items.len() as i32 - 1) * ITEM_HEIGHT - scroll.h() + 2 + } + // Otherwise, + } else { + // Leave the scroll at the top + 0 + }; - pub fn remove(h: &mut Pack, index: usize, items: &mut Items) { - h.remove(items.index(index).frame()); + // Remove the item from the Items and its frame from the Holder + self.holder_mut().remove(items.index(index).frame()); items.remove(index); - Self::resize(h, items.len()); - if let Some(ref mut s) = h.parent() { - s.redraw(); + + // Resize the Holder (shrink it), change the scroll position + // if hitting the bottom of the list, redraw the Scroll + self.resize(items.len()); + scroll.scroll_to(0, to); + scroll.redraw(); + } +} + +impl Clone for Holder { + fn clone(&self) -> Self { + Holder { + holder: Rc::clone(&self.holder), } } } diff --git a/src/ui/widgets/list/item.rs b/src/ui/widgets/list/item.rs index 8554e64..4721bab 100644 --- a/src/ui/widgets/list/item.rs +++ b/src/ui/widgets/list/item.rs @@ -1,3 +1,7 @@ +//! List's Item is a wrapper around a string in the list. +//! +//! It manages its associated widget, label (the displayed string), and a selection state. + use fltk::{ draw, enums::{Align, Color, FrameType}, @@ -5,150 +9,171 @@ use fltk::{ prelude::*, }; use std::{ - cell::{Ref, RefCell, RefMut}, - ops::Range, + cell::{Ref, RefCell}, rc::Rc, }; -use crate::ui::app::CONSTANTS; -const TEXT_PADDING: i32 = 4; - -type ItemString = Rc>; -type ItemIsSelected = Rc>; -type ItemLabel = Rc>; +use super::{ITEM_HEIGHT, SCROLLBAR_WIDTH, TEXT_PADDING}; +/// An item in the List pub struct Item { + /// Associated widget frame: Frame, - string: ItemString, - selected: ItemIsSelected, - label: ItemLabel, + /// Contents of the item + string: Rc>, + /// Selection state + selected: Rc>, + /// Visible (within the width of the list) part of the [`string`](Item#structfield.string) + label: Rc>, } impl Item { + /// Initialize a new item pub fn new(string: String) -> Self { - let mut frame = Frame::default(); - frame.set_frame(FrameType::FlatBox); - - let selected = ItemIsSelected::default(); - let label = ItemLabel::default(); - - if let Some(ref h) = frame.parent() { - frame.set_size(h.w(), 20); - Self::update_label_for(&mut frame, &label, string.clone()); - frame.draw({ - let selected = Rc::clone(&selected); - let label = Rc::clone(&label); + // Create an item with default values + let mut item = Item { + frame: Frame::default(), + string: Rc::new(RefCell::new(string)), + selected: Rc::>::default(), + label: Rc::>::default(), + }; + + item.frame.set_frame(FrameType::FlatBox); + + // If the item is a child of the Holder + if let Some(ref h) = item.frame.parent() { + // Change the width of the frame and update the label + item.frame.set_size(h.w(), ITEM_HEIGHT); + item.update_label(); + + // Set a custom `draw` function + item.frame.draw({ + let selected = Rc::clone(&item.selected); + let label = Rc::clone(&item.label); move |f| { + // Saving the draw color and reapplying it in the end is a safety measure let color = draw::get_color(); + + // If the item is selected if *selected.borrow() { draw::set_draw_color(Color::White); - draw::set_font(draw::font(), 16); } else { - draw::set_font(draw::font(), 16); draw::set_draw_color(Color::Black); } + + // Draw the text within the padding box + draw::set_font(draw::font(), 16); draw::draw_text2( &*label.borrow(), f.x() + TEXT_PADDING, f.y(), - f.w() - 2 * TEXT_PADDING - CONSTANTS.scrollbar_width, + f.w() - 2 * TEXT_PADDING - SCROLLBAR_WIDTH, f.h(), Align::Left, ); + draw::set_draw_color(color); } }); } - Item { - frame, - string: ItemString::new(RefCell::new(string)), - selected, - label, - } + item } + /// Get the frame of the item pub fn frame(&self) -> &Frame { &self.frame } - /// Get the y coordinate of the item - pub fn y(&self) -> i32 { - self.frame.y() - } - - /// Get the height of the item - pub fn h(&self) -> i32 { - self.frame.h() - } - - fn string(&self) -> Ref { + /// Get the contents of the item + pub fn string(&self) -> Ref { self.string.borrow() } - fn string_mut(&self) -> RefMut { - self.string.borrow_mut() - } - + /// Clone the contents of the item pub fn clone(&self) -> String { self.string().clone() } + /// Get the selection state of the item pub fn selected(&self) -> bool { *self.selected.borrow() } - fn selected_mut(&self) -> RefMut { - self.selected.borrow_mut() - } - - pub fn find(&self, pat: char) -> Option { - self.string().find(pat) + /// Check if the item is partially or completely hidden + pub fn hidden(&self, completely: bool) -> bool { + self.frame.parent().map_or(false, |ref p| { + // Get the List's Scroll + p.parent().map_or(false, |ref s| { + // Return `true` if + if completely { + self.frame.y() > s.y() + s.h() // The top border is hidden below, or + || self.frame.y() + self.frame.h() < s.y() // The bottom border is hidden above + } else { + self.frame.y() < s.y() // The top border is hidden above, or + || self.frame.y() + self.frame.h() > s.y() + s.h() // The bottom border is hidden below + } + }) + }) } + /// Set the contents of the item to the passed string pub fn set(&mut self, string: String) { - *self.string_mut() = string.clone(); - self.update_label(string); + *self.string.borrow_mut() = string; + self.update_label(); } - fn update_label(&mut self, string: String) { - Self::update_label_for(&mut self.frame, &self.label, string) - } + /// Update the label (the visible part of the string) of the item + fn update_label(&mut self) { + // Get the copy of the item's contents + let string = self.string().clone(); - fn update_label_for(f: &mut Frame, l: &ItemLabel, string: String) { + // Setting the font size is crucial for the calculation of the visible width draw::set_font(draw::font(), 16); - let fw = f64::from(f.w()); + // Width of the frame, but + let aw = f64::from(self.frame.w()) + // Minus the width of the text padding (meaning, from the right side) + - f64::from(TEXT_PADDING) + // Minus the scrollbar width + - f64::from(SCROLLBAR_WIDTH); + // Visible width of the string let sw = draw::width(&string); + // Visible width of the "..." string let dw = draw::width("..."); - let cw = fw - f64::from(TEXT_PADDING) - f64::from(CONSTANTS.scrollbar_width); - if cw > 0.0 { - if sw < cw { - f.set_tooltip(""); - *l.borrow_mut() = string; + // If the available width is positive (this is a safety measure) + if aw > 0.0 { + // If the string's width is shorter than the available width + if sw < aw { + // Disable the tooltip + self.frame.set_tooltip(""); + // Set the label to the string + *self.label.borrow_mut() = string; + // Otherwise, } else { - let mut n = string.len(); - while draw::width(&string[..n]) + dw > cw { + // Shorten the string until it (plus the dots) fits + let mut n = self.string().len(); + while draw::width(&string[..n]) + dw > aw { n -= 1; } - f.set_tooltip(&string); - *l.borrow_mut() = string[..n].to_string() + "..."; + // Set the tooltip to the string + self.frame.set_tooltip(&string); + // Set the label to the shortened string + *self.label.borrow_mut() = self.string()[..n].to_string() + "..."; } } } - pub fn index(&self, index: Range) -> Ref { - Ref::map(self.string(), |items| &items[index]) - } - + /// Select the item pub fn select(&mut self) { - *self.selected_mut() = true; + *self.selected.borrow_mut() = true; self.frame.set_color(Color::DarkBlue); } + /// Unselect the item pub fn unselect(&mut self) { - *self.selected_mut() = false; + *self.selected.borrow_mut() = false; self.frame.set_color(Color::BackGround); } } diff --git a/src/ui/widgets/list/items.rs b/src/ui/widgets/list/items.rs index d910356..35a73e7 100644 --- a/src/ui/widgets/list/items.rs +++ b/src/ui/widgets/list/items.rs @@ -1,51 +1,66 @@ -use fltk::prelude::*; +//! List's Items is a wrapper around a vector of the [`Item`](super::Item)s. + use std::{ cell::{Ref, RefCell, RefMut}, rc::Rc, }; -use super::Item; - -pub type Array = Rc>>; +use super::item::Item; +/// A wrapper around a vector of the [`Item`](super::Item)s pub struct Items { + /// A reference-counting pointer to a mutable vector of the [`Item`](super::Item)s items: Rc>>, } impl Items { + /// Get the default struct pub fn default() -> Self { Items { - items: Array::default(), + items: Rc::>>::default(), } } + /// Get an immutable reference to the vector fn items(&self) -> Ref> { self.items.borrow() } + /// Get a mutable reference to the vector fn items_mut(&self) -> RefMut> { self.items.borrow_mut() } + /// Get the length of the vector pub fn len(&self) -> usize { self.items().len() } + /// Get an immutable reference to an item at the specified index pub fn index(&self, index: usize) -> Ref { Ref::map(self.items(), |items| &items[index]) } + /// Get a mutable reference to an item at the specified index pub fn index_mut(&self, index: usize) -> RefMut { RefMut::map(self.items_mut(), |items| &mut items[index]) } + /// Push an item to the vector pub fn push(&self, item: Item) { self.items_mut().push(item); } + /// Select an item at the specified index, unselecting all others. + /// + /// Counting of the items here starts from 1. Selecting 0, or + /// any other value outside the bounds will unselect all items. pub fn select(&self, idx: usize) { - if idx > 0 { + // If the index is in the limits of the vector (if counting from 1) + if idx > 0 && idx <= self.len() { + // Get the actual index let idx = idx - 1; + // Select the specified item, unselecting all others for i in 0..self.len() { if i == idx { self.index_mut(i).select(); @@ -53,27 +68,16 @@ impl Items { self.index_mut(i).unselect(); } } + // Otherwise, } else { + // Unselect all items for i in 0..self.len() { self.index_mut(i).unselect(); } } } - /// Return `true` if the item with the specified index is partially or completely hidden - pub fn hidden(&self, index: usize) -> bool { - let item = self.index(index); - // Get the Pack in the custom List widget - item.frame().parent().map_or(false, |ref p| { - // Get the Scroll in the custom List widget - p.parent().map_or(false, |ref s| { - // Return `true` if - item.y() < s.y() // The top border is hidden, or - || item.y() + item.h() > s.y() + s.h() // The bottom border is hidden - }) - }) - } - + /// Remove an item at the specified index from the vector pub fn remove(&self, index: usize) { self.items_mut().remove(index); } diff --git a/src/ui/widgets/list/mod.rs b/src/ui/widgets/list/mod.rs index e435bbc..24fa872 100644 --- a/src/ui/widgets/list/mod.rs +++ b/src/ui/widgets/list/mod.rs @@ -1,3 +1,7 @@ +//! List widget is a rework of the standard [`Browser`](fltk::browser::Browser) widget that removes +//! some annoyances (like the focus box) and allows for some customization (items' tooltips, +//! shortened labels, custom selection, etc). The user interface is similar where possible. + mod holder; mod item; mod items; @@ -5,8 +9,14 @@ mod selected; mod widget; pub use holder::Holder; +pub use item::Item; pub use items::Items; pub use selected::Selected; pub use widget::List; -use item::Item; +/// Height of an item in the list +const ITEM_HEIGHT: i32 = 20; +/// Width of the scrollbar +const SCROLLBAR_WIDTH: i32 = 17; +/// The text padding for the label +const TEXT_PADDING: i32 = 4; diff --git a/src/ui/widgets/list/selected.rs b/src/ui/widgets/list/selected.rs index d7b8571..8d70400 100644 --- a/src/ui/widgets/list/selected.rs +++ b/src/ui/widgets/list/selected.rs @@ -1,36 +1,45 @@ -use std::{cell::RefCell, rc::Rc}; +//! List's Selected is a wrapper around an index of the selected item. +//! +//! Note that the counting of the items starts from 1. -type Index = usize; -type IndexRc = Rc>; +use std::{cell::RefCell, rc::Rc}; +/// A wrapper around an index of the selected item pub struct Selected { - selected: IndexRc, + /// A reference-counting pointer to an index of the selected item + selected: Rc>, } impl Selected { + /// Get the default struct pub fn default() -> Self { Selected { - selected: IndexRc::default(), + selected: Rc::>::default(), } } - pub fn get(&self) -> Index { + /// Get the index + pub fn get(&self) -> usize { *self.selected.borrow() } - pub fn index(&self) -> Index { + /// Get the index into a vector + pub fn index(&self) -> usize { self.get() - 1 } - pub fn decrement(&mut self) { - self.set(self.get() - 1) + /// Decrement the index + pub fn decrement(&self) { + self.set(self.get() - 1); } - pub fn increment(&mut self) { - self.set(self.get() + 1) + /// Increment the index + pub fn increment(&self) { + self.set(self.get() + 1); } - pub fn set(&self, idx: Index) { + /// Set the index to the passed value + pub fn set(&self, idx: usize) { *self.selected.borrow_mut() = idx; } } diff --git a/src/ui/widgets/list/widget.rs b/src/ui/widgets/list/widget.rs index 9f00e66..80a1967 100644 --- a/src/ui/widgets/list/widget.rs +++ b/src/ui/widgets/list/widget.rs @@ -1,72 +1,104 @@ +//! The widget itself. +//! +//! Visually it consists of a packed into a vertical [`Scroll`](Scroll) +//! [`Holder`](Holder), which handles the items as a collection of [`Frame`](fltk::frame::Frame)s. +//! The items itself are stored in the [`Items`] vector. + use fltk::{ app, enums::{Event, FrameType, Key}, - group::{Group, Pack, Scroll, ScrollType}, + group::{Group, Scroll, ScrollType}, prelude::*, }; -use super::{Holder, Items, Selected}; -use crate::ui::app::CONSTANTS; +use super::{Holder, Items, Selected, ITEM_HEIGHT, SCROLLBAR_WIDTH}; +/// A [`Browser`](fltk::browser::Browser) replica that provides more customization pub struct List { + /// Vertical scroll scroll: Scroll, + /// Items holder holder: Holder, + /// Index of the selected item selected: Selected, + /// Vector of the items items: Items, } impl List { + /// Get the default struct pub fn default() -> List { + // The Scroll will be visible as a bounding box let mut scroll = Scroll::default(); scroll.set_frame(FrameType::BorderBox); scroll.set_type(ScrollType::Vertical); - scroll.set_scrollbar_size(CONSTANTS.scrollbar_width); + scroll.set_scrollbar_size(SCROLLBAR_WIDTH); - let holder = Holder::default(); + // Create the Holder as a child of the Scroll. Mind the 1px borders of the Scroll + let holder = Holder::default().with_pos(scroll.x() + 1, scroll.y() + 1); scroll.end(); - let mut w = List { + let mut l = List { scroll, holder, selected: Selected::default(), items: Items::default(), }; - w.handle(|_, _, _| {}, |_, _, _| {}, |_, _, _, _| false); - w + + // Set the default handles (implicitly), providing empty custom handles (explicitly) + l.handle(|_, _, _| {}, |_, _, _| {}, |_, _, _, _, _| false); + + l } + /// Get the Scroll pub fn scroll(&self) -> &Scroll { &self.scroll } + /// Get the parent of the Scroll pub fn parent(&self) -> Option { self.scroll.parent() } + /// Set the size of the widget pub fn set_size(&mut self, width: i32, height: i32) { + // If the specified width is equal to 0 if width == 0 { + // Imitate the standard behavior for the packs: inherit the width of the parent if let Some(p) = self.parent() { self.scroll.set_size(p.w(), height); + // Also, mind the borders of the Scroll + self.holder.set_size(p.w() - 2, self.holder.h()); } + // Otherwise, } else { + // Set the size to the specified values self.scroll.set_size(width, height); + // Also, mind the borders of the Scroll + self.holder.set_size(width - 2, self.holder.h()); } } + /// Add an item to the List pub fn add(&mut self, s: &'static str) { - self.holder.add(s.to_string(), &self.items); + self.holder + .add(s.to_string(), &mut self.scroll, &self.items); } + /// Select an item at the specified index (explicitly; useful in the handles) pub fn select_in(selected: &Selected, items: &Items, idx: usize) { selected.set(idx); items.select(idx); } + /// Select an item at the specified index pub fn select(&self, idx: usize) { - Self::select_in(&self.selected, &self.items, idx) + Self::select_in(&self.selected, &self.items, idx); } + /// Apply the default handles and set the custom ones pub fn handle( &mut self, handle_push: A, @@ -75,53 +107,65 @@ impl List { ) where A: Fn(&mut Scroll, &Selected, &Items), B: Fn(&mut Scroll, &Selected, &Items), - C: Fn(&mut Pack, &mut Selected, &mut Items, i32) -> bool, + C: Fn(&mut Scroll, &Holder, &Selected, &Items, i32) -> bool, { self.scroll.handle({ + let mut handle = self.holder.clone(); let mut selected = self.selected.clone(); let mut items = self.items.clone(); move |s, ev| match ev { + // Setting `true` here will allow to focus on the List Event::Focus => true, + // Apply default and custom handles for the `Push` event Event::Push => { handle_push_default(s, &mut selected, &mut items); handle_push(s, &selected, &items); true } + // Apply the default and custom handles for the `KeyDown` event Event::KeyDown => { + // If the default `KeyDown` buttons were handled if handle_key_down_default(s, &mut selected, &mut items) { + // Handle the custom `KeyDown` events for these buttons handle_key_down(s, &selected, &items); true } else { false } } - _ => false, + // Block the `Enter` button from doing anything on the `KeyUp` event + Event::KeyUp => app::event_key() == Key::Enter, + // Handle custom events (that is, signals) + _ => handle_custom_events(s, &mut handle, &mut selected, &mut items, ev.bits()), } }); - - self.holder.handle({ - let mut selected = self.selected.clone(); - let mut items = self.items.clone(); - move |l, ev| handle_custom_events(l, &mut selected, &mut items, ev.bits()) - }) } } +/// Set the default handle for the [`Push`](Event::Push) event fn handle_push_default(s: &mut Scroll, selected: &mut Selected, items: &mut Items) { s.take_focus().ok(); - if let Some(l) = s.child(0) { - // Exclude scrollbar clicks - if l.h() < s.h() - 20 - || (l.h() >= s.h() - 20 && app::event_x() < s.x() + s.w() - CONSTANTS.scrollbar_width) + // Get the Holder + if let Some(h) = s.child(0) { + // If the click happened on an item (that is, if there is a scrollbar, exclude its width) + if h.h() < s.h() - ITEM_HEIGHT + || (h.h() >= s.h() - ITEM_HEIGHT && app::event_x() < s.x() + s.w() - SCROLLBAR_WIDTH) { - let idx = ((app::event_y() - s.y() + s.yposition()) / 20) as usize + 1; + // Compute the index of the clicked item + let idx = ((app::event_y() - s.y() + s.yposition()) / ITEM_HEIGHT) as usize + 1; + // If this index is inside the bounds if idx < items.len() { + // Select an item at that index selected.set(idx); + // Otherwise (that is, if clicking at the empty space below the list), } else { + // Select the last item selected.set(items.len()); } + + // Apply the selection items.select(selected.get()); s.redraw(); @@ -129,26 +173,36 @@ fn handle_push_default(s: &mut Scroll, selected: &mut Selected, items: &mut Item } } -/// Set the default handles for the `Key::Up` and `Key::Down` events +/// Set the default handles for the [`Event::KeyDown`] events fn handle_key_down_default(s: &mut Scroll, selected: &mut Selected, items: &mut Items) -> bool { match app::event_key() { + // Pressing Up Key::Up => { - // Pressing Up without any selection or on the top item will + // Without any selection or on the top item will let to = if selected.get() == 0 || selected.get() == 1 { // Select the last item selected.set(items.len()); // Set the scroll position to the bottom of the list // (these two pixels come from the compensation of the Scroll borders) - selected.get() as i32 * 20 - s.h() + 2 - // Pressing Up on any other item will + selected.get() as i32 * ITEM_HEIGHT - s.h() + 2 + // On any other item will } else { // Select the previous item selected.decrement(); - // Set the scroll position to the top border of the previous item - selected.index() as i32 * 20 + // If the selected item is close to the bottom of the list (meaning, if the + // scroll is at the bottom of the list, this item would be visible) + if (selected.get() as i32) > (items.len() as i32) - s.h() / ITEM_HEIGHT { + // Set the scroll position to the bottom of the list + // (these two pixels come from the compensation of the Scroll borders) + items.len() as i32 * ITEM_HEIGHT - s.h() + 2 + // Otherwise, + } else { + // Set the scroll position to the top border of the previous item + selected.index() as i32 * ITEM_HEIGHT + } }; // If the selected item is partially or completely hidden, change the scroll position - if items.hidden(selected.index()) { + if items.index(selected.index()).hidden(false) { s.scroll_to(0, to); } // Select the new item, unselecting all others @@ -157,22 +211,32 @@ fn handle_key_down_default(s: &mut Scroll, selected: &mut Selected, items: &mut s.redraw(); true } + // Pressing Down Key::Down => { - // Pressing Down without on the last item will + // Without any selection or on the last item will let to = if selected.get() == items.len() { // Select the top item selected.set(1); // Set the scroll position to the top of the list 0 + // On any other item will } else { // Select the next item selected.increment(); - // Set the scroll position, so that the current item is on the top - // (these two pixels come from the compensation of the Scroll borders) - selected.get() as i32 * 20 - s.h() + 2 + // If the selected item is close to the top of the list (meaning, if the + // scroll is at the top of the list, this item would be visible) + if (selected.get() as i32) <= s.h() / ITEM_HEIGHT { + // Set the scroll position to the top of the list + 0 + // Otherwise, + } else { + // Set the scroll position, so that the current item is at the bottom + // (these two pixels come from the compensation of the Scroll borders) + selected.get() as i32 * ITEM_HEIGHT - s.h() + 2 + } }; // If the selected item is partially or completely hidden, change the scroll position - if items.hidden(selected.index()) { + if items.index(selected.index()).hidden(false) { s.scroll_to(0, to); } // Select the new item, unselecting all others @@ -181,6 +245,7 @@ fn handle_key_down_default(s: &mut Scroll, selected: &mut Selected, items: &mut s.redraw(); true } + Key::Enter => true, _ => false, } } diff --git a/src/ui/widgets/mod.rs b/src/ui/widgets/mod.rs index d17e233..6e9b923 100644 --- a/src/ui/widgets/mod.rs +++ b/src/ui/widgets/mod.rs @@ -1 +1,3 @@ +//! This module provides custom [FLTK](fltk) widgets. + pub mod list; diff --git a/src/ui/windows/icon.rs b/src/ui/windows/icon.rs index 22046dc..590dcf2 100644 --- a/src/ui/windows/icon.rs +++ b/src/ui/windows/icon.rs @@ -1,3 +1,5 @@ +//! Load a Window Icon. + use fltk::image::PngImage; /// Load a Window Icon diff --git a/src/ui/windows/main.rs b/src/ui/windows/main.rs index 7a79f8c..c90d04b 100644 --- a/src/ui/windows/main.rs +++ b/src/ui/windows/main.rs @@ -1,20 +1,27 @@ -use fltk::{prelude::*, window::Window}; +//! Main Window is the window the user sees after launching the program. + +use fltk::{ + app, + enums::{Event, Key}, + prelude::*, + window::Window, +}; use super::icon; use crate::channels::Channels; use crate::events; -use crate::ui::{app::CONSTANTS, focus, rates, rewards}; +use crate::ui::{ + constants::{FOCUS_PANE_HEIGHT, MAIN_WINDOW_HEIGHT, MAIN_WINDOW_WIDTH}, + focus, rates, rewards, +}; -/// Create the Main Window +/// Initialize the Main Window pub fn main(channels: &Channels) -> Window { - let mut w = Window::new( - 100, - 100, - CONSTANTS.main_window_width, - CONSTANTS.main_window_height, - "Shuchu", - ); + let mut w = Window::new(100, 100, MAIN_WINDOW_WIDTH, MAIN_WINDOW_HEIGHT, "Shuchu"); + + // Set the default size range expand(&mut w); + w.set_icon(Some(icon())); // 1. Focus Pane @@ -35,36 +42,75 @@ pub fn main(channels: &Channels) -> Window { w } +/// Set a handle for the window fn logic(w: &mut T) { - w.handle(|w, ev| match ev.bits() { - events::MAIN_WINDOW_HIDE_THE_PANE => { - w.resize(w.x(), w.y(), w.w(), 10 + CONSTANTS.focus_pane_height + 10); - shrink(w); - true - } - events::MAIN_WINDOW_SHOW_THE_PANE => { - w.resize(w.x(), w.y(), w.w(), CONSTANTS.main_window_height); - expand(w); - true + w.handle(|w, ev| match ev { + // Handle shortcuts. This is done manually instead of using the `set_shortcut` + // method because this way associated buttons will not take focus + Event::KeyUp => { + if app::event_key() == Key::from_char('f') { + app::handle_main(events::TIMER_SHORTCUT).ok(); + true + } else if app::event_key() == Key::from_char('r') { + app::handle_main(events::RATES_SHORTCUT).ok(); + true + } else if app::event_key() == Key::from_char('c') { + app::handle_main(events::REWARDS_SHORTCUT).ok(); + true + } else if app::event_key() == Key::from_char('a') { + app::handle_main(events::ADD_BUTTON_SHORTCUT).ok(); + true + } else if app::event_key() == Key::from_char('d') { + app::handle_main(events::DELETE_BUTTON_SHORTCUT).ok(); + true + } else if app::event_key() == Key::from_char('e') { + app::handle_main(events::EDIT_BUTTON_SHORTCUT).ok(); + true + } else if app::event_key() == Key::from_char('s') { + app::handle_main(events::SPEND_BUTTON_SHORTCUT).ok(); + true + } else { + false + } } - _ => false, + // In case of the + _ => match ev.bits() { + // 'Main window: hide the pane' event + events::MAIN_WINDOW_HIDE_THE_PANE => { + // Shrink the window + w.resize(w.x(), w.y(), w.w(), 10 + FOCUS_PANE_HEIGHT + 10); + // Shrink the size range + shrink(w); + true + } + events::MAIN_WINDOW_SHOW_THE_PANE => { + // Expand the window + w.resize(w.x(), w.y(), w.w(), MAIN_WINDOW_HEIGHT); + // Expand the size range + expand(w); + true + } + _ => false, + }, }); } +/// Shrink the size range of the window fn shrink(w: &mut T) { w.size_range( - CONSTANTS.main_window_width, - 10 + CONSTANTS.focus_pane_height + 10, - CONSTANTS.main_window_width, - 10 + CONSTANTS.focus_pane_height + 10, + MAIN_WINDOW_WIDTH, + 10 + FOCUS_PANE_HEIGHT + 10, + MAIN_WINDOW_WIDTH, + 10 + FOCUS_PANE_HEIGHT + 10, ); } +/// Expand the size range of the window fn expand(w: &mut T) { w.size_range( - CONSTANTS.main_window_width, - CONSTANTS.main_window_height, - CONSTANTS.main_window_width, - CONSTANTS.main_window_height + 100, + MAIN_WINDOW_WIDTH, + MAIN_WINDOW_HEIGHT, + MAIN_WINDOW_WIDTH, + MAIN_WINDOW_HEIGHT + 100, ); } diff --git a/src/ui/windows/mod.rs b/src/ui/windows/mod.rs index 996d329..f3a5b17 100644 --- a/src/ui/windows/mod.rs +++ b/src/ui/windows/mod.rs @@ -1,6 +1,9 @@ +//! This module defines the windows the program creates. + +pub mod rewards_edit; + mod icon; mod main; -mod rewards_edit; pub use main::main; pub use rewards_edit::rewards_edit; diff --git a/src/ui/windows/rewards_edit.rs b/src/ui/windows/rewards_edit.rs index 34b1e99..029dd74 100644 --- a/src/ui/windows/rewards_edit.rs +++ b/src/ui/windows/rewards_edit.rs @@ -1,22 +1,30 @@ +//! Rewards Edit window provides a way to add / edit items in the [Rewards](super::super::rewards) +//! list. +//! +//! This secondary window is called by the 'Add a Reward' and 'Edit the Reward' buttons in the +//! Rewards' Menubar. + use fltk::app; use fltk::{prelude::*, window::Window}; use super::icon; use crate::channels::Channels; use crate::events; -use crate::ui::app::CONSTANTS; +use crate::ui::constants::{REWARDS_EDIT_WINDOW_HEIGHT, REWARDS_EDIT_WINDOW_WIDTH}; +use crate::ui::logic; use crate::ui::rewards::edit; -/// Create the Rewards Edit Window +/// Initialize the Rewards Edit Window pub fn rewards_edit(channels: &Channels) -> Window { let mut w = Window::new( 100, 100, - CONSTANTS.rewards_edit_window_width, - CONSTANTS.rewards_edit_window_height, + REWARDS_EDIT_WINDOW_WIDTH, + REWARDS_EDIT_WINDOW_HEIGHT, "Add a Reward", ) .center_screen(); + w.set_icon(Some(icon())); w.make_modal(true); @@ -36,25 +44,36 @@ pub fn rewards_edit(channels: &Channels) -> Window { w } +/// Set a handle for the window fn logic(w: &mut T, channels: &Channels) { w.handle({ let r_item = channels.rewards_receive_item.r.clone(); let s_coins = channels.rewards_receive_coins.s.clone(); let s_reward = channels.rewards_receive_reward.s.clone(); + // In case of the move |w, ev| match ev.bits() { + // 'Add a reward` event events::ADD_A_REWARD_OPEN => { + w.set_label("Add a Reward"); w.show(); true } - events::EDIT_A_REWARD_OPEN => { + // 'Edit the reward' event + events::EDIT_THE_REWARD_OPEN => { + w.set_label("Edit the Reward"); w.show(); + // Get the item if let Ok(item) = r_item.try_recv() { - if let Some(rb_i) = item.find(')') { - if let Ok(coins) = item[1..rb_i].parse::() { - s_coins.try_send(coins).ok(); - app::handle_main(events::EDIT_A_REWARD_RECEIVE_COINS).ok(); - s_reward.try_send(item[(rb_i + 2)..].to_string()).ok(); - app::handle_main(events::EDIT_A_REWARD_RECEIVE_REWARD).ok(); + // Get the price and the text + if let Some(price) = logic::get_price(&item) { + if let Some(text) = logic::get_text(&item) { + // Send the price to the Reward Edit's Coins widget + s_coins.try_send(price).ok(); + app::handle_main(events::EDIT_THE_REWARD_RECEIVE_COINS).ok(); + + // Send the text to the Reward Edit's Reward widget + s_reward.try_send(text).ok(); + app::handle_main(events::EDIT_THE_REWARD_RECEIVE_REWARD).ok(); } } } @@ -62,5 +81,5 @@ fn logic(w: &mut T, channels: &Channels) { } _ => false, } - }) + }); } From 00deb2655a5552e3cb05082e067ec6fd4a43f698 Mon Sep 17 00:00:00 2001 From: Pavel Sobolev Date: Tue, 10 Aug 2021 18:39:38 +0300 Subject: [PATCH 14/16] Split the Rates' Menubar's module into pieces; update `fltk-rs`. --- Cargo.lock | 12 +-- Cargo.toml | 2 +- src/ui/rates/list.rs | 2 +- src/ui/rates/menubar.rs | 113 ------------------------ src/ui/rates/menubar/add_button.rs | 37 ++++++++ src/ui/rates/menubar/delete_button.rs | 37 ++++++++ src/ui/rates/menubar/mod.rs | 10 +++ src/ui/rates/menubar/new.rs | 52 +++++++++++ src/ui/rates/mod.rs | 3 - src/ui/rates/pane.rs | 6 +- src/ui/rewards/menubar/delete_button.rs | 2 +- src/ui/rewards/pane.rs | 7 +- 12 files changed, 154 insertions(+), 129 deletions(-) delete mode 100644 src/ui/rates/menubar.rs create mode 100644 src/ui/rates/menubar/add_button.rs create mode 100644 src/ui/rates/menubar/delete_button.rs create mode 100644 src/ui/rates/menubar/mod.rs create mode 100644 src/ui/rates/menubar/new.rs diff --git a/Cargo.lock b/Cargo.lock index a97b519..059b3dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -51,9 +51,9 @@ dependencies = [ [[package]] name = "fltk" -version = "1.1.4" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43ad3dcd38b2d09083c9ec0f3cfe640e3eab7015369e44cd441ba1361207e12b" +checksum = "ee82581699ebc218b80eb23e6c999e4f304d45632336fcef07d3a592619d5b3b" dependencies = [ "bitflags", "fltk-derive", @@ -65,9 +65,9 @@ dependencies = [ [[package]] name = "fltk-derive" -version = "1.1.4" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49579fb48d55e4a498b5c98e59fac28dbc5c92513ee80b52ff1ad9e17a04ae66" +checksum = "86ebc92d8a6b63d8011a47d644af7bbea495fff38e92a48c1ca933db797ca660" dependencies = [ "quote", "syn", @@ -75,9 +75,9 @@ dependencies = [ [[package]] name = "fltk-sys" -version = "1.1.4" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b583e67ee6a049c208e1aac37d2ed481f95be8325fe1a14fa4012d1d653f62" +checksum = "727a64659be2b6f39c880f3fb228df14a3c8dfc9a0b9eabcba64f29442a0d62e" dependencies = [ "cmake", "libc", diff --git a/Cargo.toml b/Cargo.toml index 8c27fbe..a7e064a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,5 +29,5 @@ codegen-units = 1 panic = 'abort' [dependencies] -fltk = { version = "=1.1.4", features = ["fltk-bundled", "use-ninja"] } +fltk = { version = "=1.1.10", features = ["fltk-bundled", "use-ninja"] } crossbeam-channel = {version = "~0.5.1"} diff --git a/src/ui/rates/list.rs b/src/ui/rates/list.rs index 4080dc5..8dced30 100644 --- a/src/ui/rates/list.rs +++ b/src/ui/rates/list.rs @@ -6,7 +6,7 @@ use crate::ui::constants::REWARDS_MENUBAR_HEIGHT; use crate::ui::widgets::list::List; /// Initialize the list -pub fn list() -> List { +pub fn new() -> List { let mut l = List::default(); // If this list is a child of the Rates pane diff --git a/src/ui/rates/menubar.rs b/src/ui/rates/menubar.rs deleted file mode 100644 index 217961d..0000000 --- a/src/ui/rates/menubar.rs +++ /dev/null @@ -1,113 +0,0 @@ -//! Rates' Menubar provides ways to add / delete / edit the conversion rates. - -use fltk::{ - app, - button::Button, - enums::{Event, Key}, - group::{Pack, PackType}, - image::SvgImage, - prelude::*, -}; - -use crate::events; -use crate::ui::constants::{ - DOWN_BUTTON_FRAME_TYPE, FLAT_BUTTON_FRAME_TYPE, REWARDS_MENUBAR_HEIGHT, -}; -use crate::ui::logic; - -/// Initialize the menubar -pub fn menubar() -> Pack { - // The menubar is a horizontal pack instead of an - // actual menubar just for the customization's sake - let mut m = Pack::default().with_size(0, REWARDS_MENUBAR_HEIGHT); - m.set_type(PackType::Horizontal); - - // 1. Add a Rate - let _ab = add_button(); - // 2. Delete the Rate - let _db = delete_button(); - - m.end(); - - menubar_logic(&mut m); - - m -} - -/// Set a handler for the menubar -fn menubar_logic(m: &mut T) { - m.handle(|m, ev| match ev { - // In case of a pressed down button - Event::KeyDown => { - // If it's a `Tab` - if app::event_key() == Key::Tab { - // Focus on the Rates' list - if let Some(ref p) = m.parent() { - if let Some(ref mut l) = p.child(1) { - l.take_focus().ok(); - } - } - true - } else { - false - } - } - _ => false, - }); -} - -/// Initialize the add button -fn add_button() -> Button { - let mut ai = SvgImage::from_data(include_str!("../../../assets/menu/plus.svg")).unwrap(); - ai.scale(24, 24, true, true); - - let mut ab = Button::default().with_size(REWARDS_MENUBAR_HEIGHT, 0); - ab.set_image(Some(ai)); - ab.set_frame(FLAT_BUTTON_FRAME_TYPE); - ab.set_down_frame(DOWN_BUTTON_FRAME_TYPE); - ab.set_tooltip("Add a Rate"); - - add_button_logic(&mut ab); - - ab -} - -/// Set a callback and a handler for the add button -fn add_button_logic(ab: &mut T) { - ab.set_callback(|_| { - println!("Add Pressed!"); - }); - // Handle the shortcut and focus / selection events - ab.handle(|ab, ev| { - logic::handle_shortcut(ab, ev.bits(), events::ADD_BUTTON_SHORTCUT) - || logic::handle_button(ab, ev, 0, false) - }); -} - -/// Initialize the delete button -fn delete_button() -> Button { - let mut di = SvgImage::from_data(include_str!("../../../assets/menu/minus.svg")).unwrap(); - di.scale(24, 24, true, true); - - let mut db = Button::default().with_size(REWARDS_MENUBAR_HEIGHT, 0); - db.set_image(Some(di)); - db.set_frame(FLAT_BUTTON_FRAME_TYPE); - db.set_down_frame(DOWN_BUTTON_FRAME_TYPE); - db.set_tooltip("Delete the Rate"); - - delete_button_logic(&mut db); - - db -} - -/// Set a callback and a handler for the delete button -fn delete_button_logic(db: &mut T) { - db.set_callback(|_| { - println!("Delete Pressed!"); - }); - // Handle the shortcut and focus / selection events - db.handle(|db, ev| { - logic::handle_shortcut(db, ev.bits(), events::DELETE_BUTTON_SHORTCUT) - || logic::handle_button(db, ev, 1, false) - }); -} diff --git a/src/ui/rates/menubar/add_button.rs b/src/ui/rates/menubar/add_button.rs new file mode 100644 index 0000000..543f5c5 --- /dev/null +++ b/src/ui/rates/menubar/add_button.rs @@ -0,0 +1,37 @@ +//! The add button opens the Rates Edit window, set to add a new item. + +use fltk::{button::Button, image::SvgImage, prelude::*}; + +use crate::events; +use crate::ui::constants::{ + DOWN_BUTTON_FRAME_TYPE, FLAT_BUTTON_FRAME_TYPE, REWARDS_MENUBAR_HEIGHT, +}; +use crate::ui::logic; + +/// Initialize the add button +pub fn add_button() -> Button { + let mut ai = SvgImage::from_data(include_str!("../../../../assets/menu/plus.svg")).unwrap(); + ai.scale(24, 24, true, true); + + let mut ab = Button::default().with_size(REWARDS_MENUBAR_HEIGHT, 0); + ab.set_image(Some(ai)); + ab.set_frame(FLAT_BUTTON_FRAME_TYPE); + ab.set_down_frame(DOWN_BUTTON_FRAME_TYPE); + ab.set_tooltip("Add a Rate"); + + logic(&mut ab); + + ab +} + +/// Set a callback and a handler for the add button +fn logic(ab: &mut T) { + ab.set_callback(|_| { + println!("Add Pressed!"); + }); + // Handle the shortcut and focus / selection events + ab.handle(|ab, ev| { + logic::handle_shortcut(ab, ev.bits(), events::ADD_BUTTON_SHORTCUT) + || logic::handle_button(ab, ev, 0, false) + }); +} diff --git a/src/ui/rates/menubar/delete_button.rs b/src/ui/rates/menubar/delete_button.rs new file mode 100644 index 0000000..781a62d --- /dev/null +++ b/src/ui/rates/menubar/delete_button.rs @@ -0,0 +1,37 @@ +//! The delete button deletes the selected item in the [Rates](super::super)' List. + +use fltk::{button::Button, image::SvgImage, prelude::*}; + +use crate::events; +use crate::ui::constants::{ + DOWN_BUTTON_FRAME_TYPE, FLAT_BUTTON_FRAME_TYPE, REWARDS_MENUBAR_HEIGHT, +}; +use crate::ui::logic; + +/// Initialize the delete button +pub fn delete_button() -> Button { + let mut di = SvgImage::from_data(include_str!("../../../../assets/menu/minus.svg")).unwrap(); + di.scale(24, 24, true, true); + + let mut db = Button::default().with_size(REWARDS_MENUBAR_HEIGHT, 0); + db.set_image(Some(di)); + db.set_frame(FLAT_BUTTON_FRAME_TYPE); + db.set_down_frame(DOWN_BUTTON_FRAME_TYPE); + db.set_tooltip("Delete the Rate"); + + logic(&mut db); + + db +} + +/// Set a callback and a handler for the delete button +fn logic(db: &mut T) { + db.set_callback(|_| { + println!("Delete Pressed!"); + }); + // Handle the shortcut and focus / selection events + db.handle(|db, ev| { + logic::handle_shortcut(db, ev.bits(), events::DELETE_BUTTON_SHORTCUT) + || logic::handle_button(db, ev, 1, false) + }); +} diff --git a/src/ui/rates/menubar/mod.rs b/src/ui/rates/menubar/mod.rs new file mode 100644 index 0000000..971e287 --- /dev/null +++ b/src/ui/rates/menubar/mod.rs @@ -0,0 +1,10 @@ +//! Rates' Menubar provides ways to add / delete / edit the conversion rates. + +mod add_button; +mod delete_button; +mod new; + +pub use new::new; + +use add_button::add_button; +use delete_button::delete_button; diff --git a/src/ui/rates/menubar/new.rs b/src/ui/rates/menubar/new.rs new file mode 100644 index 0000000..f44e2ea --- /dev/null +++ b/src/ui/rates/menubar/new.rs @@ -0,0 +1,52 @@ +//! The menubar initializer. + +use fltk::{ + app, + enums::{Event, Key}, + group::{Pack, PackType}, + prelude::*, +}; + +use super::{add_button, delete_button}; +use crate::ui::constants::REWARDS_MENUBAR_HEIGHT; + +/// Initialize the menubar +pub fn new() -> Pack { + // The menubar is a horizontal pack instead of an + // actual menubar just for the customization's sake + let mut m = Pack::default().with_size(0, REWARDS_MENUBAR_HEIGHT); + m.set_type(PackType::Horizontal); + + // 1. Add a Rate + let _ab = add_button(); + // 2. Delete the Rate + let _db = delete_button(); + + m.end(); + + logic(&mut m); + + m +} + +/// Set a handler for the menubar +fn logic(m: &mut T) { + m.handle(|m, ev| match ev { + // In case of a pressed down button + Event::KeyDown => { + // If it's a `Tab` + if app::event_key() == Key::Tab { + // Focus on the Rates' list + if let Some(ref p) = m.parent() { + if let Some(ref mut l) = p.child(1) { + l.take_focus().ok(); + } + } + true + } else { + false + } + } + _ => false, + }); +} diff --git a/src/ui/rates/mod.rs b/src/ui/rates/mod.rs index b48c884..67a9d67 100644 --- a/src/ui/rates/mod.rs +++ b/src/ui/rates/mod.rs @@ -10,6 +10,3 @@ mod menubar; mod pane; pub use pane::pane; - -use list::list; -use menubar::menubar; diff --git a/src/ui/rates/pane.rs b/src/ui/rates/pane.rs index 0762ec3..58f6e57 100644 --- a/src/ui/rates/pane.rs +++ b/src/ui/rates/pane.rs @@ -20,15 +20,15 @@ pub fn pane() -> Pack { } // Initialize the widgets - let _menubar = menubar(); - let list = list(); + let _menubar = menubar::new(); + let list = list::new(); p.end(); // The list's Scroll is handling the resizing p.resizable(list.scroll()); - // This pane is hidden by default. Pressing the arrow button will bring it up + // This pane is hidden by default. Pressing the Arrow button will bring it up p.hide(); p diff --git a/src/ui/rewards/menubar/delete_button.rs b/src/ui/rewards/menubar/delete_button.rs index a1319fd..5853ef2 100644 --- a/src/ui/rewards/menubar/delete_button.rs +++ b/src/ui/rewards/menubar/delete_button.rs @@ -1,4 +1,4 @@ -//! The delete button deletes the selected item in the [Rewards](super::super)' list. +//! The delete button deletes the selected item in the [Rewards](super::super)' List. use fltk::{app, button::Button, image::SvgImage, prelude::*}; diff --git a/src/ui/rewards/pane.rs b/src/ui/rewards/pane.rs index 03c3ae8..bad62cd 100644 --- a/src/ui/rewards/pane.rs +++ b/src/ui/rewards/pane.rs @@ -20,10 +20,15 @@ pub fn pane(channels: &Channels) -> Pack { ); } + // Initialize the widgets let _menubar = menubar::new(channels); let list = list::new(channels); - p.resizable(list.scroll()); + // This pane is shown by default. Pressing the Coins button will hide it p.end(); + + // The list's Scroll is handling the resizing + p.resizable(list.scroll()); + p } From 9fd8f0a4e38b09d2b9f6dca52b175fd2af9ee859 Mon Sep 17 00:00:00 2001 From: Pavel Sobolev Date: Tue, 10 Aug 2021 19:14:36 +0300 Subject: [PATCH 15/16] Move the "change the OK button's callback" events to the window's handler. --- src/ui/rewards/list.rs | 2 -- src/ui/rewards/menubar/add_button.rs | 2 +- src/ui/windows/rewards_edit.rs | 6 ++++++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/ui/rewards/list.rs b/src/ui/rewards/list.rs index ed2042e..b53eea9 100644 --- a/src/ui/rewards/list.rs +++ b/src/ui/rewards/list.rs @@ -145,8 +145,6 @@ fn handle_custom_events( // Send it to the Rewards Edit window s_item.try_send(string).ok(); s_re.try_send(events::EDIT_THE_REWARD_OPEN).ok(); - // Set the Rewards Edit window to the Edit mode - s_re.try_send(events::OK_BUTTON_SET_TO_EDIT).ok(); } true } diff --git a/src/ui/rewards/menubar/add_button.rs b/src/ui/rewards/menubar/add_button.rs index c45100c..f25ade5 100644 --- a/src/ui/rewards/menubar/add_button.rs +++ b/src/ui/rewards/menubar/add_button.rs @@ -27,12 +27,12 @@ pub fn add_button(channels: &Channels) -> Button { /// Set a callback and a handler for the add button fn logic(ab: &mut T, channels: &Channels) { + // Open the Rewards Edit window on push, set to add a new reward ab.set_callback({ // Send signals to the Rewards Edit window let s = channels.rewards_edit.s.clone(); move |_| { s.try_send(events::ADD_A_REWARD_OPEN).ok(); - s.try_send(events::OK_BUTTON_SET_TO_ADD).ok(); } }); // Handle the shortcut and focus / selection events diff --git a/src/ui/windows/rewards_edit.rs b/src/ui/windows/rewards_edit.rs index 029dd74..c697962 100644 --- a/src/ui/windows/rewards_edit.rs +++ b/src/ui/windows/rewards_edit.rs @@ -56,12 +56,18 @@ fn logic(w: &mut T, channels: &Channels) { events::ADD_A_REWARD_OPEN => { w.set_label("Add a Reward"); w.show(); + // Set the OK button's callback to 'Add a reward' (this is done after the window + // is shown, so that the OK button is able to receive it) + app::handle_main(events::OK_BUTTON_SET_TO_ADD).ok(); true } // 'Edit the reward' event events::EDIT_THE_REWARD_OPEN => { w.set_label("Edit the Reward"); w.show(); + // Set the OK button's callback to 'Edit a reward' (this is done after the window + // is shown, so that the OK button is able to receive it) + app::handle_main(events::OK_BUTTON_SET_TO_EDIT).ok(); // Get the item if let Ok(item) = r_item.try_recv() { // Get the price and the text From 6b51b75ce27ba512a8984af70c84281729b21767 Mon Sep 17 00:00:00 2001 From: Pavel Sobolev Date: Tue, 10 Aug 2021 19:39:05 +0300 Subject: [PATCH 16/16] Move the Rewards Edit's widgets' modules to the `windows` module; also: - Update the docstrings to have items' names capitalized and without The's. --- src/ui/rates/menubar/add_button.rs | 2 +- src/ui/rates/menubar/delete_button.rs | 2 +- src/ui/rates/menubar/new.rs | 2 +- src/ui/rewards/edit/mod.rs | 14 ------------- src/ui/rewards/menubar/add_button.rs | 3 ++- src/ui/rewards/menubar/delete_button.rs | 2 +- src/ui/rewards/menubar/edit_button.rs | 4 ++-- src/ui/rewards/menubar/new.rs | 2 +- src/ui/rewards/menubar/spend_button.rs | 4 ++-- src/ui/rewards/mod.rs | 2 -- src/ui/windows/mod.rs | 2 +- .../edit => windows/rewards_edit}/buttons.rs | 0 .../edit => windows/rewards_edit}/cancel.rs | 0 .../edit => windows/rewards_edit}/coins.rs | 0 src/ui/windows/rewards_edit/mod.rs | 20 +++++++++++++++++++ .../{rewards_edit.rs => rewards_edit/new.rs} | 17 ++++++---------- .../edit => windows/rewards_edit}/ok.rs | 0 .../edit => windows/rewards_edit}/reward.rs | 0 18 files changed, 38 insertions(+), 38 deletions(-) delete mode 100644 src/ui/rewards/edit/mod.rs rename src/ui/{rewards/edit => windows/rewards_edit}/buttons.rs (100%) rename src/ui/{rewards/edit => windows/rewards_edit}/cancel.rs (100%) rename src/ui/{rewards/edit => windows/rewards_edit}/coins.rs (100%) create mode 100644 src/ui/windows/rewards_edit/mod.rs rename src/ui/windows/{rewards_edit.rs => rewards_edit/new.rs} (85%) rename src/ui/{rewards/edit => windows/rewards_edit}/ok.rs (100%) rename src/ui/{rewards/edit => windows/rewards_edit}/reward.rs (100%) diff --git a/src/ui/rates/menubar/add_button.rs b/src/ui/rates/menubar/add_button.rs index 543f5c5..f073416 100644 --- a/src/ui/rates/menubar/add_button.rs +++ b/src/ui/rates/menubar/add_button.rs @@ -1,4 +1,4 @@ -//! The add button opens the Rates Edit window, set to add a new item. +//! Add Button opens the Rates Edit window, set to add a new item. use fltk::{button::Button, image::SvgImage, prelude::*}; diff --git a/src/ui/rates/menubar/delete_button.rs b/src/ui/rates/menubar/delete_button.rs index 781a62d..ca28f08 100644 --- a/src/ui/rates/menubar/delete_button.rs +++ b/src/ui/rates/menubar/delete_button.rs @@ -1,4 +1,4 @@ -//! The delete button deletes the selected item in the [Rates](super::super)' List. +//! Delete Button deletes the selected item in the [Rates](super::super)' List. use fltk::{button::Button, image::SvgImage, prelude::*}; diff --git a/src/ui/rates/menubar/new.rs b/src/ui/rates/menubar/new.rs index f44e2ea..93b8231 100644 --- a/src/ui/rates/menubar/new.rs +++ b/src/ui/rates/menubar/new.rs @@ -1,4 +1,4 @@ -//! The menubar initializer. +//! Menubar initializer. use fltk::{ app, diff --git a/src/ui/rewards/edit/mod.rs b/src/ui/rewards/edit/mod.rs deleted file mode 100644 index 67a847e..0000000 --- a/src/ui/rewards/edit/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -//! This module provides widgets for the [Rewards Edit](mod@crate::ui::windows::rewards_edit) window. - -mod buttons; -mod cancel; -mod coins; -mod ok; -mod reward; - -pub use buttons::buttons; -pub use coins::coins; -pub use reward::reward; - -use cancel::cancel; -use ok::ok; diff --git a/src/ui/rewards/menubar/add_button.rs b/src/ui/rewards/menubar/add_button.rs index f25ade5..60f2dbf 100644 --- a/src/ui/rewards/menubar/add_button.rs +++ b/src/ui/rewards/menubar/add_button.rs @@ -1,4 +1,5 @@ -//! The add button opens the [Rewards Edit](super::super::edit) window, set to add a new item. +//! Add Button opens the [Rewards Edit](mod@crate::ui::windows::rewards_edit) window, set to +//! add a new item. use fltk::{button::Button, image::SvgImage, prelude::*}; diff --git a/src/ui/rewards/menubar/delete_button.rs b/src/ui/rewards/menubar/delete_button.rs index 5853ef2..576fb12 100644 --- a/src/ui/rewards/menubar/delete_button.rs +++ b/src/ui/rewards/menubar/delete_button.rs @@ -1,4 +1,4 @@ -//! The delete button deletes the selected item in the [Rewards](super::super)' List. +//! Delete Button deletes the selected item in the [Rewards](super::super)' List. use fltk::{app, button::Button, image::SvgImage, prelude::*}; diff --git a/src/ui/rewards/menubar/edit_button.rs b/src/ui/rewards/menubar/edit_button.rs index 76fa15b..de569a8 100644 --- a/src/ui/rewards/menubar/edit_button.rs +++ b/src/ui/rewards/menubar/edit_button.rs @@ -1,5 +1,5 @@ -//! The edit button opens the [Rewards Edit](super::super::edit) -//! window, set to edit the selected item. +//! Edit Button opens the [Rewards Edit](mod@crate::ui::windows::rewards_edit) window, set to +//! edit the selected item. use fltk::{app, button::Button, image::SvgImage, prelude::*}; diff --git a/src/ui/rewards/menubar/new.rs b/src/ui/rewards/menubar/new.rs index e6df63c..d468b94 100644 --- a/src/ui/rewards/menubar/new.rs +++ b/src/ui/rewards/menubar/new.rs @@ -1,4 +1,4 @@ -//! The menubar initializer. +//! Menubar initializer. use fltk::{ app, diff --git a/src/ui/rewards/menubar/spend_button.rs b/src/ui/rewards/menubar/spend_button.rs index fd42cdb..1bb7dec 100644 --- a/src/ui/rewards/menubar/spend_button.rs +++ b/src/ui/rewards/menubar/spend_button.rs @@ -1,5 +1,5 @@ -//! The spend button deletes the selected item and -//! subtracts its price from the total number of coins. +//! Spend Button deletes the selected item and subtracts its price from the total number of +//! coins. use fltk::{app, button::Button, image::SvgImage, prelude::*}; diff --git a/src/ui/rewards/mod.rs b/src/ui/rewards/mod.rs index a2bfdf9..70e77b3 100644 --- a/src/ui/rewards/mod.rs +++ b/src/ui/rewards/mod.rs @@ -6,8 +6,6 @@ //! - delete the existing reward; //! - spend coins on the selected reward -pub mod edit; - mod list; mod menubar; mod pane; diff --git a/src/ui/windows/mod.rs b/src/ui/windows/mod.rs index f3a5b17..fe3b7ea 100644 --- a/src/ui/windows/mod.rs +++ b/src/ui/windows/mod.rs @@ -6,6 +6,6 @@ mod icon; mod main; pub use main::main; -pub use rewards_edit::rewards_edit; +pub use rewards_edit::new as rewards_edit; use icon::icon; diff --git a/src/ui/rewards/edit/buttons.rs b/src/ui/windows/rewards_edit/buttons.rs similarity index 100% rename from src/ui/rewards/edit/buttons.rs rename to src/ui/windows/rewards_edit/buttons.rs diff --git a/src/ui/rewards/edit/cancel.rs b/src/ui/windows/rewards_edit/cancel.rs similarity index 100% rename from src/ui/rewards/edit/cancel.rs rename to src/ui/windows/rewards_edit/cancel.rs diff --git a/src/ui/rewards/edit/coins.rs b/src/ui/windows/rewards_edit/coins.rs similarity index 100% rename from src/ui/rewards/edit/coins.rs rename to src/ui/windows/rewards_edit/coins.rs diff --git a/src/ui/windows/rewards_edit/mod.rs b/src/ui/windows/rewards_edit/mod.rs new file mode 100644 index 0000000..4dc1616 --- /dev/null +++ b/src/ui/windows/rewards_edit/mod.rs @@ -0,0 +1,20 @@ +//! Rewards Edit window provides a way to add / edit items in the [Rewards](super::super::rewards) +//! list. +//! +//! This secondary window is called by the 'Add a Reward' and 'Edit the Reward' buttons in the +//! Rewards' Menubar. + +mod buttons; +mod cancel; +mod coins; +mod new; +mod ok; +mod reward; + +pub use buttons::buttons; +pub use coins::coins; +pub use new::new; +pub use reward::reward; + +use cancel::cancel; +use ok::ok; diff --git a/src/ui/windows/rewards_edit.rs b/src/ui/windows/rewards_edit/new.rs similarity index 85% rename from src/ui/windows/rewards_edit.rs rename to src/ui/windows/rewards_edit/new.rs index c697962..97eaa8a 100644 --- a/src/ui/windows/rewards_edit.rs +++ b/src/ui/windows/rewards_edit/new.rs @@ -1,21 +1,16 @@ -//! Rewards Edit window provides a way to add / edit items in the [Rewards](super::super::rewards) -//! list. -//! -//! This secondary window is called by the 'Add a Reward' and 'Edit the Reward' buttons in the -//! Rewards' Menubar. +//! Window initializer. Reexported as the main module. use fltk::app; use fltk::{prelude::*, window::Window}; -use super::icon; +use super::{super::icon, buttons, coins, reward}; use crate::channels::Channels; use crate::events; use crate::ui::constants::{REWARDS_EDIT_WINDOW_HEIGHT, REWARDS_EDIT_WINDOW_WIDTH}; use crate::ui::logic; -use crate::ui::rewards::edit; /// Initialize the Rewards Edit Window -pub fn rewards_edit(channels: &Channels) -> Window { +pub fn new(channels: &Channels) -> Window { let mut w = Window::new( 100, 100, @@ -29,13 +24,13 @@ pub fn rewards_edit(channels: &Channels) -> Window { w.make_modal(true); // 1. Coins - let _c = edit::coins(channels); + let _c = coins(channels); // 2. Reward - let _r = edit::reward(channels); + let _r = reward(channels); // 3. Buttons - let _bs = edit::buttons(); + let _bs = buttons(); w.end(); diff --git a/src/ui/rewards/edit/ok.rs b/src/ui/windows/rewards_edit/ok.rs similarity index 100% rename from src/ui/rewards/edit/ok.rs rename to src/ui/windows/rewards_edit/ok.rs diff --git a/src/ui/rewards/edit/reward.rs b/src/ui/windows/rewards_edit/reward.rs similarity index 100% rename from src/ui/rewards/edit/reward.rs rename to src/ui/windows/rewards_edit/reward.rs