From 171cf3f7330752933cc9b42d9435c0e8a7eeeaf9 Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Sat, 19 Oct 2024 08:16:26 -0700 Subject: [PATCH] Refactor ScrollBars to their own widget Closes #181 --- CHANGELOG.md | 9 + Cargo.lock | 107 +++-- examples/nested-scroll.rs | 3 +- examples/tilemap.rs | 9 +- examples/unsaved-changes.rs | 28 +- src/dialog.rs | 6 +- src/lib.rs | 23 +- src/widget.rs | 8 +- src/widgets/button.rs | 2 +- src/widgets/custom.rs | 8 +- src/widgets/expand.rs | 16 +- src/widgets/layers.rs | 16 +- src/widgets/progress.rs | 2 +- src/widgets/scroll.rs | 894 +++++++++++++++++++++++------------- 14 files changed, 693 insertions(+), 438 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f014af8..644bfa3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `MakeWidgetList::into_layers` -> `IntoWidgetList::into_layers` - `MakeWidgetList::into_wrap` -> `IntoWidgetList::into_wrap` - `MakeWidgetList::into_list` -> `IntoWidgetList::into_list` +- `ConstraintLimit::fit_measured` and `FitMeasuredSize::fit_measured` now accept + either a `Px` or `UPx` measurement, and does not perform scaling adjustments. + To convert `Lp` use `into_upx()` first. ### Changed @@ -115,6 +118,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 allow overdrawing the widget's bounds. This was noticable in the nested-scroll.rs example when reducing the height of the window below 6 inches. +- `Scroll` now uses the new `ScrollBar` widget for its bars rather than manually + drawing them. By making this change, the bars now have input priority over the + contents. This means that the scroll bars are now clickable even in areas + where interactive widgets are beneath them. ### Added @@ -252,6 +259,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `Scroll` now exposes its scroll amount, maximum scroll, and more information that allows completely customizing a scroll view's behavior. Thanks to @danbulant for helping with this change! +- `ScrollBar` is a new widget that renders a scroll bar meant to scroll through + a large container. [139]: https://github.com/khonsulabs/cushy/issues/139 diff --git a/Cargo.lock b/Cargo.lock index c911662..7b3fb0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -117,9 +117,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.89" +version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" +checksum = "37bf3594c4c988a53154954629820791dde498571819ae4ca50ca811e060cc95" [[package]] name = "appit" @@ -203,9 +203,9 @@ dependencies = [ [[package]] name = "ashpd" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe7e0dd0ac5a401dc116ed9f9119cf9decc625600474cb41f0fc0a0050abc9a" +checksum = "4d43c03d9e36dd40cab48435be0b09646da362c278223ca535493877b2c1dee9" dependencies = [ "async-fs", "async-net", @@ -435,9 +435,9 @@ dependencies = [ [[package]] name = "avif-serialize" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876c75a42f6364451a033496a14c44bffe41f5f4a8236f697391f11024e596d2" +checksum = "e335041290c43101ca215eed6f43ec437eb5a42125573f600fc3fa42b9bddd62" dependencies = [ "arrayvec", ] @@ -535,9 +535,9 @@ dependencies = [ [[package]] name = "built" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "236e6289eda5a812bc6b53c3b024039382a2895fbbeef2d748b2931546d392c4" +checksum = "c360505aed52b7ec96a3636c3f039d99103c37d1d9b4f7a8c743d3ea9ffcd03b" [[package]] name = "bumpalo" @@ -553,9 +553,9 @@ checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" [[package]] name = "bytemuck" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae" +checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d" dependencies = [ "bytemuck_derive", ] @@ -617,9 +617,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.28" +version = "1.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e80e3b6a3ab07840e1cae9b0666a63970dc28e8ed5ffbcdacbfc760c281bfc1" +checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" dependencies = [ "jobserver", "libc", @@ -1190,9 +1190,9 @@ checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" [[package]] name = "flume" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" dependencies = [ "spin", ] @@ -1554,9 +1554,9 @@ dependencies = [ [[package]] name = "image" -version = "0.25.2" +version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10" +checksum = "bc144d44a31d753b02ce64093d532f55ff8dc4ebf2ffb8a63c0dda691385acae" dependencies = [ "bytemuck", "byteorder-lite", @@ -1577,9 +1577,9 @@ dependencies = [ [[package]] name = "image-webp" -version = "0.1.3" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79afb8cbee2ef20f59ccd477a218c12a93943d075b492015ecb1bb81f8ee904" +checksum = "e031e8e3d94711a9ccb5d6ea357439ef3dcbed361798bd4071dc4d9793fbe22f" dependencies = [ "byteorder-lite", "quick-error", @@ -1587,9 +1587,9 @@ dependencies = [ [[package]] name = "imgref" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126" +checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" [[package]] name = "indexmap" @@ -1690,9 +1690,9 @@ checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" [[package]] name = "js-sys" -version = "0.3.70" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" dependencies = [ "wasm-bindgen", ] @@ -1729,7 +1729,7 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "kludgine" version = "0.11.0" -source = "git+https://github.com/khonsulabs/kludgine#0df62c716c13a93ee249b79d9c831933d0d49d99" +source = "git+https://github.com/khonsulabs/kludgine#2f7755a1a9b7cae67711f7c41ee2cd2c6b12fd64" dependencies = [ "ahash", "alot", @@ -1766,9 +1766,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.159" +version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" [[package]] name = "libdbus-sys" @@ -1808,13 +1808,13 @@ checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "libredox" -version = "0.0.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3af92c55d7d839293953fcd0fda5ecfe93297cfde6ffbdec13b41d99c0ba6607" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.6.0", "libc", - "redox_syscall 0.4.1", + "redox_syscall 0.5.7", ] [[package]] @@ -2418,9 +2418,9 @@ checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "orbclient" -version = "0.3.47" +version = "0.3.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f0d54bde9774d3a51dcf281a5def240c71996bc6ca05d2c847ec8b2b216166" +checksum = "ba0b26cec2e24f08ed8bb31519a9333140a6599b867dac464bb150bdb796fd43" dependencies = [ "libredox", ] @@ -2705,27 +2705,27 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.87" +version = "1.0.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" +checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9" dependencies = [ "unicode-ident", ] [[package]] name = "profiling" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58" +checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" dependencies = [ "profiling-procmacros", ] [[package]] name = "profiling-procmacros" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" +checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30" dependencies = [ "quote", "syn 2.0.79", @@ -2865,9 +2865,9 @@ dependencies = [ [[package]] name = "ravif" -version = "0.11.10" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f0bfd976333248de2078d350bfdf182ff96e168a24d23d2436cef320dd4bdd" +checksum = "2413fd96bd0ea5cdeeb37eaf446a22e6ed7b981d792828721e74ded1980a45c6" dependencies = [ "avif-serialize", "imgref", @@ -3008,9 +3008,6 @@ name = "rgb" version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" -dependencies = [ - "bytemuck", -] [[package]] name = "roxmltree" @@ -3744,9 +3741,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" dependencies = [ "cfg-if", "once_cell", @@ -3755,9 +3752,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" dependencies = [ "bumpalo", "log", @@ -3770,9 +3767,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.43" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" dependencies = [ "cfg-if", "js-sys", @@ -3782,9 +3779,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3792,9 +3789,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", @@ -3805,9 +3802,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" [[package]] name = "wayland-backend" @@ -3920,9 +3917,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.70" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/examples/nested-scroll.rs b/examples/nested-scroll.rs index 408a224..9b343eb 100644 --- a/examples/nested-scroll.rs +++ b/examples/nested-scroll.rs @@ -1,7 +1,7 @@ use cushy::figures::units::Lp; use cushy::kludgine::cosmic_text::FamilyOwned; use cushy::styles::components::FontFamily; -use cushy::styles::FontFamilyList; +use cushy::styles::{Edges, FontFamilyList}; use cushy::widget::MakeWidget; use cushy::Run; @@ -17,6 +17,7 @@ fn main() -> cushy::Result { .height(Lp::inches(3)), ) .into_rows() + .pad_by(Edges::default().with_right(Lp::points(7))) .vertical_scroll() .expand() .run() diff --git a/examples/tilemap.rs b/examples/tilemap.rs index 530dc48..73b7ecb 100644 --- a/examples/tilemap.rs +++ b/examples/tilemap.rs @@ -3,18 +3,15 @@ use std::cmp::Ordering; use std::time::Duration; use cushy::figures::units::Px; -use cushy::figures::FloatConversion; -use cushy::figures::{Point, Rect, Size}; -use cushy::kludgine::app::winit::keyboard::Key; -use cushy::kludgine::app::winit::keyboard::NamedKey; +use cushy::figures::{FloatConversion, Point, Rect, Size}; +use cushy::kludgine::app::winit::keyboard::{Key, NamedKey}; use cushy::kludgine::drawing::Renderer; use cushy::kludgine::shapes::Shape; use cushy::kludgine::sprite::{Sprite, SpriteSource}; use cushy::kludgine::tilemap::{ DebugGrid, Object, ObjectLayer, TileArray, TileKind, TileMapFocus, TILE_SIZE, }; -use cushy::kludgine::Color; -use cushy::kludgine::{include_aseprite_sprite, DrawableExt}; +use cushy::kludgine::{include_aseprite_sprite, Color, DrawableExt}; use cushy::value::{Destination, Dynamic}; use cushy::widgets::TileMap; use cushy::{Run, Tick}; diff --git a/examples/unsaved-changes.rs b/examples/unsaved-changes.rs index b57198f..78a9603 100644 --- a/examples/unsaved-changes.rs +++ b/examples/unsaved-changes.rs @@ -1,15 +1,13 @@ -use cushy::{ - value::{Dynamic, Source}, - widget::MakeWidget, - Run, -}; - -fn main() -> cushy::Result { - let has_unsaved_changes = Dynamic::new(true); - - "Prevent Closing" - .into_checkbox(has_unsaved_changes.clone()) - .into_window() - .on_close_requested(move |()| !has_unsaved_changes.get()) - .run() -} +use cushy::value::{Dynamic, Source}; +use cushy::widget::MakeWidget; +use cushy::Run; + +fn main() -> cushy::Result { + let has_unsaved_changes = Dynamic::new(true); + + "Prevent Closing" + .into_checkbox(has_unsaved_changes.clone()) + .into_window() + .on_close_requested(move |()| !has_unsaved_changes.get()) + .run() +} diff --git a/src/dialog.rs b/src/dialog.rs index 32d7c7e..9ab6370 100644 --- a/src/dialog.rs +++ b/src/dialog.rs @@ -801,10 +801,10 @@ impl MakeWidget for FilePickerWidget { .with_explanation( format!("A file named \"{name}\" already exists. Do you want to overwrite the existing file?") ) - .with_yes({ - let callback = callback.clone(); + .with_yes({ + let callback = callback.clone(); move || { - let Some(ModeCallback::Single(cb)) = callback.lock().take() else { + let Some(ModeCallback::Single(cb)) = callback.lock().take() else { unreachable!("re-set above"); }; cb.invoke(chosen_path.clone()); diff --git a/src/lib.rs b/src/lib.rs index 4b0cfe0..28e4308 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -131,7 +131,7 @@ pub use app::{ /// ``` pub use cushy_macros::main; use figures::units::UPx; -use figures::{Fraction, ScreenUnit, Size, Zero}; +use figures::{IntoUnsigned, Size, Zero}; use kludgine::app::winit::error::EventLoopError; pub use names::Name; pub use utils::{Lazy, ModifiersExt, ModifiersStateExt, WithClone}; @@ -186,14 +186,13 @@ impl ConstraintLimit { /// If this constraint is of a known size, it will return the maximum of the /// measured size and the constraint. If it is of an unknown size, it will /// return the measured size. - pub fn fit_measured(self, measured: Unit, scale: Fraction) -> UPx + pub fn fit_measured(self, measured: Unit) -> UPx where - Unit: ScreenUnit, + Unit: IntoUnsigned, { - let measured = measured.into_upx(scale); match self { - ConstraintLimit::Fill(size) => size.max(measured), - ConstraintLimit::SizeToFit(_) => measured, + ConstraintLimit::Fill(size) => size.max(measured.into_unsigned()), + ConstraintLimit::SizeToFit(_) => measured.into_unsigned(), } } } @@ -202,19 +201,19 @@ impl ConstraintLimit { pub trait FitMeasuredSize { /// Returns the result of calling [`ConstraintLimit::fit_measured`] for each /// matching component in `self` and `measured`. - fn fit_measured(self, measured: Size, scale: Fraction) -> Size + fn fit_measured(self, measured: Size) -> Size where - Unit: ScreenUnit; + Unit: IntoUnsigned; } impl FitMeasuredSize for Size { - fn fit_measured(self, measured: Size, scale: Fraction) -> Size + fn fit_measured(self, measured: Size) -> Size where - Unit: ScreenUnit, + Unit: IntoUnsigned, { Size::new( - self.width.fit_measured(measured.width, scale), - self.height.fit_measured(measured.height, scale), + self.width.fit_measured(measured.width), + self.height.fit_measured(measured.height), ) } } diff --git a/src/widget.rs b/src/widget.rs index 8668f21..c8247df 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -592,12 +592,8 @@ pub trait WrapperWidget: Debug + Send + 'static { context: &mut LayoutContext<'_, '_, '_, '_>, ) -> WrappedLayout { Size::new( - available_space - .width - .fit_measured(size.width, context.gfx.scale()), - available_space - .height - .fit_measured(size.height, context.gfx.scale()), + available_space.width.fit_measured(size.width), + available_space.height.fit_measured(size.height), ) .into() } diff --git a/src/widgets/button.rs b/src/widgets/button.rs index 6cd27ac..027ae3b 100644 --- a/src/widgets/button.rs +++ b/src/widgets/button.rs @@ -497,7 +497,7 @@ impl Widget for Button { let mounted = self.content.mounted(context); let available_space = available_space.map(|space| space - double_padding); let size = context.for_other(&mounted).layout(available_space); - let size = available_space.fit_measured(size, context.gfx.scale()); + let size = available_space.fit_measured(size); context.set_child_layout( &mounted, Rect::new(Point::squared(padding), size).into_signed(), diff --git a/src/widgets/custom.rs b/src/widgets/custom.rs index cddac27..b165dd5 100644 --- a/src/widgets/custom.rs +++ b/src/widgets/custom.rs @@ -477,12 +477,8 @@ impl WrapperWidget for Custom { position_child.invoke(size, available_space, context) } else { Size::new( - available_space - .width - .fit_measured(size.width, context.gfx.scale()), - available_space - .height - .fit_measured(size.height, context.gfx.scale()), + available_space.width.fit_measured(size.width), + available_space.height.fit_measured(size.height), ) .into() } diff --git a/src/widgets/expand.rs b/src/widgets/expand.rs index 76ba945..0b096c1 100644 --- a/src/widgets/expand.rs +++ b/src/widgets/expand.rs @@ -113,24 +113,16 @@ impl WrapperWidget for Expand { let (width, height) = match &self.kind { ExpandKind::Weighted(_) => ( - available_space - .width - .fit_measured(size.width, context.gfx.scale()), - available_space - .height - .fit_measured(size.height, context.gfx.scale()), + available_space.width.fit_measured(size.width), + available_space.height.fit_measured(size.height), ), ExpandKind::Horizontal => ( - available_space - .width - .fit_measured(size.width, context.gfx.scale()), + available_space.width.fit_measured(size.width), size.height.min(available_space.height.max()), ), ExpandKind::Vertical => ( size.width.min(available_space.width.max()), - available_space - .height - .fit_measured(size.height, context.gfx.scale()), + available_space.height.fit_measured(size.height), ), }; diff --git a/src/widgets/layers.rs b/src/widgets/layers.rs index a9ba3ff..1655f7c 100644 --- a/src/widgets/layers.rs +++ b/src/widgets/layers.rs @@ -88,12 +88,8 @@ impl Widget for Layers { // Now we know the size of the widget, we can request the widgets fill // the allocated space. let size = Size::new( - available_space - .width - .fit_measured(size.width, context.gfx.scale()), - available_space - .height - .fit_measured(size.height, context.gfx.scale()), + available_space.width.fit_measured(size.width), + available_space.height.fit_measured(size.height), ); let layout = Rect::from(size.into_signed()); for child in self.mounted.children() { @@ -1046,12 +1042,8 @@ impl WrapperWidget for ModalLayer { } } Size::new( - available_space - .width - .fit_measured(size.width, context.gfx.scale()), - available_space - .height - .fit_measured(size.height, context.gfx.scale()), + available_space.width.fit_measured(size.width), + available_space.height.fit_measured(size.height), ) .into() } diff --git a/src/widgets/progress.rs b/src/widgets/progress.rs index de5e4cb..e067211 100644 --- a/src/widgets/progress.rs +++ b/src/widgets/progress.rs @@ -407,6 +407,6 @@ impl Widget for Spinner { let track_size = context.get(&TrackSize).into_px(context.gfx.scale()); let minimum_size = track_size * 4; - available_space.map(|constraint| constraint.fit_measured(minimum_size, context.gfx.scale())) + available_space.map(|constraint| constraint.fit_measured(minimum_size)) } } diff --git a/src/widgets/scroll.rs b/src/widgets/scroll.rs index a015853..0e312dc 100644 --- a/src/widgets/scroll.rs +++ b/src/widgets/scroll.rs @@ -1,9 +1,12 @@ //! A container that scrolls its contents on a virtual surface. -use std::time::{Duration, Instant}; + +use std::mem; +use std::time::Duration; use figures::units::{Lp, Px, UPx}; use figures::{FloatConversion, IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size, Zero}; use intentional::Cast; +use kempt::Set; use kludgine::app::winit::event::{MouseScrollDelta, TouchPhase}; use kludgine::app::winit::window::CursorIcon; use kludgine::shapes::Shape; @@ -13,11 +16,88 @@ use crate::animation::{AnimationHandle, AnimationTarget, IntoAnimate, Spawn, Zer use crate::context::{AsEventContext, EventContext, LayoutContext}; use crate::styles::components::{EasingIn, EasingOut, LineHeight}; use crate::styles::Dimension; -use crate::value::{Destination, Dynamic, DynamicReader, IntoValue, Source, Value}; -use crate::widget::{EventHandling, MakeWidget, Widget, WidgetRef, HANDLED, IGNORED}; +use crate::value::{ + Destination, Dynamic, DynamicReader, IntoDynamic, IntoValue, MapEachCloned, Source, Value, +}; +use crate::widget::{EventHandling, MakeWidget, Widget, WidgetId, WidgetRef, HANDLED, IGNORED}; use crate::window::DeviceId; use crate::ConstraintLimit; +// TODO is this useful enough to make public? +#[derive(Debug)] +struct OwnedWidget(OwnedWidgetState); + +#[derive(Debug)] +enum OwnedWidgetState { + Unmade(W), + Making, + Made(WidgetRef), +} + +impl OwnedWidget +where + W: Widget, +{ + pub const fn new(widget: W) -> Self { + Self(OwnedWidgetState::Unmade(widget)) + } + + // pub fn make(&mut self) -> &WidgetInstance { + // self.make_if_needed().widget() + // } + + pub fn make_if_needed(&mut self) -> &mut WidgetRef { + if matches!(&self.0, OwnedWidgetState::Unmade(_)) { + let OwnedWidgetState::Unmade(widget) = + mem::replace(&mut self.0, OwnedWidgetState::Making) + else { + unreachable!("just matched") + }; + + self.0 = OwnedWidgetState::Made(WidgetRef::new(widget)); + } + + self.expect_made_mut() + } + + pub fn expect_made(&self) -> &WidgetRef { + let OwnedWidgetState::Made(widget) = &self.0 else { + unreachable!("widget made") + }; + widget + } + + pub fn expect_made_mut(&mut self) -> &mut WidgetRef { + let OwnedWidgetState::Made(widget) = &mut self.0 else { + unreachable!("widget made") + }; + widget + } + + // pub fn expect_unmade(&self) -> &W { + // let OwnedWidgetState::Unmade(widget) = &self.0 else { + // unreachable!("widget unmade") + // }; + // widget + // } + + pub fn expect_unmade_mut(&mut self) -> &mut W { + let OwnedWidgetState::Unmade(widget) = &mut self.0 else { + unreachable!("widget unmade") + }; + widget + } +} + +impl Default for OwnedWidget +where + T: Widget + Default, +{ + fn default() -> Self { + Self::new(T::default()) + } +} + /// A widget that supports scrolling its contents. #[derive(Debug)] pub struct Scroll { @@ -31,46 +111,66 @@ pub struct Scroll { /// clamped and this dynamic will be updated with clamped scroll. pub scroll: Dynamic>, enabled: Point, - preserve_max_scroll: Value, - max_scroll: Dynamic>, - scrollbar_opacity: Dynamic, - scrollbar_opacity_animation: OpacityAnimationState, - horizontal_bar: ScrollbarInfo, - vertical_bar: ScrollbarInfo, - bar_width: UPx, - line_height: UPx, - drag: DragInfo, + max_scroll: DynamicReader>, + vertical_widget: OwnedWidget, + horizontal_widget: OwnedWidget, } #[derive(Debug)] struct OpacityAnimationState { + hovering: Set, + is_hide: bool, will_hide: bool, - started_at: Instant, handle: AnimationHandle, } impl Scroll { /// Returns a new scroll widget containing `contents`. fn construct(contents: impl MakeWidget, enabled: Point) -> Self { + let scroll = Dynamic::>::default(); + let content_size = Dynamic::>::default(); + let x = scroll.map_each_cloned(|scroll| scroll.x); + x.for_each_cloned({ + let scroll = scroll.clone(); + move |x| { + if let Ok(mut scroll) = scroll.try_lock() { + if scroll.x != x { + scroll.x = x; + } + } + } + }) + .persist(); + let horizontal = ScrollBar::new(content_size.map_each_cloned(|size| size.width), x, false); + + let y = scroll.map_each_cloned(|scroll| scroll.y); + y.for_each_cloned({ + let scroll = scroll.clone(); + move |y| { + if let Ok(mut scroll) = scroll.try_lock() { + if scroll.y != y { + scroll.y = y; + } + } + } + }) + .persist(); + let mut vertical = + ScrollBar::new(content_size.map_each_cloned(|size| size.height), y, true); + vertical.synchronize_visibility_with(&horizontal); + let max_scroll = (&horizontal.max_scroll(), &vertical.max_scroll()) + .map_each_cloned(|(x, y)| Point::new(x, y)) + .into_reader(); + Self { contents: WidgetRef::new(contents), enabled, - content_size: Dynamic::new(Size::default()), + content_size, control_size: Dynamic::new(Size::default()), - scroll: Dynamic::new(Point::default()), - max_scroll: Dynamic::new(Point::default()), - scrollbar_opacity: Dynamic::default(), - scrollbar_opacity_animation: OpacityAnimationState { - handle: AnimationHandle::new(), - started_at: Instant::now(), - will_hide: true, - }, - horizontal_bar: ScrollbarInfo::default(), - vertical_bar: ScrollbarInfo::default(), - bar_width: UPx::default(), - line_height: UPx::default(), - drag: DragInfo::default(), - preserve_max_scroll: Value::Constant(true), + scroll, + max_scroll, + horizontal_widget: OwnedWidget::new(horizontal), + vertical_widget: OwnedWidget::new(vertical), } } @@ -99,7 +199,11 @@ impl Scroll { /// this setting is `true`. #[must_use] pub fn preserve_max_scroll(mut self, preserve: impl IntoValue) -> Self { - self.preserve_max_scroll = preserve.into_value(); + let preserve = preserve.into_value(); + self.vertical_widget.expect_unmade_mut().preserve_max_scroll = preserve.clone(); + self.horizontal_widget + .expect_unmade_mut() + .preserve_max_scroll = preserve; self } @@ -107,8 +211,8 @@ impl Scroll { /// /// This represents the maximum amount that the scroll can be moved by. #[must_use] - pub fn max_scroll(&self) -> DynamicReader> { - self.max_scroll.create_reader() + pub const fn max_scroll(&self) -> &DynamicReader> { + &self.max_scroll } /// Returns a reader for the size of the scrollable area. @@ -123,63 +227,20 @@ impl Scroll { self.control_size.create_reader() } - fn constrained_scroll(scroll: Point, max_scroll: Point) -> Point { - scroll.min(max_scroll) - } - - fn constrain_scroll(&mut self) -> (Point, Point) { - let scroll = self.scroll.get(); - let max_scroll = self.max_scroll.get(); - let clamped = Self::constrained_scroll(scroll, max_scroll); - if clamped != scroll { - self.scroll.set(clamped); - } - (clamped, max_scroll) - } - fn show_scrollbars(&mut self, context: &mut EventContext<'_>) { - let should_hide = self.drag.mouse_buttons_down == 0; - if should_hide != self.scrollbar_opacity_animation.will_hide - || self.scrollbar_opacity_animation.handle.is_complete() - // Prevent respawning the same animation multiple times if we get a - // lot of events. - || self.scrollbar_opacity_animation.started_at.elapsed() > Duration::from_millis(500) - { - let current_opacity = self.scrollbar_opacity.get(); - let transition_time = *current_opacity.one_minus() / 4.; - let animation = self - .scrollbar_opacity - .transition_to(ZeroToOne::ONE) - .over(Duration::from_secs_f32(transition_time)) - .with_easing(context.get(&EasingIn)); - - self.scrollbar_opacity_animation.will_hide = should_hide; - self.scrollbar_opacity_animation.handle = if should_hide { - animation - .and_then(Duration::from_secs(1)) - .and_then( - self.scrollbar_opacity - .transition_to(ZeroToOne::ZERO) - .over(Duration::from_millis(300)) - .with_easing(context.get(&EasingOut)), - ) - .spawn() - } else { - animation.spawn() - }; - } + let mut horizontal = self.horizontal_widget.expect_made_mut().widget().lock(); + horizontal + .downcast_mut::() + .expect("a ScrollBar") + .show(context); } fn hide_scrollbars(&mut self, context: &mut EventContext<'_>) { - if self.drag.mouse_buttons_down == 0 && !self.scrollbar_opacity_animation.will_hide { - self.scrollbar_opacity_animation.will_hide = true; - self.scrollbar_opacity_animation.handle = self - .scrollbar_opacity - .transition_to(ZeroToOne::ZERO) - .over(Duration::from_millis(300)) - .with_easing(context.get(&EasingOut)) - .spawn(); - } + let mut horizontal = self.horizontal_widget.expect_made_mut().widget().lock(); + horizontal + .downcast_mut::() + .expect("a ScrollBar") + .hide(context); } } @@ -207,33 +268,21 @@ impl Widget for Scroll { } fn redraw(&mut self, context: &mut crate::context::GraphicsContext<'_, '_, '_, '_>) { - context.redraw_when_changed(&self.scrollbar_opacity); - - let managed = self.contents.mounted(&mut context.as_event_context()); - context.for_other(&managed).redraw(); - - let size = context.gfx.region().size.into_unsigned(); - - if self.horizontal_bar.amount_hidden > 0 { - context.gfx.draw_shape(&Shape::filled_rect( - Rect::new( - Point::new(self.horizontal_bar.offset, size.height - self.bar_width), - Size::new(self.horizontal_bar.size, self.bar_width), - ) - .into_signed(), // See https://github.com/khonsulabs/cushy/issues/186 - Color::new_f32(1.0, 1.0, 1.0, *self.scrollbar_opacity.get()), - )); + let contents = self.contents.mounted(&mut context.as_event_context()); + context.for_other(&contents).redraw(); + if self.enabled.x { + let horizontal = self + .horizontal_widget + .expect_made_mut() + .mounted(&mut context.as_event_context()); + context.for_other(&horizontal).redraw(); } - - if self.vertical_bar.amount_hidden > 0 { - context.gfx.draw_shape(&Shape::filled_rect( - Rect::new( - Point::new(size.width - self.bar_width, self.vertical_bar.offset), - Size::new(self.bar_width, self.vertical_bar.size), - ) - .into_signed(), // See https://github.com/khonsulabs/cushy/issues/186 - Color::new_f32(1.0, 1.0, 1.0, *self.scrollbar_opacity.get()), - )); + if self.enabled.y { + let vertical = self + .vertical_widget + .expect_made_mut() + .mounted(&mut context.as_event_context()); + context.for_other(&vertical).redraw(); } } @@ -242,13 +291,6 @@ impl Widget for Scroll { available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_>, ) -> Size { - self.bar_width = context - .get(&ScrollBarThickness) - .into_upx(context.gfx.scale()); - self.line_height = context.get(&LineHeight).into_upx(context.gfx.scale()); - - let (mut scroll, current_max_scroll) = self.constrain_scroll(); - let max_extents = Size::new( if self.enabled.x { ConstraintLimit::SizeToFit(UPx::MAX) @@ -261,8 +303,9 @@ impl Widget for Scroll { available_space.height }, ); - let managed = self.contents.mounted(&mut context.as_event_context()); - let new_content_size = context.for_other(&managed).layout(max_extents); + let contents = self.contents.mounted(&mut context.as_event_context()); + let new_content_size = context.for_other(&contents).layout(max_extents); + self.content_size.set(new_content_size); let new_control_size = Size::new( if self.enabled.x { @@ -277,60 +320,47 @@ impl Widget for Scroll { }, ); - self.horizontal_bar = - scrollbar_region(scroll.x, new_content_size.width, new_control_size.width); - let max_scroll_x = if self.enabled.x { - self.horizontal_bar.amount_hidden - } else { - UPx::ZERO - }; + let horizontal = self + .horizontal_widget + .make_if_needed() + .mounted(&mut context.as_event_context()); + let layout = context.for_other(&horizontal).layout(available_space); + context.set_child_layout( + &horizontal, + Rect::new( + Point::new( + Px::ZERO, + max_extents + .height + .fit_measured(new_control_size.height) + .saturating_sub(layout.height) + .into_signed(), + ), + layout.into_signed(), + ), + ); + let vertical = self + .vertical_widget + .make_if_needed() + .mounted(&mut context.as_event_context()); + let layout = context.for_other(&vertical).layout(available_space); + context.set_child_layout( + &vertical, + Rect::new( + Point::new( + max_extents + .width + .fit_measured(new_control_size.width) + .saturating_sub(layout.width) + .into_signed(), + Px::ZERO, + ), + layout.into_signed(), + ), + ); + let scroll = self.scroll.get_tracking_invalidate(context); - self.vertical_bar = - scrollbar_region(scroll.y, new_content_size.height, new_control_size.height); - let max_scroll_y = if self.enabled.y { - self.vertical_bar.amount_hidden - } else { - UPx::ZERO - }; - let new_max_scroll = Point::new(max_scroll_x, max_scroll_y); - if current_max_scroll != new_max_scroll { - self.max_scroll.set(new_max_scroll); - scroll = scroll.max(new_max_scroll); - } - - // This is not tracked on purpose - it's only ever changed in layout - let content_size = self.content_size.get(); - let control_size = self.control_size.get(); - - // Preserve the current scroll if the widget has resized - if content_size != Size::ZERO && content_size != new_content_size { - if (content_size.width != new_content_size.width - || control_size.width != new_control_size.width) - && scroll.x == current_max_scroll.x - && self.preserve_max_scroll.get() - { - scroll.x = max_scroll_x; - } - - if (content_size.height != new_content_size.height - || control_size.height != new_control_size.height) - && scroll.y == current_max_scroll.y - && self.preserve_max_scroll.get() - { - scroll.y = max_scroll_y; - } - } - - // Set the current scroll, but prevent immediately triggering - // invalidate. - { - let mut current_scroll = self.scroll.lock(); - current_scroll.prevent_notifications(); - *current_scroll = scroll; - } - context.invalidate_when_changed(&self.scroll); self.control_size.set(new_control_size); - self.content_size.set(new_content_size); let region = Rect::new( -scroll.into_signed(), @@ -338,7 +368,7 @@ impl Widget for Scroll { .min(Size::new(UPx::MAX, UPx::MAX) - scroll.max(Point::default())) .into_signed(), ); - context.set_child_layout(&managed, region); + context.set_child_layout(&contents, region); new_control_size } @@ -350,118 +380,28 @@ impl Widget for Scroll { _phase: TouchPhase, context: &mut EventContext<'_>, ) -> EventHandling { - let amount = match delta { - MouseScrollDelta::LineDelta(x, y) => Point::new(x, y) * self.line_height.into_float(), - MouseScrollDelta::PixelDelta(px) => Point::new(px.x.cast(), px.y.cast()), - }; - let mut scroll = self.scroll.lock(); - let old_scroll = *scroll; - let new_scroll = Self::constrained_scroll( - (scroll.into_signed() - amount.cast::()).into_unsigned(), - self.max_scroll.get(), - ); - if old_scroll == new_scroll { - IGNORED - } else { - *scroll = new_scroll; - drop(scroll); - + let mut handled = false; + { + let mut vertical = self.vertical_widget.expect_made().widget().lock(); + handled |= vertical + .downcast_mut::() + .expect("a ScrollBar") + .mouse_wheel(delta, context) + .is_break(); + let mut horizontal = self.horizontal_widget.expect_made().widget().lock(); + handled |= horizontal + .downcast_mut::() + .expect("a ScrollBar") + .mouse_wheel(delta, context) + .is_break(); + } + if handled { self.show_scrollbars(context); context.set_needs_redraw(); HANDLED - } - } - - fn mouse_down( - &mut self, - location: Point, - _device_id: DeviceId, - _button: kludgine::app::winit::event::MouseButton, - context: &mut EventContext<'_>, - ) -> EventHandling { - let control_size = self.control_size.get(); - - let relative_x = (control_size.width.into_signed() - location.x).into_unsigned(); - let in_vertical_area = self.enabled.y && relative_x <= self.bar_width; - - let relative_y = (control_size.height.into_signed() - location.y).into_unsigned(); - let in_horizontal_area = self.enabled.x && relative_y <= self.bar_width; - - if matches!( - (in_horizontal_area, in_vertical_area), - (true, true) | (false, false) - ) { - return IGNORED; - } - - self.drag.start = location.into_signed(); - self.drag.start_scroll = self.scroll.get(); - self.drag.horizontal = in_horizontal_area; - self.drag.in_bar = if in_horizontal_area { - let relative = location.x - self.horizontal_bar.offset.into_signed(); - relative >= 0 && relative < self.horizontal_bar.size } else { - let relative = location.y - self.vertical_bar.offset.into_signed(); - relative >= 0 && relative < self.vertical_bar.size - }; - - // If we clicked in the open area, we need to jump to the new location - // immediately. - if !self.drag.in_bar { - self.drag.update( - location, - &self.scroll, - &self.horizontal_bar, - &self.vertical_bar, - self.max_scroll.get(), - control_size, - ); - } - - self.drag.mouse_buttons_down += 1; - self.show_scrollbars(context); - - HANDLED - } - - fn mouse_drag( - &mut self, - location: Point, - _device_id: DeviceId, - _button: kludgine::app::winit::event::MouseButton, - _context: &mut EventContext<'_>, - ) { - self.drag.update( - location, - &self.scroll, - &self.horizontal_bar, - &self.vertical_bar, - self.max_scroll.get(), - self.control_size.get(), - ); - } - - fn mouse_up( - &mut self, - location: Option>, - _device_id: DeviceId, - _button: kludgine::app::winit::event::MouseButton, - context: &mut EventContext<'_>, - ) { - self.drag.mouse_buttons_down -= 1; - - if self.drag.mouse_buttons_down == 0 { - if location.map_or(false, |location| { - Rect::from(self.control_size.get()) - .into_signed() - .contains(location) - }) { - self.scrollbar_opacity_animation.handle.clear(); - self.show_scrollbars(context); - } else { - self.hide_scrollbars(context); - } + IGNORED } } @@ -476,42 +416,29 @@ impl Widget for Scroll { #[derive(Default, Debug)] struct DragInfo { mouse_buttons_down: usize, - start: Point, - start_scroll: Point, - horizontal: bool, + start: Px, + start_scroll: UPx, in_bar: bool, } impl DragInfo { fn update( &self, - location: Point, - dynamic_scroll: &Dynamic>, - horizontal_bar: &ScrollbarInfo, - vertical_bar: &ScrollbarInfo, - max_scroll: Point, - control_size: Size, + location: Px, + dynamic_scroll: &Dynamic, + info: &ScrollbarInfo, + max_scroll: UPx, + control_size: UPx, ) { - let mut scroll = dynamic_scroll.get(); - if self.horizontal { - scroll.x = self.update_bar( - location.x, - self.start.x, - max_scroll.x, - self.start_scroll.x, - horizontal_bar, - control_size.width, - ); - } else { - scroll.y = self.update_bar( - location.y, - self.start.y, - max_scroll.y, - self.start_scroll.y, - vertical_bar, - control_size.height, - ); - } + let scroll = self.update_bar( + location, + self.start, + max_scroll, + self.start_scroll, + info, + control_size, + ); + dynamic_scroll.set(scroll); } @@ -585,3 +512,354 @@ define_components! { ScrollBarThickness(Dimension, "size", Dimension::Lp(Lp::points(7))) } } + +/// A draggable bar that is used to scroll a region. +#[derive(Debug)] +pub struct ScrollBar { + content_size: Dynamic, + last_content_size: UPx, + scroll: Dynamic, + preserve_max_scroll: Value, + max_scroll: Dynamic, + bar_width: UPx, + control_size: UPx, + line_height: UPx, + vertical: bool, + info: ScrollbarInfo, + scrollbar_opacity: Dynamic, + scrollbar_opacity_animation: Dynamic, + drag: DragInfo, +} + +impl ScrollBar { + /// Creates a new scroll bar that updates `scroll_by` to scroll across + /// `content_size`. + /// + /// If `vertical` this bar will draw a bar from the top of the widget to the + /// bottom of the widget. Otherwise, the bar will be drawn from the left to + /// the right of the widget. + pub fn new( + content_size: impl IntoDynamic, + scroll_by: impl IntoDynamic, + vertical: bool, + ) -> Self { + Self { + content_size: content_size.into_dynamic(), + scroll: scroll_by.into_dynamic(), + preserve_max_scroll: Value::Constant(true), + max_scroll: Dynamic::new(UPx::ZERO), + bar_width: UPx::ZERO, + line_height: UPx::ZERO, + control_size: UPx::ZERO, + vertical, + info: ScrollbarInfo::default(), + scrollbar_opacity: Dynamic::default(), + scrollbar_opacity_animation: Dynamic::new(OpacityAnimationState { + handle: AnimationHandle::new(), + will_hide: true, + is_hide: true, + hovering: Set::new(), + }), + drag: DragInfo::default(), + last_content_size: UPx::ZERO, + } + } + + /// Sets whether the scroll view will stay scrolled to the maximum when a + /// child is resized. + /// + /// When enabled, this setting allows the scroll view to remain scrolled to + /// the bottom or to the right when its contents grow. The default value for + /// this setting is `true`. + #[must_use] + pub fn preserve_max_scroll(mut self, preserve_max_scroll: impl IntoValue) -> Self { + self.preserve_max_scroll = preserve_max_scroll.into_value(); + self + } + + /// Returns a reader for the maximum scroll value. + /// + /// This represents the maximum amount that the scroll can be moved by. + #[must_use] + pub fn max_scroll(&self) -> DynamicReader { + self.max_scroll.create_reader() + } + + /// Applies the delta from a mouse wheel event. + /// + /// Returns whether the event resulted in any scroll. + #[must_use] + pub fn mouse_wheel( + &mut self, + delta: MouseScrollDelta, + context: &mut EventContext<'_>, + ) -> EventHandling { + let amount = match delta { + MouseScrollDelta::LineDelta(x, y) => Point::new(x, y) * self.line_height.into_float(), + MouseScrollDelta::PixelDelta(px) => Point::new(px.x.cast(), px.y.cast()), + }; + let amount = if self.vertical { amount.y } else { amount.x }; + + let mut scroll = self.scroll.lock(); + let old_scroll = *scroll; + let new_scroll = Self::constrained_scroll( + (scroll.into_signed() - amount.cast::()).into_unsigned(), + self.max_scroll.get(), + ); + if old_scroll == new_scroll { + IGNORED + } else { + *scroll = new_scroll; + drop(scroll); + self.show(context); + HANDLED + } + } + + fn constrained_scroll(scroll: UPx, max_scroll: UPx) -> UPx { + scroll.min(max_scroll) + } + + /// Updates this scroll bar to synchronize its visibility with `other`. + pub fn synchronize_visibility_with(&mut self, other: &ScrollBar) { + self.scrollbar_opacity = other.scrollbar_opacity.clone(); + self.scrollbar_opacity_animation = other.scrollbar_opacity_animation.clone(); + } + + /// Shows this scroll bar, automatically hiding after a short delay. + pub fn show(&mut self, context: &mut EventContext<'_>) { + let mut animation_state = self.scrollbar_opacity_animation.lock(); + let should_hide = self.drag.mouse_buttons_down == 0 && animation_state.hovering.is_empty(); + if animation_state.is_hide + || should_hide != animation_state.will_hide + || animation_state.handle.is_complete() + { + let current_opacity = self.scrollbar_opacity.get(); + let transition_time = *current_opacity.one_minus() / 4.; + let animation = self + .scrollbar_opacity + .transition_to(ZeroToOne::ONE) + .over(Duration::from_secs_f32(transition_time)) + .with_easing(context.get(&EasingIn)); + + animation_state.is_hide = false; + animation_state.will_hide = should_hide; + animation_state.handle = if should_hide { + animation + .and_then(Duration::from_secs(1)) + .and_then( + self.scrollbar_opacity + .transition_to(ZeroToOne::ZERO) + .over(Duration::from_millis(300)) + .with_easing(context.get(&EasingOut)), + ) + .spawn() + } else { + animation.spawn() + }; + } + } + + /// Hides the scroll bar, if it can be hidden. + pub fn hide(&mut self, context: &mut EventContext<'_>) { + let mut animation_state = self.scrollbar_opacity_animation.lock(); + if self.drag.mouse_buttons_down == 0 + && !animation_state.will_hide + && animation_state.hovering.is_empty() + { + animation_state.is_hide = true; + animation_state.will_hide = true; + animation_state.handle = self + .scrollbar_opacity + .transition_to(ZeroToOne::ZERO) + .over(Duration::from_millis(300)) + .with_easing(context.get(&EasingOut)) + .spawn(); + } + } +} + +impl Widget for ScrollBar { + fn redraw(&mut self, context: &mut crate::context::GraphicsContext<'_, '_, '_, '_>) { + let scroll = self.scroll.get_tracking_redraw(context); + let content_size = self.content_size.get_tracking_redraw(context); + let control_size = context.gfx.region().size.into_unsigned(); + let scrolled_to_end = scroll == self.info.amount_hidden; + + self.control_size = if self.vertical { + control_size.height + } else { + control_size.width + }; + self.info = scrollbar_region(scroll, content_size, self.control_size); + let mut constrained = Self::constrained_scroll(scroll, self.info.amount_hidden); + + // Preserve the current scroll if the widget has resized + if scrolled_to_end + && self.last_content_size != 0 + && self.last_content_size != content_size + && self.preserve_max_scroll.get() + { + constrained = self.info.amount_hidden; + } + self.last_content_size = content_size; + self.scroll.set(constrained); + self.max_scroll.set(self.info.amount_hidden); + + let opacity = self.scrollbar_opacity.get_tracking_redraw(context); + if context.enabled() && self.info.amount_hidden > 0 && opacity > 0. { + let rect = if self.vertical { + Rect::new( + Point::new(control_size.width - self.bar_width, self.info.offset), + Size::new(self.bar_width, self.info.size), + ) + } else { + Rect::new( + Point::new(UPx::ZERO, control_size.height - self.bar_width), + Size::new(self.info.size, self.bar_width), + ) + }; + context.gfx.draw_shape(&Shape::filled_rect( + rect.into_signed(), // See https://github.com/khonsulabs/cushy/issues/186 + Color::new_f32(1.0, 1.0, 1.0, *opacity), + )); + } + } + + fn hit_test(&mut self, _location: Point, context: &mut EventContext<'_>) -> bool { + self.max_scroll.get() > 0 && context.enabled() + } + + fn hover( + &mut self, + _location: Point, + context: &mut EventContext<'_>, + ) -> Option { + self.scrollbar_opacity_animation + .lock() + .hovering + .insert(context.widget().id()); + self.show(context); + + None + } + + fn unhover(&mut self, context: &mut EventContext<'_>) { + self.scrollbar_opacity_animation + .lock() + .hovering + .remove(&context.widget().id()); + self.hide(context); + } + + fn layout( + &mut self, + available_space: Size, + context: &mut LayoutContext<'_, '_, '_, '_>, + ) -> Size { + self.bar_width = context + .get(&ScrollBarThickness) + .into_upx(context.gfx.scale()); + self.line_height = context.get(&LineHeight).into_upx(context.gfx.scale()); + + if self.vertical { + Size::new(self.bar_width, available_space.height.max()) + } else { + Size::new(available_space.width.max(), self.bar_width) + } + } + + fn mouse_wheel( + &mut self, + _device_id: DeviceId, + delta: MouseScrollDelta, + _phase: TouchPhase, + context: &mut EventContext<'_>, + ) -> EventHandling { + self.mouse_wheel(delta, context) + } + + fn mouse_down( + &mut self, + location: Point, + _device_id: DeviceId, + _button: kludgine::app::winit::event::MouseButton, + context: &mut EventContext<'_>, + ) -> EventHandling { + if self.max_scroll.get().is_zero() || !context.enabled() { + return IGNORED; + } + self.drag.start = if self.vertical { + location.y + } else { + location.x + }; + self.drag.start_scroll = self.scroll.get(); + let relative = self.drag.start - self.info.offset.into_signed(); + self.drag.in_bar = relative >= 0 && relative < self.info.size; + + // If we clicked in the open area, we need to jump to the new location + // immediately. + if !self.drag.in_bar { + self.drag.update( + self.drag.start, + &self.scroll, + &self.info, + self.max_scroll.get(), + self.control_size, + ); + } + + self.drag.mouse_buttons_down += 1; + self.show(context); + + HANDLED + } + + fn mouse_drag( + &mut self, + location: Point, + _device_id: DeviceId, + _button: kludgine::app::winit::event::MouseButton, + _context: &mut EventContext<'_>, + ) { + let offset = if self.vertical { + location.y + } else { + location.x + }; + self.drag.update( + offset, + &self.scroll, + &self.info, + self.max_scroll.get(), + self.control_size, + ); + } + + fn mouse_up( + &mut self, + location: Option>, + _device_id: DeviceId, + _button: kludgine::app::winit::event::MouseButton, + context: &mut EventContext<'_>, + ) { + self.drag.mouse_buttons_down -= 1; + + if self.drag.mouse_buttons_down == 0 { + if location.map_or(false, |location| { + let offset = if self.vertical { + location.y + } else { + location.x + }; + offset >= 0 && offset < self.control_size + }) { + self.scrollbar_opacity_animation.lock().handle.clear(); + self.show(context); + } else { + self.hide(context); + } + } + } +}