mirror of
https://github.com/danbulant/cushy
synced 2026-06-10 18:13:48 +00:00
Ranged sliders, advance_focus, allow_blur
Closes #60 Stepping in sliders is a compromise due to the flexibility of the current slider implementation. I don't want to force types to implement Add, and I don't like forcing types to require a Step (ie, what's the appropriate value for f32 to specify as its next value?). Using a percentage combined with lerp keeps the implementation fairly straightfoward, although I remember experiencing this type of configuration in another UI framework a long time ago and thinking it was a little annoying to work with. Ultimately, setting actual step boundaries can be done by customizing the type that the slider is operating over. I feel like that's a much more powerful design than I've experienced in previous frameworks, so I'm hoping this percent step behavior is a reasonable compromise.
This commit is contained in:
parent
8f2ff1b5dc
commit
2201f2c83b
8 changed files with 769 additions and 176 deletions
87
Cargo.lock
generated
87
Cargo.lock
generated
|
|
@ -106,9 +106,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "arboard"
|
||||
version = "3.2.1"
|
||||
version = "3.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac57f2b058a76363e357c056e4f74f1945bf734d37b8b3ef49066c4787dde0fc"
|
||||
checksum = "aafb29b107435aa276664c1db8954ac27a6e105cdad3c88287a199eb0e313c08"
|
||||
dependencies = [
|
||||
"clipboard-win",
|
||||
"core-graphics 0.22.3",
|
||||
|
|
@ -120,7 +120,7 @@ dependencies = [
|
|||
"parking_lot",
|
||||
"thiserror",
|
||||
"winapi",
|
||||
"x11rb 0.10.1",
|
||||
"x11rb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -757,16 +757,6 @@ version = "0.3.29"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817"
|
||||
|
||||
[[package]]
|
||||
name = "gethostname"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gethostname"
|
||||
version = "0.3.0"
|
||||
|
|
@ -1086,7 +1076,7 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
|
|||
[[package]]
|
||||
name = "kludgine"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/khonsulabs/kludgine#267c42d9454b31ebfe944e49522da33811e93772"
|
||||
source = "git+https://github.com/khonsulabs/kludgine#138997a46158d96509c466751d95d80988c6d7bd"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"alot",
|
||||
|
|
@ -1301,15 +1291,6 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.7.1"
|
||||
|
|
@ -1410,18 +1391,6 @@ dependencies = [
|
|||
"jni-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.24.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"memoffset 0.6.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.26.4"
|
||||
|
|
@ -1431,7 +1400,7 @@ dependencies = [
|
|||
"bitflags 1.3.2",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"memoffset 0.7.1",
|
||||
"memoffset",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2021,18 +1990,18 @@ checksum = "e388332cd64eb80cd595a00941baf513caffae8dce9cfd0467fc9c66397dade6"
|
|||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.192"
|
||||
version = "1.0.193"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001"
|
||||
checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.192"
|
||||
version = "1.0.193"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1"
|
||||
checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
@ -2508,7 +2477,7 @@ checksum = "19152ddd73f45f024ed4534d9ca2594e0ef252c1847695255dae47f34df9fbe4"
|
|||
dependencies = [
|
||||
"cc",
|
||||
"downcast-rs",
|
||||
"nix 0.26.4",
|
||||
"nix",
|
||||
"scoped-tls",
|
||||
"smallvec",
|
||||
"wayland-sys",
|
||||
|
|
@ -2521,7 +2490,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "1ca7d52347346f5473bf2f56705f360e8440873052e575e55890c4fa57843ed3"
|
||||
dependencies = [
|
||||
"bitflags 2.4.1",
|
||||
"nix 0.26.4",
|
||||
"nix",
|
||||
"wayland-backend",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
|
@ -2543,7 +2512,7 @@ version = "0.31.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a44aa20ae986659d6c77d64d808a046996a932aa763913864dc40c359ef7ad5b"
|
||||
dependencies = [
|
||||
"nix 0.26.4",
|
||||
"nix",
|
||||
"wayland-client",
|
||||
"xcursor",
|
||||
]
|
||||
|
|
@ -2977,7 +2946,7 @@ dependencies = [
|
|||
"web-time",
|
||||
"windows-sys 0.48.0",
|
||||
"x11-dl",
|
||||
"x11rb 0.12.0",
|
||||
"x11rb",
|
||||
"xkbcommon-dl",
|
||||
]
|
||||
|
||||
|
|
@ -3001,19 +2970,6 @@ dependencies = [
|
|||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "x11rb"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "592b4883219f345e712b3209c62654ebda0bb50887f330cbd018d0f654bfd507"
|
||||
dependencies = [
|
||||
"gethostname 0.2.3",
|
||||
"nix 0.24.3",
|
||||
"winapi",
|
||||
"winapi-wsapoll",
|
||||
"x11rb-protocol 0.10.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "x11rb"
|
||||
version = "0.12.0"
|
||||
|
|
@ -3021,23 +2977,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "b1641b26d4dec61337c35a1b1aaf9e3cba8f46f0b43636c609ab0291a648040a"
|
||||
dependencies = [
|
||||
"as-raw-xcb-connection",
|
||||
"gethostname 0.3.0",
|
||||
"gethostname",
|
||||
"libc",
|
||||
"libloading 0.7.4",
|
||||
"nix 0.26.4",
|
||||
"nix",
|
||||
"once_cell",
|
||||
"winapi",
|
||||
"winapi-wsapoll",
|
||||
"x11rb-protocol 0.12.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "x11rb-protocol"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56b245751c0ac9db0e006dc812031482784e434630205a93c73cfefcaabeac67"
|
||||
dependencies = [
|
||||
"nix 0.24.3",
|
||||
"x11rb-protocol",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3046,7 +2993,7 @@ version = "0.12.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82d6c3f9a0fb6701fab8f6cea9b0c0bd5d6876f1f89f7fada07e558077c344bc"
|
||||
dependencies = [
|
||||
"nix 0.26.4",
|
||||
"nix",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
35
examples/focus.rs
Normal file
35
examples/focus.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
use gooey::value::Dynamic;
|
||||
use gooey::widget::MakeWidget;
|
||||
use gooey::widgets::input::InputValue;
|
||||
use gooey::widgets::slider::Slidable;
|
||||
use gooey::widgets::{Checkbox, Custom};
|
||||
use gooey::Run;
|
||||
use kludgine::figures::units::Lp;
|
||||
|
||||
fn main() -> gooey::Result {
|
||||
let allow_blur = Dynamic::new(true);
|
||||
"Input Field"
|
||||
.and(Dynamic::<String>::default().into_input())
|
||||
.and("Range Slider")
|
||||
.and(Dynamic::<u8>::default().slider_between(0_u8, 100_u8))
|
||||
.and("Range Slider")
|
||||
.and(Dynamic::new(10..=30).slider_between(0_u8, 100_u8))
|
||||
.and(Checkbox::new(
|
||||
allow_blur.clone(),
|
||||
"Allow Custom Widget to Lose Focus",
|
||||
))
|
||||
.and(
|
||||
Custom::empty()
|
||||
.on_accept_focus(|_| true)
|
||||
.on_redraw(|context| {
|
||||
context.fill(context.theme().secondary.color);
|
||||
if context.focused() {
|
||||
context.draw_focus_ring();
|
||||
}
|
||||
})
|
||||
.on_allow_blur(move |_| allow_blur.get())
|
||||
.height(Lp::inches(1)),
|
||||
)
|
||||
.into_rows()
|
||||
.run()
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
use gooey::animation::{LinearInterpolate, PercentBetween};
|
||||
use gooey::value::Dynamic;
|
||||
use gooey::value::{Dynamic, ForEach};
|
||||
use gooey::widget::MakeWidget;
|
||||
use gooey::widgets::input::InputValue;
|
||||
use gooey::widgets::slider::Slidable;
|
||||
|
|
@ -9,9 +9,11 @@ use kludgine::figures::Ranged;
|
|||
|
||||
fn main() -> gooey::Result {
|
||||
u8_slider()
|
||||
.and(u8_range_slider())
|
||||
.and(enum_slider())
|
||||
.into_rows()
|
||||
.expand_horizontally()
|
||||
.contain()
|
||||
.width(..Lp::points(800))
|
||||
.centered()
|
||||
.expand()
|
||||
|
|
@ -19,10 +21,10 @@ fn main() -> gooey::Result {
|
|||
}
|
||||
|
||||
fn u8_slider() -> impl MakeWidget {
|
||||
let min_text = Dynamic::new(u8::MIN.to_string());
|
||||
let min = min_text.map_each(|min| min.parse().unwrap_or(u8::MIN));
|
||||
let max_text = Dynamic::new(u8::MAX.to_string());
|
||||
let max = max_text.map_each(|max| max.parse().unwrap_or(u8::MAX));
|
||||
let min = Dynamic::new(u8::MIN);
|
||||
let min_text = min.linked_string();
|
||||
let max = Dynamic::new(u8::MAX);
|
||||
let max_text = max.linked_string();
|
||||
let value = Dynamic::new(128_u8);
|
||||
let value_text = value.map_each(ToString::to_string);
|
||||
|
||||
|
|
@ -37,6 +39,40 @@ fn u8_slider() -> impl MakeWidget {
|
|||
.into_rows()
|
||||
}
|
||||
|
||||
fn u8_range_slider() -> impl MakeWidget {
|
||||
let range = Dynamic::new(42..=127);
|
||||
let start = range.map_each_unique(|range| *range.start());
|
||||
let end = range.map_each_unique(|range| *range.end());
|
||||
(&start, &end).for_each({
|
||||
let range = range.clone();
|
||||
move |(start, end)| {
|
||||
let _result = range.try_update(*start..=*end);
|
||||
}
|
||||
});
|
||||
|
||||
let min = Dynamic::new(u8::MIN);
|
||||
let min_text = min.linked_string();
|
||||
let start_text = start.linked_string();
|
||||
let end_text = end.linked_string();
|
||||
let max = Dynamic::new(u8::MAX);
|
||||
let max_text = max.linked_string();
|
||||
let value_text = range.map_each(|r| format!("{}..={}", r.start(), r.end()));
|
||||
|
||||
"Min"
|
||||
.and(min_text.into_input())
|
||||
.and("Start")
|
||||
.and(start_text.into_input())
|
||||
.and("End")
|
||||
.and(end_text.into_input())
|
||||
.and("Max")
|
||||
.and(max_text.into_input())
|
||||
.into_columns()
|
||||
.centered()
|
||||
.and(range.slider_between(min, max))
|
||||
.and(value_text.centered())
|
||||
.into_rows()
|
||||
}
|
||||
|
||||
#[derive(LinearInterpolate, Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
|
||||
enum SlidableEnum {
|
||||
A,
|
||||
|
|
|
|||
|
|
@ -178,6 +178,7 @@ impl<'context, 'window> EventContext<'context, 'window> {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)] // TODO
|
||||
pub(crate) fn apply_pending_state(&mut self) {
|
||||
const MAX_ITERS: u8 = 100;
|
||||
// These two blocks apply active/focus in a loop to pick up the event
|
||||
|
|
@ -262,9 +263,18 @@ impl<'context, 'window> EventContext<'context, 'window> {
|
|||
});
|
||||
let new = match self.current_node.tree.focus(self.pending_state.focus) {
|
||||
Ok(old) => {
|
||||
if let Some(old) = old {
|
||||
let mut old_context = self.for_other(&old);
|
||||
old.lock().as_widget().blur(&mut old_context);
|
||||
if let Some(old_widget) = old {
|
||||
let mut old_context = self.for_other(&old_widget);
|
||||
let mut old = old_widget.lock();
|
||||
if old.as_widget().allow_blur(&mut old_context) {
|
||||
old.as_widget().blur(&mut old_context);
|
||||
} else {
|
||||
// This widget is rejecting the focus change.
|
||||
drop(old_context);
|
||||
let _result = self.current_node.tree.focus(Some(old_widget.id()));
|
||||
self.pending_state.focus = Some(old_widget.id());
|
||||
break;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
|
@ -417,6 +427,20 @@ impl<'context, 'window> EventContext<'context, 'window> {
|
|||
}
|
||||
|
||||
fn move_focus(&mut self, advance: bool) {
|
||||
let node = self.current_node.clone();
|
||||
let mut direction = self.get(&LayoutOrder);
|
||||
if !advance {
|
||||
direction = direction.rev();
|
||||
}
|
||||
if node
|
||||
.lock()
|
||||
.as_widget()
|
||||
.advance_focus(direction, self)
|
||||
.is_break()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(explicit_next_focus) = self.current_node.explicit_focus_target(advance) {
|
||||
self.for_other(&explicit_next_focus).focus();
|
||||
} else {
|
||||
|
|
@ -951,6 +975,14 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns true if the last focus event was an advancing motion, not a
|
||||
/// returning motion.
|
||||
///
|
||||
/// This value is meaningless outside of focus-related events.
|
||||
pub fn focus_is_advancing(&mut self) -> bool {
|
||||
self.pending_state.focus_is_advancing
|
||||
}
|
||||
|
||||
/// Activates this widget, if it is not already active.
|
||||
///
|
||||
/// Returns true if this function resulted in the currently active widget
|
||||
|
|
|
|||
|
|
@ -51,12 +51,6 @@ pub trait Widget: Send + UnwindSafe + Debug + 'static {
|
|||
available_space.map(ConstraintLimit::min)
|
||||
}
|
||||
|
||||
/// Return true if this widget should expand to fill the window when it is
|
||||
/// the root widget.
|
||||
fn expand_if_at_root(&self) -> Option<bool> {
|
||||
Some(false)
|
||||
}
|
||||
|
||||
/// The widget has been mounted into a parent widget.
|
||||
#[allow(unused_variables)]
|
||||
fn mounted(&mut self, context: &mut EventContext<'_, '_>) {}
|
||||
|
|
@ -91,6 +85,25 @@ pub trait Widget: Send + UnwindSafe + Debug + 'static {
|
|||
#[allow(unused_variables)]
|
||||
fn focus(&mut self, context: &mut EventContext<'_, '_>) {}
|
||||
|
||||
/// The widget should switch to the next focusable area within this widget,
|
||||
/// honoring `direction` in a consistent manner. Returning `HANDLED` will
|
||||
/// cause the search for the next focus widget stop.
|
||||
#[allow(unused_variables)]
|
||||
fn advance_focus(
|
||||
&mut self,
|
||||
direction: VisualOrder,
|
||||
context: &mut EventContext<'_, '_>,
|
||||
) -> EventHandling {
|
||||
IGNORED
|
||||
}
|
||||
|
||||
/// The widget is about to lose focus. Returning true allows the focus to
|
||||
/// switch away from this widget.
|
||||
#[allow(unused_variables)]
|
||||
fn allow_blur(&mut self, context: &mut EventContext<'_, '_>) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// The widget is no longer focused for user input.
|
||||
#[allow(unused_variables)]
|
||||
fn blur(&mut self, context: &mut EventContext<'_, '_>) {}
|
||||
|
|
@ -333,10 +346,29 @@ pub trait WrapperWidget: Debug + Send + UnwindSafe + 'static {
|
|||
false
|
||||
}
|
||||
|
||||
/// The widget should switch to the next focusable area within this widget,
|
||||
/// honoring `direction` in a consistent manner. Returning `HANDLED` will
|
||||
/// cause the search for the next focus widget stop.
|
||||
#[allow(unused_variables)]
|
||||
fn advance_focus(
|
||||
&mut self,
|
||||
direction: VisualOrder,
|
||||
context: &mut EventContext<'_, '_>,
|
||||
) -> EventHandling {
|
||||
IGNORED
|
||||
}
|
||||
|
||||
/// The widget has received focus for user input.
|
||||
#[allow(unused_variables)]
|
||||
fn focus(&mut self, context: &mut EventContext<'_, '_>) {}
|
||||
|
||||
/// The widget is about to lose focus. Returning true allows the focus to
|
||||
/// switch away from this widget.
|
||||
#[allow(unused_variables)]
|
||||
fn allow_blur(&mut self, context: &mut EventContext<'_, '_>) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// The widget is no longer focused for user input.
|
||||
#[allow(unused_variables)]
|
||||
fn blur(&mut self, context: &mut EventContext<'_, '_>) {}
|
||||
|
|
@ -548,6 +580,18 @@ where
|
|||
) -> EventHandling {
|
||||
T::mouse_wheel(self, device_id, delta, phase, context)
|
||||
}
|
||||
|
||||
fn advance_focus(
|
||||
&mut self,
|
||||
direction: VisualOrder,
|
||||
context: &mut EventContext<'_, '_>,
|
||||
) -> EventHandling {
|
||||
T::advance_focus(self, direction, context)
|
||||
}
|
||||
|
||||
fn allow_blur(&mut self, context: &mut EventContext<'_, '_>) -> bool {
|
||||
T::allow_blur(self, context)
|
||||
}
|
||||
}
|
||||
|
||||
/// A type that can create a [`WidgetInstance`].
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ use kludgine::figures::{Point, Size};
|
|||
use kludgine::Color;
|
||||
|
||||
use crate::context::{EventContext, GraphicsContext, LayoutContext, WidgetContext};
|
||||
use crate::styles::VisualOrder;
|
||||
use crate::value::{IntoValue, Value};
|
||||
use crate::widget::{EventHandling, MakeWidget, WidgetRef, WrappedLayout, WrapperWidget, IGNORED};
|
||||
use crate::widgets::Space;
|
||||
|
|
@ -44,6 +45,8 @@ pub struct Custom {
|
|||
keyboard_input: Option<Box<dyn ThreeParamEventFunc<DeviceId, KeyEvent, bool, EventHandling>>>,
|
||||
mouse_wheel:
|
||||
Option<Box<dyn ThreeParamEventFunc<DeviceId, MouseScrollDelta, TouchPhase, EventHandling>>>,
|
||||
allow_blur: Option<Box<dyn EventFunc<bool>>>,
|
||||
advance_focus: Option<Box<dyn OneParamEventFunc<VisualOrder, EventHandling>>>,
|
||||
}
|
||||
|
||||
impl Debug for Custom {
|
||||
|
|
@ -91,6 +94,8 @@ impl Custom {
|
|||
ime: None,
|
||||
keyboard_input: None,
|
||||
mouse_wheel: None,
|
||||
allow_blur: None,
|
||||
advance_focus: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -262,6 +267,42 @@ impl Custom {
|
|||
self
|
||||
}
|
||||
|
||||
/// Invokes `allow_blur` when this widget is about to lose focus. If
|
||||
/// `allow_blur` returns false, focus will be disallowed from leaving this
|
||||
/// widget.
|
||||
///
|
||||
/// This callback corresponds to [`WrapperWidget::allow_blur`].
|
||||
pub fn on_allow_blur<AllowBlur>(mut self, allow_blur: AllowBlur) -> Self
|
||||
where
|
||||
AllowBlur: Send
|
||||
+ UnwindSafe
|
||||
+ 'static
|
||||
+ for<'context, 'window> FnMut(&mut EventContext<'context, 'window>) -> bool,
|
||||
{
|
||||
self.allow_blur = Some(Box::new(allow_blur));
|
||||
self
|
||||
}
|
||||
|
||||
/// Invokes `advance_focus` when this widget has focus and focus is
|
||||
/// requested to advance to another widget. Returning
|
||||
/// [`HANDLED`](crate::widget::HANDLED) will signal to Gooey that the focus
|
||||
/// has moved and the request should not be processed any further.
|
||||
///
|
||||
/// This callback corresponds to [`WrapperWidget::advance_focus`].
|
||||
pub fn on_advance_focus<AdvanceFocus>(mut self, advance_focus: AdvanceFocus) -> Self
|
||||
where
|
||||
AdvanceFocus: Send
|
||||
+ UnwindSafe
|
||||
+ 'static
|
||||
+ for<'context, 'window> FnMut(
|
||||
VisualOrder,
|
||||
&mut EventContext<'context, 'window>,
|
||||
) -> EventHandling,
|
||||
{
|
||||
self.advance_focus = Some(Box::new(advance_focus));
|
||||
self
|
||||
}
|
||||
|
||||
/// Invokes `adjust_child_constraints` before measuring the child widget.
|
||||
/// The returned constraints will be passed along to the child in its layout
|
||||
/// function.
|
||||
|
|
@ -639,6 +680,26 @@ impl WrapperWidget for Custom {
|
|||
IGNORED
|
||||
}
|
||||
}
|
||||
|
||||
fn advance_focus(
|
||||
&mut self,
|
||||
direction: VisualOrder,
|
||||
context: &mut EventContext<'_, '_>,
|
||||
) -> EventHandling {
|
||||
if let Some(advance_focus) = &mut self.advance_focus {
|
||||
advance_focus.invoke(direction, context)
|
||||
} else {
|
||||
IGNORED
|
||||
}
|
||||
}
|
||||
|
||||
fn allow_blur(&mut self, context: &mut EventContext<'_, '_>) -> bool {
|
||||
if let Some(allow_blur) = &mut self.allow_blur {
|
||||
allow_blur.invoke(context)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait RedrawFunc: Send + UnwindSafe {
|
||||
|
|
|
|||
|
|
@ -1,140 +1,273 @@
|
|||
//! A widget that allows a user to "slide" between values.
|
||||
use std::fmt::Debug;
|
||||
use std::mem;
|
||||
use std::ops::RangeInclusive;
|
||||
use std::panic::UnwindSafe;
|
||||
|
||||
use kludgine::app::winit::event::{DeviceId, MouseButton};
|
||||
use intentional::{Assert, Cast as _};
|
||||
use kludgine::app::winit::event::{DeviceId, MouseButton, MouseScrollDelta, TouchPhase};
|
||||
use kludgine::app::winit::keyboard::{Key, NamedKey};
|
||||
use kludgine::figures::units::{Lp, Px, UPx};
|
||||
use kludgine::figures::{
|
||||
FloatConversion, FromComponents, IntoComponents, IntoSigned, Point, Ranged, Rect, ScreenScale,
|
||||
Size,
|
||||
FloatConversion, FromComponents, IntoComponents, IntoSigned, Point, Ranged, Rect, Round,
|
||||
ScreenScale, Size,
|
||||
};
|
||||
use kludgine::shapes::Shape;
|
||||
use kludgine::shapes::{Shape, StrokeOptions};
|
||||
use kludgine::{Color, DrawableExt, Origin};
|
||||
|
||||
use crate::animation::{LinearInterpolate, PercentBetween};
|
||||
use crate::animation::{LinearInterpolate, PercentBetween, ZeroToOne};
|
||||
use crate::context::{EventContext, GraphicsContext, LayoutContext};
|
||||
use crate::styles::components::{OpaqueWidgetColor, WidgetAccentColor};
|
||||
use crate::styles::Dimension;
|
||||
use crate::styles::components::{
|
||||
AutoFocusableControls, OpaqueWidgetColor, OutlineColor, WidgetAccentColor,
|
||||
};
|
||||
use crate::styles::{Dimension, HorizontalOrder, VerticalOrder, VisualOrder};
|
||||
use crate::value::{Dynamic, IntoDynamic, IntoValue, Value};
|
||||
use crate::widget::{EventHandling, Widget, HANDLED};
|
||||
use crate::widget::{EventHandling, Widget, HANDLED, IGNORED};
|
||||
use crate::ConstraintLimit;
|
||||
|
||||
/// A widget that allows sliding between two values.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Slider<T> {
|
||||
pub struct Slider<T>
|
||||
where
|
||||
T: SliderValue,
|
||||
{
|
||||
/// The current value.
|
||||
pub value: Dynamic<T>,
|
||||
/// The minimum value represented by this slider.
|
||||
pub minimum: Value<T>,
|
||||
pub minimum: Value<T::Value>,
|
||||
/// The maximum value represented by this slider.
|
||||
pub maximum: Value<T>,
|
||||
pub maximum: Value<T::Value>,
|
||||
/// The percentage to step when advancing the slider using alternative
|
||||
/// inputs (e.g, keyboard/mousewheel).
|
||||
///
|
||||
/// The widget will use this as a starting value, but will continue to step
|
||||
/// by this amount until a new unique value is obtained from linear
|
||||
/// interpolation.
|
||||
///
|
||||
/// This defaults to `0.05`/5%.
|
||||
pub step: Value<ZeroToOne>,
|
||||
knob_size: UPx,
|
||||
horizontal: bool,
|
||||
rendered_size: Px,
|
||||
focused_knob: Option<Knob>,
|
||||
previous_focus: Option<Knob>,
|
||||
mouse_buttons_down: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
enum Knob {
|
||||
Start,
|
||||
End,
|
||||
}
|
||||
|
||||
impl<T> Slider<T>
|
||||
where
|
||||
T: Ranged,
|
||||
T: SliderValue,
|
||||
T::Value: Ranged,
|
||||
{
|
||||
/// Returns a new slider over `value` using the types full range.
|
||||
#[must_use]
|
||||
pub fn from_value(value: impl IntoDynamic<T>) -> Self {
|
||||
Self::new(value, T::MIN, T::MAX)
|
||||
Self::new(value, <T::Value>::MIN, <T::Value>::MAX)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Slider<T> {
|
||||
impl<T> Slider<T>
|
||||
where
|
||||
T: SliderValue,
|
||||
{
|
||||
/// Returns a new slider using `value` as the slider's value, keeping the
|
||||
/// value between `min` and `max`.
|
||||
#[must_use]
|
||||
pub fn new(value: impl IntoDynamic<T>, min: impl IntoValue<T>, max: impl IntoValue<T>) -> Self {
|
||||
pub fn new(
|
||||
value: impl IntoDynamic<T>,
|
||||
min: impl IntoValue<T::Value>,
|
||||
max: impl IntoValue<T::Value>,
|
||||
) -> Self {
|
||||
Self {
|
||||
value: value.into_dynamic(),
|
||||
minimum: min.into_value(),
|
||||
maximum: max.into_value(),
|
||||
step: Value::Constant(ZeroToOne::new(0.05)),
|
||||
knob_size: UPx::ZERO,
|
||||
horizontal: true,
|
||||
rendered_size: Px::ZERO,
|
||||
focused_knob: None,
|
||||
mouse_buttons_down: 0,
|
||||
previous_focus: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the maximum value of this slider to `max` and returns self.
|
||||
#[must_use]
|
||||
pub fn maximum(mut self, max: impl IntoValue<T>) -> Self {
|
||||
pub fn maximum(mut self, max: impl IntoValue<T::Value>) -> Self {
|
||||
self.maximum = max.into_value();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the minimum value of this slider to `min` and returns self.
|
||||
#[must_use]
|
||||
pub fn minimum(mut self, min: impl IntoValue<T>) -> Self {
|
||||
pub fn minimum(mut self, min: impl IntoValue<T::Value>) -> Self {
|
||||
self.minimum = min.into_value();
|
||||
self
|
||||
}
|
||||
|
||||
/// The percentage to step when advancing the slider using alternative
|
||||
/// inputs (e.g, keyboard/mousewheel).
|
||||
///
|
||||
/// The widget will use this as a starting value, but will continue to step
|
||||
/// by this amount until a new unique value is obtained from linear
|
||||
/// interpolation.
|
||||
///
|
||||
/// This defaults to `0.05`/5%.
|
||||
#[must_use]
|
||||
pub fn step_by(mut self, percent: impl IntoValue<ZeroToOne>) -> Self {
|
||||
self.step = percent.into_value();
|
||||
self
|
||||
}
|
||||
|
||||
fn draw_track(&mut self, spec: &TrackSpec, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {
|
||||
if self.horizontal {
|
||||
self.rendered_size = spec.size.width;
|
||||
} else {
|
||||
self.rendered_size = spec.size.height;
|
||||
}
|
||||
let track_length = self.rendered_size - spec.knob_size;
|
||||
let value_location = track_length * spec.percent;
|
||||
let half_focus_ring = (Lp::points(2).into_px(context.gfx.scale()) / 2).ceil();
|
||||
let focus_ring = half_focus_ring * 2;
|
||||
let track_length = self.rendered_size - spec.knob_size - focus_ring;
|
||||
let (start, end) = if let Some(end) = spec.end {
|
||||
(track_length * spec.start, track_length * end)
|
||||
} else {
|
||||
(Px::ZERO, track_length * spec.start)
|
||||
};
|
||||
let inset = Point::squared(half_focus_ring);
|
||||
|
||||
let half_track = spec.track_size / 2;
|
||||
// Draw the track
|
||||
if value_location < track_length {
|
||||
context.gfx.draw_shape(&Shape::filled_round_rect(
|
||||
Rect::new(
|
||||
flipped(
|
||||
!self.horizontal,
|
||||
Point::new(value_location + spec.half_knob, spec.half_knob - half_track),
|
||||
if start > 0 {
|
||||
context.gfx.draw_shape(
|
||||
Shape::filled_round_rect(
|
||||
Rect::new(
|
||||
flipped(
|
||||
!self.horizontal,
|
||||
Point::new(spec.half_knob - half_track, spec.half_knob - half_track),
|
||||
),
|
||||
flipped(!self.horizontal, Size::new(start, spec.track_size)),
|
||||
),
|
||||
flipped(
|
||||
!self.horizontal,
|
||||
Size::new(track_length - value_location + half_track, spec.track_size),
|
||||
half_track,
|
||||
spec.inactive_track_color,
|
||||
)
|
||||
.translate_by(inset),
|
||||
);
|
||||
}
|
||||
if end < track_length {
|
||||
context.gfx.draw_shape(
|
||||
Shape::filled_round_rect(
|
||||
Rect::new(
|
||||
flipped(
|
||||
!self.horizontal,
|
||||
Point::new(end + spec.half_knob, spec.half_knob - half_track),
|
||||
),
|
||||
flipped(
|
||||
!self.horizontal,
|
||||
Size::new(track_length - end + half_track, spec.track_size),
|
||||
),
|
||||
),
|
||||
),
|
||||
half_track,
|
||||
spec.inactive_track_color,
|
||||
));
|
||||
half_track,
|
||||
spec.inactive_track_color,
|
||||
)
|
||||
.translate_by(inset),
|
||||
);
|
||||
}
|
||||
|
||||
if value_location > 0 {
|
||||
context.gfx.draw_shape(&Shape::filled_round_rect(
|
||||
Rect::new(
|
||||
flipped(
|
||||
!self.horizontal,
|
||||
Point::new(spec.half_knob - half_track, spec.half_knob - half_track),
|
||||
if start != end {
|
||||
context.gfx.draw_shape(
|
||||
Shape::filled_round_rect(
|
||||
Rect::new(
|
||||
flipped(
|
||||
!self.horizontal,
|
||||
Point::new(
|
||||
start + spec.half_knob - half_track,
|
||||
spec.half_knob - half_track,
|
||||
),
|
||||
),
|
||||
flipped(
|
||||
!self.horizontal,
|
||||
Size::new(end - start + spec.track_size, spec.track_size),
|
||||
),
|
||||
),
|
||||
flipped(
|
||||
!self.horizontal,
|
||||
Size::new(value_location + spec.track_size, spec.track_size),
|
||||
),
|
||||
),
|
||||
half_track,
|
||||
spec.track_color,
|
||||
));
|
||||
half_track,
|
||||
spec.track_color,
|
||||
)
|
||||
.translate_by(inset),
|
||||
);
|
||||
}
|
||||
|
||||
// Draw the knob
|
||||
context.gfx.draw_shape(
|
||||
Shape::filled_circle(spec.half_knob, spec.knob_color, Origin::Center).translate_by(
|
||||
let focused = context.focused();
|
||||
let this_knob_role = if spec.end.is_some() {
|
||||
Knob::End
|
||||
} else {
|
||||
Knob::Start
|
||||
};
|
||||
self.draw_knob(
|
||||
flipped(
|
||||
!self.horizontal,
|
||||
Point::new(end + spec.half_knob, spec.half_knob) + inset,
|
||||
),
|
||||
focused && self.focused_knob == Some(this_knob_role),
|
||||
focus_ring,
|
||||
spec,
|
||||
context,
|
||||
);
|
||||
|
||||
if spec.end.is_some() {
|
||||
self.draw_knob(
|
||||
flipped(
|
||||
!self.horizontal,
|
||||
Point::new(value_location + spec.half_knob, spec.half_knob),
|
||||
Point::new(start + spec.half_knob, spec.half_knob) + inset,
|
||||
),
|
||||
),
|
||||
focused && matches!(self.focused_knob, Some(Knob::Start)),
|
||||
focus_ring,
|
||||
spec,
|
||||
context,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_knob(
|
||||
&mut self,
|
||||
knob_center: Point<Px>,
|
||||
is_focused: bool,
|
||||
focus_ring_width: Px,
|
||||
spec: &TrackSpec,
|
||||
context: &mut GraphicsContext<'_, '_, '_, '_, '_>,
|
||||
) {
|
||||
context.gfx.draw_shape(
|
||||
Shape::filled_circle(spec.half_knob, spec.knob_color, Origin::Center)
|
||||
.translate_by(flipped(!self.horizontal, knob_center)),
|
||||
);
|
||||
|
||||
if is_focused {
|
||||
let focus_color = context.get(&OutlineColor);
|
||||
context.gfx.draw_shape(
|
||||
Shape::stroked_circle(
|
||||
spec.half_knob,
|
||||
focus_color,
|
||||
Origin::Center,
|
||||
StrokeOptions::px_wide(focus_ring_width),
|
||||
)
|
||||
.translate_by(knob_center),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Slider<T>
|
||||
where
|
||||
T: LinearInterpolate + Clone,
|
||||
T: SliderValue,
|
||||
{
|
||||
fn update_from_click(&mut self, position: Point<Px>) {
|
||||
fn update_from_click(&mut self, position: Point<Px>, previous_focus: Option<Knob>) {
|
||||
let knob_size = self.knob_size.into_signed();
|
||||
let position = if self.horizontal {
|
||||
position.x - knob_size / 2
|
||||
|
|
@ -144,22 +277,113 @@ where
|
|||
let track_width = self.rendered_size - knob_size;
|
||||
let position = position.clamp(Px::ZERO, track_width);
|
||||
let percent = position.into_float() / track_width.into_float();
|
||||
|
||||
let min = self.minimum.get();
|
||||
let max = self.maximum.get();
|
||||
self.value.update(min.lerp(&max, percent));
|
||||
let value = min.lerp(&max, percent);
|
||||
let (mut start, mut opt_end) = T::into_parts(self.value.get());
|
||||
if let Some(end) = &opt_end {
|
||||
let knob = if let Some(knob) = self.focused_knob {
|
||||
knob
|
||||
} else {
|
||||
// Check if the click is overlapping either knob
|
||||
let start_percent = start.percent_between(&min, &max);
|
||||
let end_percent = end.percent_between(&min, &max);
|
||||
let knob_width_as_percent =
|
||||
self.knob_size.into_float() / 2. / track_width.into_float();
|
||||
if percent - *start_percent <= knob_width_as_percent
|
||||
&& matches!(previous_focus, Some(Knob::Start))
|
||||
{
|
||||
Knob::Start
|
||||
} else if *end_percent - percent <= knob_width_as_percent
|
||||
&& matches!(previous_focus, Some(Knob::End))
|
||||
{
|
||||
Knob::End
|
||||
} else if value <= start {
|
||||
Knob::Start
|
||||
} else if &value >= end {
|
||||
Knob::End
|
||||
} else {
|
||||
Knob::Start
|
||||
}
|
||||
};
|
||||
match knob {
|
||||
Knob::Start => {
|
||||
if &value <= end {
|
||||
start = value;
|
||||
} else {
|
||||
start = end.clone();
|
||||
}
|
||||
}
|
||||
Knob::End => {
|
||||
if value >= start {
|
||||
opt_end = Some(value);
|
||||
} else {
|
||||
opt_end = Some(start.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
self.focused_knob = Some(knob);
|
||||
} else {
|
||||
start = value;
|
||||
self.focused_knob = Some(Knob::Start);
|
||||
}
|
||||
self.value.update(T::from_parts(start, opt_end));
|
||||
}
|
||||
|
||||
fn step(&mut self, forwards: bool, factor: f32) {
|
||||
let Some(focus) = self
|
||||
.focused_knob
|
||||
.or_else(|| (!T::RANGED).then_some(Knob::Start))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let (current, other) = match (focus, T::into_parts(self.value.get())) {
|
||||
(Knob::Start, (current, other)) => (current, other),
|
||||
(Knob::End, (other, Some(current))) => (current, Some(other)),
|
||||
(Knob::End, (_, None)) => unreachable!("invalid state"),
|
||||
};
|
||||
let min = self.minimum.get();
|
||||
let max = self.maximum.get();
|
||||
let step = self.step.get();
|
||||
let mut current_percent = current.percent_between(&min, &max);
|
||||
let new_value = loop {
|
||||
let next = if forwards {
|
||||
*current_percent + *step * factor
|
||||
} else {
|
||||
*current_percent - *step * factor
|
||||
};
|
||||
if next < 0. {
|
||||
break min.clone();
|
||||
} else if next > 1. {
|
||||
break max.clone();
|
||||
}
|
||||
current_percent = ZeroToOne::new(next);
|
||||
let generated_value = min.lerp(&max, *current_percent);
|
||||
if generated_value != current {
|
||||
break generated_value;
|
||||
}
|
||||
};
|
||||
// Check that the new value didn't go past the other marker, or min/max.
|
||||
let valid_relative_to_other = match (&other, focus) {
|
||||
(Some(end), Knob::Start) => new_value < *end,
|
||||
(Some(start), Knob::End) => new_value > *start,
|
||||
(None, _) => true,
|
||||
};
|
||||
if valid_relative_to_other && new_value >= min && new_value <= max {
|
||||
let (start, end) = match (focus, other) {
|
||||
(_, None) => (new_value, None),
|
||||
(Knob::Start, Some(end)) => (new_value, Some(end)),
|
||||
(Knob::End, Some(start)) => (start, Some(new_value)),
|
||||
};
|
||||
self.value.update(T::from_parts(start, end));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Widget for Slider<T>
|
||||
where
|
||||
T: Clone
|
||||
+ Debug
|
||||
+ PartialOrd
|
||||
+ LinearInterpolate
|
||||
+ PercentBetween
|
||||
+ UnwindSafe
|
||||
+ Send
|
||||
+ 'static,
|
||||
T: SliderValue,
|
||||
{
|
||||
fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {
|
||||
let track_color = context.get(&TrackColor);
|
||||
|
|
@ -173,7 +397,8 @@ where
|
|||
|
||||
let half_knob = knob_size / 2;
|
||||
|
||||
let mut value = self.value.get_tracking_refresh(context);
|
||||
let (mut start_value, mut end_value) =
|
||||
T::into_parts(self.value.get_tracking_refresh(context));
|
||||
let min = self.minimum.get_tracked(context);
|
||||
let mut max = self.maximum.get_tracked(context);
|
||||
|
||||
|
|
@ -182,19 +407,34 @@ where
|
|||
max = min.clone();
|
||||
}
|
||||
let mut value_clamped = false;
|
||||
if value < min {
|
||||
if start_value < min {
|
||||
value_clamped = true;
|
||||
value = min.clone();
|
||||
} else if value > max {
|
||||
start_value = min.clone();
|
||||
} else if start_value > max {
|
||||
value_clamped = true;
|
||||
value = max.clone();
|
||||
start_value = max.clone();
|
||||
}
|
||||
|
||||
if let Some(end) = &mut end_value {
|
||||
if *end < min {
|
||||
value_clamped = true;
|
||||
*end = min.clone();
|
||||
} else if *end < start_value {
|
||||
value_clamped = true;
|
||||
mem::swap(&mut start_value, end);
|
||||
} else if *end > max {
|
||||
value_clamped = true;
|
||||
*end = max.clone();
|
||||
}
|
||||
}
|
||||
|
||||
if value_clamped {
|
||||
self.value.map_mut(|v| *v = value.clone());
|
||||
self.value
|
||||
.map_mut(|v| *v = T::from_parts(start_value.clone(), end_value.clone()));
|
||||
}
|
||||
|
||||
let percent = value.percent_between(&min, &max);
|
||||
let start_percent = start_value.percent_between(&min, &max);
|
||||
let end_percent = end_value.map(|end| *end.percent_between(&min, &max));
|
||||
|
||||
let size = context.gfx.region().size;
|
||||
self.horizontal = size.width >= size.height;
|
||||
|
|
@ -202,7 +442,8 @@ where
|
|||
self.draw_track(
|
||||
&TrackSpec {
|
||||
size,
|
||||
percent: *percent,
|
||||
start: *start_percent,
|
||||
end: end_percent,
|
||||
half_knob,
|
||||
knob_size,
|
||||
track_size,
|
||||
|
|
@ -223,6 +464,8 @@ where
|
|||
let minimum_size = context
|
||||
.get(&MinimumSliderSize)
|
||||
.into_upx(context.gfx.scale());
|
||||
let focus_ring_width = (Lp::points(2).into_upx(context.gfx.scale()) / 2).ceil() * 2;
|
||||
let focused_knob_size = self.knob_size + focus_ring_width;
|
||||
|
||||
match (available_space.width, available_space.height) {
|
||||
(ConstraintLimit::Fill(width), ConstraintLimit::Fill(height)) => {
|
||||
|
|
@ -230,17 +473,17 @@ where
|
|||
// up with a horizontal slider.
|
||||
if width < height {
|
||||
// Vertical slider
|
||||
Size::new(self.knob_size, height.max(minimum_size))
|
||||
Size::new(focused_knob_size, height.max(minimum_size))
|
||||
} else {
|
||||
// Horizontal slider
|
||||
Size::new(width.max(minimum_size), self.knob_size)
|
||||
Size::new(width.max(minimum_size), focused_knob_size)
|
||||
}
|
||||
}
|
||||
(ConstraintLimit::Fill(width), ConstraintLimit::SizeToFit(_)) => {
|
||||
Size::new(width.max(minimum_size), self.knob_size)
|
||||
Size::new(width.max(minimum_size), focused_knob_size)
|
||||
}
|
||||
(ConstraintLimit::SizeToFit(_), ConstraintLimit::Fill(height)) => {
|
||||
Size::new(self.knob_size, height.max(minimum_size))
|
||||
Size::new(focused_knob_size, height.max(minimum_size))
|
||||
}
|
||||
(ConstraintLimit::SizeToFit(width), ConstraintLimit::SizeToFit(_)) => {
|
||||
// When we have no limit on our, we still want to be draggable.
|
||||
|
|
@ -250,7 +493,7 @@ where
|
|||
// user of the slider, a horizontal slider is expected. So, we
|
||||
// set the minimum measurement based on a horizontal
|
||||
// orientation.
|
||||
Size::new(width.min(minimum_size), self.knob_size)
|
||||
Size::new(width.min(minimum_size), focused_knob_size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -259,14 +502,67 @@ where
|
|||
true
|
||||
}
|
||||
|
||||
fn accept_focus(&mut self, context: &mut EventContext<'_, '_>) -> bool {
|
||||
context.get(&AutoFocusableControls).is_all()
|
||||
}
|
||||
|
||||
fn focus(&mut self, context: &mut EventContext<'_, '_>) {
|
||||
if self.mouse_buttons_down == 0 {
|
||||
self.focused_knob = Some(if T::RANGED && !context.focus_is_advancing() {
|
||||
Knob::End
|
||||
} else {
|
||||
Knob::Start
|
||||
});
|
||||
context.set_needs_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
fn advance_focus(
|
||||
&mut self,
|
||||
direction: VisualOrder,
|
||||
context: &mut EventContext<'_, '_>,
|
||||
) -> EventHandling {
|
||||
let (true, Some(focused)) = (T::RANGED, self.focused_knob) else {
|
||||
return IGNORED;
|
||||
};
|
||||
|
||||
let new_knob = if self.horizontal {
|
||||
match (direction.horizontal, focused) {
|
||||
(HorizontalOrder::LeftToRight, Knob::Start) => Knob::End,
|
||||
(HorizontalOrder::RightToLeft, Knob::End) => Knob::Start,
|
||||
_ => return IGNORED,
|
||||
}
|
||||
} else {
|
||||
match (direction.vertical, focused) {
|
||||
(VerticalOrder::TopToBottom, Knob::Start) => Knob::End,
|
||||
(VerticalOrder::BottomToTop, Knob::End) => Knob::Start,
|
||||
_ => return IGNORED,
|
||||
}
|
||||
};
|
||||
self.focused_knob = Some(new_knob);
|
||||
context.set_needs_redraw();
|
||||
HANDLED
|
||||
}
|
||||
|
||||
fn blur(&mut self, context: &mut EventContext<'_, '_>) {
|
||||
self.previous_focus = self.focused_knob.take();
|
||||
context.set_needs_redraw();
|
||||
}
|
||||
|
||||
fn mouse_down(
|
||||
&mut self,
|
||||
location: Point<Px>,
|
||||
_device_id: DeviceId,
|
||||
_button: MouseButton,
|
||||
_context: &mut EventContext<'_, '_>,
|
||||
context: &mut EventContext<'_, '_>,
|
||||
) -> EventHandling {
|
||||
self.update_from_click(location);
|
||||
let previous_focus = match (self.previous_focus.take(), self.focused_knob.take()) {
|
||||
(None | Some(_), Some(focus)) | (Some(focus), None) => Some(focus),
|
||||
(None, None) => None,
|
||||
};
|
||||
self.update_from_click(location, previous_focus);
|
||||
self.mouse_buttons_down += 1;
|
||||
context.focus();
|
||||
HANDLED
|
||||
}
|
||||
|
||||
|
|
@ -277,13 +573,70 @@ where
|
|||
_button: MouseButton,
|
||||
_context: &mut EventContext<'_, '_>,
|
||||
) {
|
||||
self.update_from_click(location);
|
||||
self.update_from_click(location, None);
|
||||
}
|
||||
|
||||
fn mouse_up(
|
||||
&mut self,
|
||||
_location: Option<Point<Px>>,
|
||||
_device_id: DeviceId,
|
||||
_button: MouseButton,
|
||||
_context: &mut EventContext<'_, '_>,
|
||||
) {
|
||||
self.mouse_buttons_down -= 1;
|
||||
}
|
||||
|
||||
fn keyboard_input(
|
||||
&mut self,
|
||||
_device_id: DeviceId,
|
||||
input: kludgine::app::winit::event::KeyEvent,
|
||||
_is_synthetic: bool,
|
||||
_context: &mut EventContext<'_, '_>,
|
||||
) -> EventHandling {
|
||||
let forwards = match input.logical_key {
|
||||
Key::Named(NamedKey::ArrowLeft | NamedKey::ArrowUp) => false,
|
||||
Key::Named(NamedKey::ArrowRight | NamedKey::ArrowDown) => true,
|
||||
_ => return IGNORED,
|
||||
};
|
||||
if !input.state.is_pressed() {
|
||||
return HANDLED;
|
||||
}
|
||||
|
||||
self.step(forwards, 1.);
|
||||
|
||||
HANDLED
|
||||
}
|
||||
|
||||
fn mouse_wheel(
|
||||
&mut self,
|
||||
_device_id: DeviceId,
|
||||
delta: MouseScrollDelta,
|
||||
_phase: TouchPhase,
|
||||
_context: &mut EventContext<'_, '_>,
|
||||
) -> EventHandling {
|
||||
let factor: f32 = match delta {
|
||||
MouseScrollDelta::LineDelta(_, y) => y,
|
||||
MouseScrollDelta::PixelDelta(pt) => pt.y.cast(),
|
||||
};
|
||||
|
||||
let (forwards, factor) = if factor.is_sign_negative() {
|
||||
(false, -factor)
|
||||
} else {
|
||||
(true, factor)
|
||||
};
|
||||
|
||||
self.step(forwards, factor);
|
||||
|
||||
// @ecton: Unlike scroll alreas cascasing, I feel like scrolling while
|
||||
// using a mouse wheel as an input is annoying.
|
||||
HANDLED
|
||||
}
|
||||
}
|
||||
|
||||
struct TrackSpec {
|
||||
size: Size<Px>,
|
||||
percent: f32,
|
||||
start: f32,
|
||||
end: Option<f32>,
|
||||
half_knob: Px,
|
||||
knob_size: Px,
|
||||
track_size: Px,
|
||||
|
|
@ -322,7 +675,27 @@ define_components! {
|
|||
}
|
||||
|
||||
/// A value that can be used in a [`Slider`] widget.
|
||||
pub trait Slidable<T>: IntoDynamic<T> + Sized
|
||||
pub trait SliderValue: Clone + PartialEq + UnwindSafe + Send + Debug + 'static {
|
||||
/// The component value for the slider.
|
||||
type Value: Clone
|
||||
+ Debug
|
||||
+ PartialOrd
|
||||
+ LinearInterpolate
|
||||
+ PercentBetween
|
||||
+ UnwindSafe
|
||||
+ Send
|
||||
+ 'static;
|
||||
/// When true, this type is expected to represent two values: start and an
|
||||
/// end.
|
||||
const RANGED: bool;
|
||||
|
||||
/// Returns this value split into its start and end components.
|
||||
fn into_parts(self) -> (Self::Value, Option<Self::Value>);
|
||||
/// Constructs a value from its start and end components.
|
||||
fn from_parts(min_or_value: Self::Value, max: Option<Self::Value>) -> Self;
|
||||
}
|
||||
|
||||
impl<T> SliderValue for T
|
||||
where
|
||||
T: Clone
|
||||
+ Debug
|
||||
|
|
@ -333,25 +706,22 @@ where
|
|||
+ Send
|
||||
+ 'static,
|
||||
{
|
||||
/// Returns a new slider over the full [range](Ranged) of the type.
|
||||
fn slider(self) -> Slider<T>
|
||||
where
|
||||
T: Ranged,
|
||||
{
|
||||
Slider::from_value(self.into_dynamic())
|
||||
type Value = T;
|
||||
|
||||
const RANGED: bool = false;
|
||||
|
||||
fn into_parts(self) -> (Self::Value, Option<Self::Value>) {
|
||||
(self, None)
|
||||
}
|
||||
|
||||
/// Returns a new slider using the value of `self`. The slider will be
|
||||
/// limited to values between `min` and `max`.
|
||||
fn slider_between(self, min: impl IntoValue<T>, max: impl IntoValue<T>) -> Slider<T> {
|
||||
Slider::new(self.into_dynamic(), min, max)
|
||||
fn from_parts(min_or_value: Self::Value, _max: Option<Self::Value>) -> Self {
|
||||
min_or_value
|
||||
}
|
||||
}
|
||||
|
||||
impl<U, T> Slidable<U> for T
|
||||
impl<T> SliderValue for RangeInclusive<T>
|
||||
where
|
||||
T: IntoDynamic<U>,
|
||||
U: Clone
|
||||
T: Clone
|
||||
+ Debug
|
||||
+ PartialOrd
|
||||
+ LinearInterpolate
|
||||
|
|
@ -359,5 +729,72 @@ where
|
|||
+ UnwindSafe
|
||||
+ Send
|
||||
+ 'static,
|
||||
{
|
||||
type Value = T;
|
||||
|
||||
const RANGED: bool = true;
|
||||
|
||||
fn into_parts(self) -> (Self::Value, Option<Self::Value>) {
|
||||
let (start, end) = self.into_inner();
|
||||
(start, Some(end))
|
||||
}
|
||||
|
||||
fn from_parts(min_or_value: Self::Value, max: Option<Self::Value>) -> Self {
|
||||
min_or_value..=max.assert("always provided")
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> SliderValue for (T, T)
|
||||
where
|
||||
T: Clone
|
||||
+ Debug
|
||||
+ PartialOrd
|
||||
+ LinearInterpolate
|
||||
+ PercentBetween
|
||||
+ UnwindSafe
|
||||
+ Send
|
||||
+ 'static,
|
||||
{
|
||||
type Value = T;
|
||||
|
||||
const RANGED: bool = true;
|
||||
|
||||
fn into_parts(self) -> (Self::Value, Option<Self::Value>) {
|
||||
(self.0, Some(self.1))
|
||||
}
|
||||
|
||||
fn from_parts(min_or_value: Self::Value, max: Option<Self::Value>) -> Self {
|
||||
(min_or_value, max.assert("always provided"))
|
||||
}
|
||||
}
|
||||
|
||||
/// A value that can be used in a [`Slider`] widget.
|
||||
pub trait Slidable<T>: IntoDynamic<T> + Sized
|
||||
where
|
||||
T: SliderValue,
|
||||
{
|
||||
/// Returns a new slider over the full [range](Ranged) of the type.
|
||||
fn slider(self) -> Slider<T>
|
||||
where
|
||||
T::Value: Ranged,
|
||||
{
|
||||
Slider::from_value(self.into_dynamic())
|
||||
}
|
||||
|
||||
/// Returns a new slider using the value of `self`. The slider will be
|
||||
/// limited to values between `min` and `max`.
|
||||
fn slider_between(
|
||||
self,
|
||||
min: impl IntoValue<T::Value>,
|
||||
max: impl IntoValue<T::Value>,
|
||||
) -> Slider<T> {
|
||||
Slider::new(self.into_dynamic(), min, max)
|
||||
}
|
||||
}
|
||||
|
||||
impl<U, T> Slidable<U> for T
|
||||
where
|
||||
T: IntoDynamic<U>,
|
||||
U: SliderValue,
|
||||
{
|
||||
}
|
||||
|
|
|
|||
|
|
@ -819,6 +819,7 @@ where
|
|||
),
|
||||
kludgine,
|
||||
);
|
||||
|
||||
if reverse {
|
||||
target.return_focus();
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Reference in a new issue