From d07dcdc9aa5fde3e33f9904e2b0df32bb24cd6d2 Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Sat, 11 Nov 2023 16:51:07 -0800 Subject: [PATCH] Paired dynamics are now possible Also sliders look better --- examples/theme.rs | 28 +++- src/animation.rs | 17 ++- src/value.rs | 333 ++++++++++++++++++++++++++++++++++++------ src/widgets/slider.rs | 179 ++++++++++++++++------- 4 files changed, 454 insertions(+), 103 deletions(-) diff --git a/examples/theme.rs b/examples/theme.rs index e315a06e0..39f28dfe4 100644 --- a/examples/theme.rs +++ b/examples/theme.rs @@ -1,9 +1,11 @@ +use std::str::FromStr; + use gooey::animation::ZeroToOne; use gooey::styles::components::{TextColor, WidgetBackground}; use gooey::styles::{ColorSource, ColorTheme, FixedTheme, SurfaceTheme, Theme, ThemePair}; use gooey::value::{Dynamic, MapEach}; use gooey::widget::MakeWidget; -use gooey::widgets::{Label, Scroll, Slider, Stack, Themed}; +use gooey::widgets::{Input, Label, Scroll, Slider, Stack, Themed}; use gooey::window::ThemeMode; use gooey::Run; use kludgine::Color; @@ -80,15 +82,27 @@ fn dark_mode_slider() -> (Dynamic, impl MakeWidget) { ) } +fn create_paired_string(initial_value: T) -> (Dynamic, Dynamic) +where + T: ToString + PartialEq + FromStr + Default + Send + Sync + 'static, +{ + let float = Dynamic::new(initial_value); + let text = float.map_each_unique(|f| f.to_string()); + text.for_each(float.with_clone(|float| { + move |text: &String| { + let _result = float.try_update(text.parse().unwrap_or_default()); + } + })); + (float, text) +} + fn color_editor( initial_hue: f32, initial_saturation: impl Into, label: &str, ) -> (Dynamic, impl MakeWidget) { - let hue = Dynamic::new(initial_hue); - let hue_text = hue.map_each(|hue| hue.to_string()); - let saturation = Dynamic::new(initial_saturation.into()); - let saturation_text = saturation.map_each(|saturation| saturation.to_string()); + let (hue, hue_text) = create_paired_string(initial_hue); + let (saturation, saturation_text) = create_paired_string(initial_saturation.into()); let color = (&hue, &saturation).map_each(|(hue, saturation)| ColorSource::new(*hue, *saturation)); @@ -98,9 +112,9 @@ fn color_editor( Stack::rows( Label::new(label) .and(Slider::::new(hue, 0., 360.)) - .and(Label::new(hue_text)) + .and(Input::new(hue_text)) .and(Slider::::from_value(saturation)) - .and(Label::new(saturation_text)), + .and(Input::new(saturation_text)), ), ) } diff --git a/src/animation.rs b/src/animation.rs index 11e79157b..2509b8444 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -39,9 +39,10 @@ pub mod easings; -use std::fmt::Debug; +use std::fmt::{Debug, Display}; use std::ops::{ControlFlow, Deref, Div, Mul}; use std::panic::{RefUnwindSafe, UnwindSafe}; +use std::str::FromStr; use std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock, PoisonError}; use std::thread; use std::time::{Duration, Instant}; @@ -806,6 +807,20 @@ impl ZeroToOne { } } +impl Display for ZeroToOne { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self.0, f) + } +} + +impl FromStr for ZeroToOne { + type Err = std::num::ParseFloatError; + + fn from_str(s: &str) -> Result { + s.parse().map(Self) + } +} + impl From for ZeroToOne { fn from(value: f32) -> Self { Self::new(value) diff --git a/src/value.rs b/src/value.rs index 48d835c43..4d6ed5194 100644 --- a/src/value.rs +++ b/src/value.rs @@ -1,11 +1,15 @@ //! Types for storing and interacting with values in Widgets. -use std::fmt::Debug; +use std::cell::Cell; +use std::fmt::{Debug, Display}; use std::future::Future; use std::ops::{Deref, DerefMut}; use std::panic::AssertUnwindSafe; -use std::sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError}; +use std::sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError, TryLockError}; use std::task::{Poll, Waker}; +use std::thread::ThreadId; + +use intentional::Assert; use crate::animation::{DynamicTransition, LinearInterpolate}; use crate::context::{WidgetContext, WindowHandle}; @@ -30,21 +34,32 @@ impl Dynamic { readers: 0, wakers: Vec::new(), }), + during_callback_state: Mutex::default(), sync: AssertUnwindSafe(Condvar::new()), })) } /// Maps the contents with read-only access. + /// + /// # Panics + /// + /// This function panics if this value is already locked by the current + /// thread. pub fn map_ref(&self, map: impl FnOnce(&T) -> R) -> R { - let state = self.state(); + let state = self.state().expect("deadlocked"); map(&state.wrapped.value) } /// Maps the contents with exclusive access. Before returning from this /// function, all observers will be notified that the contents have been /// updated. + /// + /// # Panics + /// + /// This function panics if this value is already locked by the current + /// thread. pub fn map_mut(&self, map: impl FnOnce(&mut T) -> R) -> R { - self.0.map_mut(|value, _| map(value)) + self.0.map_mut(|value, _| map(value)).expect("deadlocked") } /// Returns a new dynamic that is updated using `U::from(T.clone())` each @@ -99,6 +114,19 @@ impl Dynamic { self.0.map_each(move |gen| map(&gen.value)) } + /// Creates a new dynamic value that contains the result of invoking `map` + /// each time this value is changed. + /// + /// This version of `map_each` uses [`Dynamic::try_update`] to prevent + /// deadlocks and debounce dependent values. + pub fn map_each_unique(&self, mut map: F) -> Dynamic + where + F: for<'a> FnMut(&'a T) -> R + Send + 'static, + R: Send + PartialEq + 'static, + { + self.0.map_each_unique(move |gen| map(&gen.value)) + } + /// A helper function that invokes `with_clone` with a clone of self. This /// code may produce slightly more readable code. /// @@ -131,17 +159,27 @@ impl Dynamic { } /// Returns a clone of the currently contained value. + /// + /// # Panics + /// + /// This function panics if this value is already locked by the current + /// thread. #[must_use] pub fn get(&self) -> T where T: Clone, { - self.0.get().value + self.0.get().expect("deadlocked").value } /// Returns a clone of the currently contained value. /// /// `context` will be invalidated when the value is updated. + /// + /// # Panics + /// + /// This function panics if this value is already locked by the current + /// thread. #[must_use] pub fn get_tracked(&self, context: &WidgetContext<'_, '_>) -> T where @@ -153,6 +191,11 @@ impl Dynamic { /// Returns the currently stored value, replacing the current contents with /// `T::default()`. + /// + /// # Panics + /// + /// This function panics if this value is already locked by the current + /// thread. #[must_use] pub fn take(&self) -> T where @@ -163,6 +206,11 @@ impl Dynamic { /// Checks if the currently stored value is different than `T::default()`, /// and if so, returns `Some(self.take())`. + /// + /// # Panics + /// + /// This function panics if this value is already locked by the current + /// thread. #[must_use] pub fn take_if_not_default(&self) -> Option where @@ -180,44 +228,99 @@ impl Dynamic { /// Replaces the contents with `new_value`, returning the previous contents. /// Before returning from this function, all observers will be notified that /// the contents have been updated. + /// + /// # Panics + /// + /// This function panics if this value is already locked by the current + /// thread. #[must_use] pub fn replace(&self, new_value: T) -> T { self.0 .map_mut(|value, _| std::mem::replace(value, new_value)) + .expect("deadlocked") } /// Stores `new_value` in this dynamic. Before returning from this function, /// all observers will be notified that the contents have been updated. + /// + /// # Panics + /// + /// This function panics if this value is already locked by the current + /// thread. pub fn set(&self, new_value: T) { let _old = self.replace(new_value); } /// Updates this dynamic with `new_value`, but only if `new_value` is not /// equal to the currently stored value. - pub fn update(&self, new_value: T) + /// + /// Returns true if the value was updated. + /// + /// # Panics + /// + /// This function panics if this value is already locked by the current + /// thread. + pub fn update(&self, new_value: T) -> bool where T: PartialEq, { - self.0.map_mut(|value, changed| { - if *value == new_value { - *changed = false; - } else { - *value = new_value; - } - }); + self.0 + .map_mut(|value, changed| { + if *value == new_value { + *changed = false; + false + } else { + *value = new_value; + true + } + }) + .expect("deadlocked") + } + + /// Attempt to store `new_value` in `self`. If the value cannot be stored + /// due to a deadlock, it is returned as an error. + /// + /// Returns true if the value was updated. + pub fn try_update(&self, new_value: T) -> Result + where + T: PartialEq, + { + let cell = Cell::new(Some(new_value)); + self.0 + .map_mut(|value, changed| { + let new_value = cell.take().assert("only one callback will be invoked"); + if *value == new_value { + *changed = false; + false + } else { + *value = new_value; + true + } + }) + .map_err(|_| cell.take().assert("only one callback will be invoked")) } /// Returns a new reference-based reader for this dynamic value. + /// + /// # Panics + /// + /// This function panics if this value is already locked by the current + /// thread. #[must_use] pub fn create_reader(&self) -> DynamicReader { - self.state().readers += 1; + self.state().expect("deadlocked").readers += 1; DynamicReader { source: self.0.clone(), - read_generation: self.0.state().wrapped.generation, + read_generation: self.0.state().expect("deadlocked").wrapped.generation, } } /// Converts this [`Dynamic`] into a reader. + /// + /// # Panics + /// + /// This function panics if this value is already locked by the current + /// thread. #[must_use] pub fn into_reader(self) -> DynamicReader { self.create_reader() @@ -227,22 +330,32 @@ impl Dynamic { /// /// This call will block until all other guards for this dynamic have been /// dropped. + /// + /// # Panics + /// + /// This function panics if this value is already locked by the current + /// thread. #[must_use] pub fn lock(&self) -> DynamicGuard<'_, T> { DynamicGuard { - guard: self.0.state(), + guard: self.0.state().expect("deadlocked"), accessed_mut: false, } } - fn state(&self) -> MutexGuard<'_, State> { + fn state(&self) -> Result, DeadlockError> { self.0.state() } /// Returns the current generation of the value. + /// + /// # Panics + /// + /// This function panics if this value is already locked by the current + /// thread. #[must_use] pub fn generation(&self) -> Generation { - self.state().wrapped.generation + self.state().expect("deadlocked").wrapped.generation } /// Returns a pending transition for this value to `new_value`. @@ -274,7 +387,7 @@ impl Clone for Dynamic { impl Drop for Dynamic { fn drop(&mut self) { - let state = self.state(); + let state = self.state().expect("deadlocked"); if state.readers == 0 { drop(state); self.0.sync.notify_all(); @@ -288,9 +401,47 @@ impl From> for DynamicReader { } } +#[derive(Debug)] +struct DynamicMutexGuard<'a, T> { + dynamic: &'a DynamicData, + guard: MutexGuard<'a, State>, +} + +impl<'a, T> Drop for DynamicMutexGuard<'a, T> { + fn drop(&mut self) { + let mut during_state = self + .dynamic + .during_callback_state + .lock() + .map_or_else(PoisonError::into_inner, |g| g); + *during_state = None; + drop(during_state); + self.dynamic.sync.notify_all(); + } +} + +impl<'a, T> Deref for DynamicMutexGuard<'a, T> { + type Target = State; + + fn deref(&self) -> &Self::Target { + &self.guard + } +} +impl<'a, T> DerefMut for DynamicMutexGuard<'a, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.guard + } +} + +#[derive(Debug)] +struct LockState { + locked_thread: ThreadId, +} + #[derive(Debug)] struct DynamicData { state: Mutex>, + during_callback_state: Mutex>, // The AssertUnwindSafe is only needed on Mac. For some reason on // Mac OS, Condvar isn't RefUnwindSafe. @@ -298,27 +449,56 @@ struct DynamicData { } impl DynamicData { - fn state(&self) -> MutexGuard<'_, State> { - self.state + fn state(&self) -> Result, DeadlockError> { + let mut during_sync = self + .during_callback_state .lock() - .map_or_else(PoisonError::into_inner, |g| g) + .map_or_else(PoisonError::into_inner, |g| g); + + let current_thread_id = std::thread::current().id(); + let guard = loop { + match self.state.try_lock() { + Ok(g) => break g, + Err(TryLockError::Poisoned(poision)) => break poision.into_inner(), + Err(TryLockError::WouldBlock) => loop { + match &*during_sync { + Some(state) if state.locked_thread == current_thread_id => { + return Err(DeadlockError) + } + Some(_) => { + during_sync = self + .sync + .wait(during_sync) + .map_or_else(PoisonError::into_inner, |g| g); + } + None => break, + } + }, + } + }; + *during_sync = Some(LockState { + locked_thread: current_thread_id, + }); + Ok(DynamicMutexGuard { + dynamic: self, + guard, + }) } pub fn redraw_when_changed(&self, window: WindowHandle) { - let mut state = self.state(); + let mut state = self.state().expect("deadlocked"); state.windows.push(window); } - #[must_use] - pub fn get(&self) -> GenerationalValue + pub fn get(&self) -> Result, DeadlockError> where T: Clone, { - self.state().wrapped.clone() + self.state().map(|state| state.wrapped.clone()) } - pub fn map_mut(&self, map: impl FnOnce(&mut T, &mut bool) -> R) -> R { - let mut state = self.state(); + pub fn map_mut(&self, map: impl FnOnce(&mut T, &mut bool) -> R) -> Result { + let mut state = self.state()?; let old = { let state = &mut *state; let mut changed = true; @@ -333,14 +513,14 @@ impl DynamicData { self.sync.notify_all(); - old + Ok(old) } pub fn for_each(&self, map: F) where F: for<'a> FnMut(&'a GenerationalValue) + Send + 'static, { - let mut state = self.state(); + let mut state = self.state().expect("deadlocked"); state.callbacks.push(Box::new(map)); } @@ -349,7 +529,7 @@ impl DynamicData { F: for<'a> FnMut(&'a GenerationalValue) -> R + Send + 'static, R: Send + 'static, { - let mut state = self.state(); + let mut state = self.state().expect("deadlocked"); let initial_value = map(&state.wrapped); let mapped_value = Dynamic::new(initial_value); let returned = mapped_value.clone(); @@ -361,6 +541,39 @@ impl DynamicData { returned } + + pub fn map_each_unique(&self, mut map: F) -> Dynamic + where + F: for<'a> FnMut(&'a GenerationalValue) -> R + Send + 'static, + R: PartialEq + Send + 'static, + { + let mut state = self.state().expect("deadlocked"); + let initial_value = map(&state.wrapped); + let mapped_value = Dynamic::new(initial_value); + let returned = mapped_value.clone(); + state + .callbacks + .push(Box::new(move |updated: &GenerationalValue| { + let _deadlock = mapped_value.try_update(map(updated)); + })); + + returned + } +} + +/// A deadlock occurred accessing a [`Dynamic`]. +/// +/// Currently Gooey is only able to detect deadlocks where a single thread tries +/// to lock the same [`Dynamic`] multiple times. +#[derive(Debug)] +pub struct DeadlockError; + +impl std::error::Error for DeadlockError {} + +impl Display for DeadlockError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("a deadlock was detected") + } } struct State { @@ -424,7 +637,7 @@ struct GenerationalValue { /// notified of a change when this guard is dropped. #[derive(Debug)] pub struct DynamicGuard<'a, T> { - guard: MutexGuard<'a, State>, + guard: DynamicMutexGuard<'a, T>, accessed_mut: bool, } @@ -462,28 +675,43 @@ impl DynamicReader { /// Maps the contents of the dynamic value and returns the result. /// /// This function marks the currently stored value as being read. + /// + /// # Panics + /// + /// This function panics if this value is already locked by the current + /// thread. pub fn map_ref(&mut self, map: impl FnOnce(&T) -> R) -> R { - let state = self.source.state(); + let state = self.source.state().expect("deadlocked"); self.read_generation = state.wrapped.generation; map(&state.wrapped.value) } /// Returns true if the dynamic has been modified since the last time the /// value was accessed through this reader. + /// + /// # Panics + /// + /// This function panics if this value is already locked by the current + /// thread. #[must_use] pub fn has_updated(&self) -> bool { - self.source.state().wrapped.generation != self.read_generation + self.source.state().expect("deadlocked").wrapped.generation != self.read_generation } /// Returns a clone of the currently contained value. /// /// This function marks the currently stored value as being read. + /// + /// # Panics + /// + /// This function panics if this value is already locked by the current + /// thread. #[must_use] pub fn get(&mut self) -> T where T: Clone, { - let GenerationalValue { value, generation } = self.source.get(); + let GenerationalValue { value, generation } = self.source.get().expect("deadlocked"); self.read_generation = generation; value } @@ -492,19 +720,42 @@ impl DynamicReader { /// there are no remaining writers for the value. /// /// Returns true if a newly updated value was discovered. + /// + /// # Panics + /// + /// This function panics if this value is already locked by the current + /// thread. pub fn block_until_updated(&mut self) -> bool { - let mut state = self.source.state(); + let mut deadlock_state = self + .source + .during_callback_state + .lock() + .map_or_else(PoisonError::into_inner, |g| g); + assert!( + deadlock_state + .as_ref() + .map_or(true, |state| state.locked_thread + != std::thread::current().id()), + "deadlocked" + ); loop { + let state = self + .source + .state + .lock() + .map_or_else(PoisonError::into_inner, |g| g); if state.wrapped.generation != self.read_generation { return true; } else if state.readers == Arc::strong_count(&self.source) { return false; } + drop(state); - state = self + // Wait for a notification of a change, which is synch + deadlock_state = self .source .sync - .wait(state) + .wait(deadlock_state) .map_or_else(PoisonError::into_inner, |g| g); } } @@ -520,7 +771,7 @@ impl DynamicReader { impl Clone for DynamicReader { fn clone(&self) -> Self { - self.source.state().readers += 1; + self.source.state().expect("deadlocked").readers += 1; Self { source: self.source.clone(), read_generation: self.read_generation, @@ -530,7 +781,7 @@ impl Clone for DynamicReader { impl Drop for DynamicReader { fn drop(&mut self) { - let mut state = self.source.state(); + let mut state = self.source.state().expect("deadlocked"); state.readers -= 1; } } @@ -547,7 +798,7 @@ impl<'a, T> Future for BlockUntilUpdatedFuture<'a, T> { type Output = bool; fn poll(self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { - let mut state = self.0.source.state(); + let mut state = self.0.source.state().expect("deadlocked"); if state.wrapped.generation != self.0.read_generation { return Poll::Ready(true); } else if state.readers == Arc::strong_count(&self.0.source) { diff --git a/src/widgets/slider.rs b/src/widgets/slider.rs index 508ae3d15..0f7309506 100644 --- a/src/widgets/slider.rs +++ b/src/widgets/slider.rs @@ -5,7 +5,8 @@ use std::panic::UnwindSafe; use kludgine::app::winit::event::{DeviceId, MouseButton}; use kludgine::figures::units::{Lp, Px, UPx}; use kludgine::figures::{ - FloatConversion, IntoSigned, IntoUnsigned, Point, Ranged, Rect, ScreenScale, Size, + FloatConversion, FromComponents, IntoComponents, IntoSigned, IntoUnsigned, Point, Ranged, Rect, + ScreenScale, Size, }; use kludgine::shapes::Shape; use kludgine::{Color, Origin}; @@ -70,6 +71,68 @@ impl Slider { self.minimum = min.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 + spec.half_knob; + + let half_track = spec.track_size / 2; + // Draw the track + if value_location > spec.half_knob { + context.gfx.draw_shape( + &Shape::filled_rect( + Rect::new( + flipped( + !self.horizontal, + Point::new(spec.half_knob, spec.half_knob - half_track), + ), + flipped(!self.horizontal, Size::new(value_location, spec.track_size)), + ), + spec.track_color, + ), + Point::default(), + None, + None, + ); + } + + if value_location < track_length { + context.gfx.draw_shape( + &Shape::filled_rect( + Rect::new( + flipped( + !self.horizontal, + Point::new(value_location, spec.half_knob - half_track), + ), + flipped( + !self.horizontal, + Size::new( + track_length - value_location + spec.half_knob, + spec.track_size, + ), + ), + ), + spec.inactive_track_color, + ), + Point::default(), + None, + None, + ); + } + + // Draw the knob + context.gfx.draw_shape( + &Shape::filled_circle(spec.half_knob, spec.knob_color, Origin::Center), + flipped(!self.horizontal, Point::new(value_location, spec.half_knob)), + None, + None, + ); + } } impl Slider @@ -102,8 +165,10 @@ where + 'static, { fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { - let styles = context.query_styles(&[&TrackColor, &KnobColor, &TrackSize]); + let styles = + context.query_styles(&[&TrackColor, &InactiveTrackColor, &KnobColor, &TrackSize]); let track_color = styles.get(&TrackColor, context); + let inactive_track_color = styles.get(&InactiveTrackColor, context); let knob_color = styles.get(&KnobColor, context); let knob_size = self.knob_size.into_signed(); let track_size = styles @@ -112,7 +177,6 @@ where .min(knob_size); let half_knob = knob_size / 2; - let half_track = track_size / 2; let mut value = self.value.get_tracked(context); let min = self.minimum.get_tracked(context); @@ -140,55 +204,19 @@ where let size = context.gfx.region().size; self.horizontal = size.width >= size.height; - if self.horizontal { - self.rendered_size = size.width; - // Draw the track - context.gfx.draw_shape( - &Shape::filled_rect( - Rect::new( - Point::new(half_knob, half_knob - half_track), - Size::new(size.width - knob_size, track_size), - ), - track_color, - ), - Point::default(), - None, - None, - ); - - // Draw the knob - context.gfx.draw_shape( - &Shape::filled_circle(half_knob, knob_color, Origin::Center), - Point::new(half_knob + (size.width - knob_size) * *percent, half_knob), - None, - None, - ); - } else { - // Vertical slider - self.rendered_size = size.height; - - // Draw the track - context.gfx.draw_shape( - &Shape::filled_rect( - Rect::new( - Point::new(half_knob - half_track, half_knob), - Size::new(track_size, size.height - knob_size), - ), - track_color, - ), - Point::default(), - None, - None, - ); - - // Draw the knob - context.gfx.draw_shape( - &Shape::filled_circle(half_knob, knob_color, Origin::Center), - Point::new(half_knob, half_knob + (size.height - knob_size) * *percent), - None, - None, - ); - } + self.draw_track( + &TrackSpec { + size, + percent: *percent, + half_knob, + knob_size, + track_size, + knob_color, + track_color, + inactive_track_color, + }, + context, + ); } fn layout( @@ -263,6 +291,29 @@ where } } +struct TrackSpec { + size: Size, + percent: f32, + half_knob: Px, + knob_size: Px, + track_size: Px, + knob_color: Color, + track_color: Color, + inactive_track_color: Color, +} + +fn flipped(flip: bool, value: T) -> T +where + T: IntoComponents + FromComponents, +{ + if flip { + let (a, b) = value.into_components(); + T::from_components((b, a)) + } else { + value + } +} + /// The size of the track that the knob of a [`Slider`] traversesq. pub struct TrackSize; @@ -331,14 +382,14 @@ impl NamedComponent for KnobColor { } } -/// The color of the draggable portion of the knob. +/// The color of the track that the knob rests on. pub struct TrackColor; impl ComponentDefinition for TrackColor { type ComponentType = Color; fn default_value(&self, context: &WidgetContext<'_, '_>) -> Self::ComponentType { - context.theme().surface.on_color_variant + context.theme().primary.color } } @@ -347,3 +398,23 @@ impl NamedComponent for TrackColor { Cow::Owned(ComponentName::new(Group::new("Slider"), "track_color")) } } + +/// The color of the draggable portion of the knob. +pub struct InactiveTrackColor; + +impl ComponentDefinition for InactiveTrackColor { + type ComponentType = Color; + + fn default_value(&self, context: &WidgetContext<'_, '_>) -> Self::ComponentType { + context.theme().surface.outline + } +} + +impl NamedComponent for InactiveTrackColor { + fn name(&self) -> Cow<'_, ComponentName> { + Cow::Owned(ComponentName::new( + Group::new("Slider"), + "inactive_track_color", + )) + } +}