mirror of
https://github.com/danbulant/cushy
synced 2026-05-24 12:28:23 +00:00
Paired dynamics are now possible
Also sliders look better
This commit is contained in:
parent
019412543c
commit
d07dcdc9aa
4 changed files with 454 additions and 103 deletions
|
|
@ -1,9 +1,11 @@
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
use gooey::animation::ZeroToOne;
|
use gooey::animation::ZeroToOne;
|
||||||
use gooey::styles::components::{TextColor, WidgetBackground};
|
use gooey::styles::components::{TextColor, WidgetBackground};
|
||||||
use gooey::styles::{ColorSource, ColorTheme, FixedTheme, SurfaceTheme, Theme, ThemePair};
|
use gooey::styles::{ColorSource, ColorTheme, FixedTheme, SurfaceTheme, Theme, ThemePair};
|
||||||
use gooey::value::{Dynamic, MapEach};
|
use gooey::value::{Dynamic, MapEach};
|
||||||
use gooey::widget::MakeWidget;
|
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::window::ThemeMode;
|
||||||
use gooey::Run;
|
use gooey::Run;
|
||||||
use kludgine::Color;
|
use kludgine::Color;
|
||||||
|
|
@ -80,15 +82,27 @@ fn dark_mode_slider() -> (Dynamic<ThemeMode>, impl MakeWidget) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_paired_string<T>(initial_value: T) -> (Dynamic<T>, Dynamic<String>)
|
||||||
|
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(
|
fn color_editor(
|
||||||
initial_hue: f32,
|
initial_hue: f32,
|
||||||
initial_saturation: impl Into<ZeroToOne>,
|
initial_saturation: impl Into<ZeroToOne>,
|
||||||
label: &str,
|
label: &str,
|
||||||
) -> (Dynamic<ColorSource>, impl MakeWidget) {
|
) -> (Dynamic<ColorSource>, impl MakeWidget) {
|
||||||
let hue = Dynamic::new(initial_hue);
|
let (hue, hue_text) = create_paired_string(initial_hue);
|
||||||
let hue_text = hue.map_each(|hue| hue.to_string());
|
let (saturation, saturation_text) = create_paired_string(initial_saturation.into());
|
||||||
let saturation = Dynamic::new(initial_saturation.into());
|
|
||||||
let saturation_text = saturation.map_each(|saturation| saturation.to_string());
|
|
||||||
|
|
||||||
let color =
|
let color =
|
||||||
(&hue, &saturation).map_each(|(hue, saturation)| ColorSource::new(*hue, *saturation));
|
(&hue, &saturation).map_each(|(hue, saturation)| ColorSource::new(*hue, *saturation));
|
||||||
|
|
@ -98,9 +112,9 @@ fn color_editor(
|
||||||
Stack::rows(
|
Stack::rows(
|
||||||
Label::new(label)
|
Label::new(label)
|
||||||
.and(Slider::<f32>::new(hue, 0., 360.))
|
.and(Slider::<f32>::new(hue, 0., 360.))
|
||||||
.and(Label::new(hue_text))
|
.and(Input::new(hue_text))
|
||||||
.and(Slider::<ZeroToOne>::from_value(saturation))
|
.and(Slider::<ZeroToOne>::from_value(saturation))
|
||||||
.and(Label::new(saturation_text)),
|
.and(Input::new(saturation_text)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,9 +39,10 @@
|
||||||
|
|
||||||
pub mod easings;
|
pub mod easings;
|
||||||
|
|
||||||
use std::fmt::Debug;
|
use std::fmt::{Debug, Display};
|
||||||
use std::ops::{ControlFlow, Deref, Div, Mul};
|
use std::ops::{ControlFlow, Deref, Div, Mul};
|
||||||
use std::panic::{RefUnwindSafe, UnwindSafe};
|
use std::panic::{RefUnwindSafe, UnwindSafe};
|
||||||
|
use std::str::FromStr;
|
||||||
use std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock, PoisonError};
|
use std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock, PoisonError};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::{Duration, Instant};
|
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<Self, Self::Err> {
|
||||||
|
s.parse().map(Self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<f32> for ZeroToOne {
|
impl From<f32> for ZeroToOne {
|
||||||
fn from(value: f32) -> Self {
|
fn from(value: f32) -> Self {
|
||||||
Self::new(value)
|
Self::new(value)
|
||||||
|
|
|
||||||
323
src/value.rs
323
src/value.rs
|
|
@ -1,11 +1,15 @@
|
||||||
//! Types for storing and interacting with values in Widgets.
|
//! 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::future::Future;
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
use std::panic::AssertUnwindSafe;
|
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::task::{Poll, Waker};
|
||||||
|
use std::thread::ThreadId;
|
||||||
|
|
||||||
|
use intentional::Assert;
|
||||||
|
|
||||||
use crate::animation::{DynamicTransition, LinearInterpolate};
|
use crate::animation::{DynamicTransition, LinearInterpolate};
|
||||||
use crate::context::{WidgetContext, WindowHandle};
|
use crate::context::{WidgetContext, WindowHandle};
|
||||||
|
|
@ -30,21 +34,32 @@ impl<T> Dynamic<T> {
|
||||||
readers: 0,
|
readers: 0,
|
||||||
wakers: Vec::new(),
|
wakers: Vec::new(),
|
||||||
}),
|
}),
|
||||||
|
during_callback_state: Mutex::default(),
|
||||||
sync: AssertUnwindSafe(Condvar::new()),
|
sync: AssertUnwindSafe(Condvar::new()),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Maps the contents with read-only access.
|
/// 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<R>(&self, map: impl FnOnce(&T) -> R) -> R {
|
pub fn map_ref<R>(&self, map: impl FnOnce(&T) -> R) -> R {
|
||||||
let state = self.state();
|
let state = self.state().expect("deadlocked");
|
||||||
map(&state.wrapped.value)
|
map(&state.wrapped.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Maps the contents with exclusive access. Before returning from this
|
/// Maps the contents with exclusive access. Before returning from this
|
||||||
/// function, all observers will be notified that the contents have been
|
/// function, all observers will be notified that the contents have been
|
||||||
/// updated.
|
/// updated.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This function panics if this value is already locked by the current
|
||||||
|
/// thread.
|
||||||
pub fn map_mut<R>(&self, map: impl FnOnce(&mut T) -> R) -> R {
|
pub fn map_mut<R>(&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
|
/// Returns a new dynamic that is updated using `U::from(T.clone())` each
|
||||||
|
|
@ -99,6 +114,19 @@ impl<T> Dynamic<T> {
|
||||||
self.0.map_each(move |gen| map(&gen.value))
|
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<R, F>(&self, mut map: F) -> Dynamic<R>
|
||||||
|
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
|
/// A helper function that invokes `with_clone` with a clone of self. This
|
||||||
/// code may produce slightly more readable code.
|
/// code may produce slightly more readable code.
|
||||||
///
|
///
|
||||||
|
|
@ -131,17 +159,27 @@ impl<T> Dynamic<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a clone of the currently contained value.
|
/// Returns a clone of the currently contained value.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This function panics if this value is already locked by the current
|
||||||
|
/// thread.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn get(&self) -> T
|
pub fn get(&self) -> T
|
||||||
where
|
where
|
||||||
T: Clone,
|
T: Clone,
|
||||||
{
|
{
|
||||||
self.0.get().value
|
self.0.get().expect("deadlocked").value
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a clone of the currently contained value.
|
/// Returns a clone of the currently contained value.
|
||||||
///
|
///
|
||||||
/// `context` will be invalidated when the value is updated.
|
/// `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]
|
#[must_use]
|
||||||
pub fn get_tracked(&self, context: &WidgetContext<'_, '_>) -> T
|
pub fn get_tracked(&self, context: &WidgetContext<'_, '_>) -> T
|
||||||
where
|
where
|
||||||
|
|
@ -153,6 +191,11 @@ impl<T> Dynamic<T> {
|
||||||
|
|
||||||
/// Returns the currently stored value, replacing the current contents with
|
/// Returns the currently stored value, replacing the current contents with
|
||||||
/// `T::default()`.
|
/// `T::default()`.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This function panics if this value is already locked by the current
|
||||||
|
/// thread.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn take(&self) -> T
|
pub fn take(&self) -> T
|
||||||
where
|
where
|
||||||
|
|
@ -163,6 +206,11 @@ impl<T> Dynamic<T> {
|
||||||
|
|
||||||
/// Checks if the currently stored value is different than `T::default()`,
|
/// Checks if the currently stored value is different than `T::default()`,
|
||||||
/// and if so, returns `Some(self.take())`.
|
/// and if so, returns `Some(self.take())`.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This function panics if this value is already locked by the current
|
||||||
|
/// thread.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn take_if_not_default(&self) -> Option<T>
|
pub fn take_if_not_default(&self) -> Option<T>
|
||||||
where
|
where
|
||||||
|
|
@ -180,44 +228,99 @@ impl<T> Dynamic<T> {
|
||||||
/// Replaces the contents with `new_value`, returning the previous contents.
|
/// Replaces the contents with `new_value`, returning the previous contents.
|
||||||
/// Before returning from this function, all observers will be notified that
|
/// Before returning from this function, all observers will be notified that
|
||||||
/// the contents have been updated.
|
/// the contents have been updated.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This function panics if this value is already locked by the current
|
||||||
|
/// thread.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn replace(&self, new_value: T) -> T {
|
pub fn replace(&self, new_value: T) -> T {
|
||||||
self.0
|
self.0
|
||||||
.map_mut(|value, _| std::mem::replace(value, new_value))
|
.map_mut(|value, _| std::mem::replace(value, new_value))
|
||||||
|
.expect("deadlocked")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stores `new_value` in this dynamic. Before returning from this function,
|
/// Stores `new_value` in this dynamic. Before returning from this function,
|
||||||
/// all observers will be notified that the contents have been updated.
|
/// 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) {
|
pub fn set(&self, new_value: T) {
|
||||||
let _old = self.replace(new_value);
|
let _old = self.replace(new_value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates this dynamic with `new_value`, but only if `new_value` is not
|
/// Updates this dynamic with `new_value`, but only if `new_value` is not
|
||||||
/// equal to the currently stored value.
|
/// 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
|
where
|
||||||
T: PartialEq,
|
T: PartialEq,
|
||||||
{
|
{
|
||||||
self.0.map_mut(|value, changed| {
|
self.0
|
||||||
|
.map_mut(|value, changed| {
|
||||||
if *value == new_value {
|
if *value == new_value {
|
||||||
*changed = false;
|
*changed = false;
|
||||||
|
false
|
||||||
} else {
|
} else {
|
||||||
*value = new_value;
|
*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<bool, T>
|
||||||
|
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.
|
/// 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]
|
#[must_use]
|
||||||
pub fn create_reader(&self) -> DynamicReader<T> {
|
pub fn create_reader(&self) -> DynamicReader<T> {
|
||||||
self.state().readers += 1;
|
self.state().expect("deadlocked").readers += 1;
|
||||||
DynamicReader {
|
DynamicReader {
|
||||||
source: self.0.clone(),
|
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.
|
/// Converts this [`Dynamic`] into a reader.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This function panics if this value is already locked by the current
|
||||||
|
/// thread.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn into_reader(self) -> DynamicReader<T> {
|
pub fn into_reader(self) -> DynamicReader<T> {
|
||||||
self.create_reader()
|
self.create_reader()
|
||||||
|
|
@ -227,22 +330,32 @@ impl<T> Dynamic<T> {
|
||||||
///
|
///
|
||||||
/// This call will block until all other guards for this dynamic have been
|
/// This call will block until all other guards for this dynamic have been
|
||||||
/// dropped.
|
/// dropped.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This function panics if this value is already locked by the current
|
||||||
|
/// thread.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn lock(&self) -> DynamicGuard<'_, T> {
|
pub fn lock(&self) -> DynamicGuard<'_, T> {
|
||||||
DynamicGuard {
|
DynamicGuard {
|
||||||
guard: self.0.state(),
|
guard: self.0.state().expect("deadlocked"),
|
||||||
accessed_mut: false,
|
accessed_mut: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn state(&self) -> MutexGuard<'_, State<T>> {
|
fn state(&self) -> Result<DynamicMutexGuard<'_, T>, DeadlockError> {
|
||||||
self.0.state()
|
self.0.state()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the current generation of the value.
|
/// Returns the current generation of the value.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This function panics if this value is already locked by the current
|
||||||
|
/// thread.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn generation(&self) -> Generation {
|
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`.
|
/// Returns a pending transition for this value to `new_value`.
|
||||||
|
|
@ -274,7 +387,7 @@ impl<T> Clone for Dynamic<T> {
|
||||||
|
|
||||||
impl<T> Drop for Dynamic<T> {
|
impl<T> Drop for Dynamic<T> {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
let state = self.state();
|
let state = self.state().expect("deadlocked");
|
||||||
if state.readers == 0 {
|
if state.readers == 0 {
|
||||||
drop(state);
|
drop(state);
|
||||||
self.0.sync.notify_all();
|
self.0.sync.notify_all();
|
||||||
|
|
@ -288,9 +401,47 @@ impl<T> From<Dynamic<T>> for DynamicReader<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct DynamicMutexGuard<'a, T> {
|
||||||
|
dynamic: &'a DynamicData<T>,
|
||||||
|
guard: MutexGuard<'a, State<T>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<T>;
|
||||||
|
|
||||||
|
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)]
|
#[derive(Debug)]
|
||||||
struct DynamicData<T> {
|
struct DynamicData<T> {
|
||||||
state: Mutex<State<T>>,
|
state: Mutex<State<T>>,
|
||||||
|
during_callback_state: Mutex<Option<LockState>>,
|
||||||
|
|
||||||
// The AssertUnwindSafe is only needed on Mac. For some reason on
|
// The AssertUnwindSafe is only needed on Mac. For some reason on
|
||||||
// Mac OS, Condvar isn't RefUnwindSafe.
|
// Mac OS, Condvar isn't RefUnwindSafe.
|
||||||
|
|
@ -298,27 +449,56 @@ struct DynamicData<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> DynamicData<T> {
|
impl<T> DynamicData<T> {
|
||||||
fn state(&self) -> MutexGuard<'_, State<T>> {
|
fn state(&self) -> Result<DynamicMutexGuard<'_, T>, DeadlockError> {
|
||||||
self.state
|
let mut during_sync = self
|
||||||
|
.during_callback_state
|
||||||
.lock()
|
.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) {
|
pub fn redraw_when_changed(&self, window: WindowHandle) {
|
||||||
let mut state = self.state();
|
let mut state = self.state().expect("deadlocked");
|
||||||
state.windows.push(window);
|
state.windows.push(window);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
pub fn get(&self) -> Result<GenerationalValue<T>, DeadlockError>
|
||||||
pub fn get(&self) -> GenerationalValue<T>
|
|
||||||
where
|
where
|
||||||
T: Clone,
|
T: Clone,
|
||||||
{
|
{
|
||||||
self.state().wrapped.clone()
|
self.state().map(|state| state.wrapped.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn map_mut<R>(&self, map: impl FnOnce(&mut T, &mut bool) -> R) -> R {
|
pub fn map_mut<R>(&self, map: impl FnOnce(&mut T, &mut bool) -> R) -> Result<R, DeadlockError> {
|
||||||
let mut state = self.state();
|
let mut state = self.state()?;
|
||||||
let old = {
|
let old = {
|
||||||
let state = &mut *state;
|
let state = &mut *state;
|
||||||
let mut changed = true;
|
let mut changed = true;
|
||||||
|
|
@ -333,14 +513,14 @@ impl<T> DynamicData<T> {
|
||||||
|
|
||||||
self.sync.notify_all();
|
self.sync.notify_all();
|
||||||
|
|
||||||
old
|
Ok(old)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn for_each<F>(&self, map: F)
|
pub fn for_each<F>(&self, map: F)
|
||||||
where
|
where
|
||||||
F: for<'a> FnMut(&'a GenerationalValue<T>) + Send + 'static,
|
F: for<'a> FnMut(&'a GenerationalValue<T>) + Send + 'static,
|
||||||
{
|
{
|
||||||
let mut state = self.state();
|
let mut state = self.state().expect("deadlocked");
|
||||||
state.callbacks.push(Box::new(map));
|
state.callbacks.push(Box::new(map));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -349,7 +529,7 @@ impl<T> DynamicData<T> {
|
||||||
F: for<'a> FnMut(&'a GenerationalValue<T>) -> R + Send + 'static,
|
F: for<'a> FnMut(&'a GenerationalValue<T>) -> R + Send + 'static,
|
||||||
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 initial_value = map(&state.wrapped);
|
||||||
let mapped_value = Dynamic::new(initial_value);
|
let mapped_value = Dynamic::new(initial_value);
|
||||||
let returned = mapped_value.clone();
|
let returned = mapped_value.clone();
|
||||||
|
|
@ -361,6 +541,39 @@ impl<T> DynamicData<T> {
|
||||||
|
|
||||||
returned
|
returned
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn map_each_unique<R, F>(&self, mut map: F) -> Dynamic<R>
|
||||||
|
where
|
||||||
|
F: for<'a> FnMut(&'a GenerationalValue<T>) -> 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<T>| {
|
||||||
|
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<T> {
|
struct State<T> {
|
||||||
|
|
@ -424,7 +637,7 @@ struct GenerationalValue<T> {
|
||||||
/// notified of a change when this guard is dropped.
|
/// notified of a change when this guard is dropped.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct DynamicGuard<'a, T> {
|
pub struct DynamicGuard<'a, T> {
|
||||||
guard: MutexGuard<'a, State<T>>,
|
guard: DynamicMutexGuard<'a, T>,
|
||||||
accessed_mut: bool,
|
accessed_mut: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -462,28 +675,43 @@ impl<T> DynamicReader<T> {
|
||||||
/// Maps the contents of the dynamic value and returns the result.
|
/// Maps the contents of the dynamic value and returns the result.
|
||||||
///
|
///
|
||||||
/// This function marks the currently stored value as being read.
|
/// 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<R>(&mut self, map: impl FnOnce(&T) -> R) -> R {
|
pub fn map_ref<R>(&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;
|
self.read_generation = state.wrapped.generation;
|
||||||
map(&state.wrapped.value)
|
map(&state.wrapped.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if the dynamic has been modified since the last time the
|
/// Returns true if the dynamic has been modified since the last time the
|
||||||
/// value was accessed through this reader.
|
/// value was accessed through this reader.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This function panics if this value is already locked by the current
|
||||||
|
/// thread.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn has_updated(&self) -> bool {
|
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.
|
/// Returns a clone of the currently contained value.
|
||||||
///
|
///
|
||||||
/// This function marks the currently stored value as being read.
|
/// 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]
|
#[must_use]
|
||||||
pub fn get(&mut self) -> T
|
pub fn get(&mut self) -> T
|
||||||
where
|
where
|
||||||
T: Clone,
|
T: Clone,
|
||||||
{
|
{
|
||||||
let GenerationalValue { value, generation } = self.source.get();
|
let GenerationalValue { value, generation } = self.source.get().expect("deadlocked");
|
||||||
self.read_generation = generation;
|
self.read_generation = generation;
|
||||||
value
|
value
|
||||||
}
|
}
|
||||||
|
|
@ -492,19 +720,42 @@ impl<T> DynamicReader<T> {
|
||||||
/// there are no remaining writers for the value.
|
/// there are no remaining writers for the value.
|
||||||
///
|
///
|
||||||
/// Returns true if a newly updated value was discovered.
|
/// 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 {
|
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 {
|
loop {
|
||||||
|
let state = self
|
||||||
|
.source
|
||||||
|
.state
|
||||||
|
.lock()
|
||||||
|
.map_or_else(PoisonError::into_inner, |g| g);
|
||||||
if state.wrapped.generation != self.read_generation {
|
if state.wrapped.generation != self.read_generation {
|
||||||
return true;
|
return true;
|
||||||
} else if state.readers == Arc::strong_count(&self.source) {
|
} else if state.readers == Arc::strong_count(&self.source) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
drop(state);
|
||||||
|
|
||||||
state = self
|
// Wait for a notification of a change, which is synch
|
||||||
|
deadlock_state = self
|
||||||
.source
|
.source
|
||||||
.sync
|
.sync
|
||||||
.wait(state)
|
.wait(deadlock_state)
|
||||||
.map_or_else(PoisonError::into_inner, |g| g);
|
.map_or_else(PoisonError::into_inner, |g| g);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -520,7 +771,7 @@ impl<T> DynamicReader<T> {
|
||||||
|
|
||||||
impl<T> Clone for DynamicReader<T> {
|
impl<T> Clone for DynamicReader<T> {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
self.source.state().readers += 1;
|
self.source.state().expect("deadlocked").readers += 1;
|
||||||
Self {
|
Self {
|
||||||
source: self.source.clone(),
|
source: self.source.clone(),
|
||||||
read_generation: self.read_generation,
|
read_generation: self.read_generation,
|
||||||
|
|
@ -530,7 +781,7 @@ impl<T> Clone for DynamicReader<T> {
|
||||||
|
|
||||||
impl<T> Drop for DynamicReader<T> {
|
impl<T> Drop for DynamicReader<T> {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
let mut state = self.source.state();
|
let mut state = self.source.state().expect("deadlocked");
|
||||||
state.readers -= 1;
|
state.readers -= 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -547,7 +798,7 @@ impl<'a, T> Future for BlockUntilUpdatedFuture<'a, T> {
|
||||||
type Output = bool;
|
type Output = bool;
|
||||||
|
|
||||||
fn poll(self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
|
fn poll(self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
|
||||||
let mut state = self.0.source.state();
|
let mut state = self.0.source.state().expect("deadlocked");
|
||||||
if state.wrapped.generation != self.0.read_generation {
|
if state.wrapped.generation != self.0.read_generation {
|
||||||
return Poll::Ready(true);
|
return Poll::Ready(true);
|
||||||
} else if state.readers == Arc::strong_count(&self.0.source) {
|
} else if state.readers == Arc::strong_count(&self.0.source) {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@ use std::panic::UnwindSafe;
|
||||||
use kludgine::app::winit::event::{DeviceId, MouseButton};
|
use kludgine::app::winit::event::{DeviceId, MouseButton};
|
||||||
use kludgine::figures::units::{Lp, Px, UPx};
|
use kludgine::figures::units::{Lp, Px, UPx};
|
||||||
use kludgine::figures::{
|
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::shapes::Shape;
|
||||||
use kludgine::{Color, Origin};
|
use kludgine::{Color, Origin};
|
||||||
|
|
@ -70,6 +71,68 @@ impl<T> Slider<T> {
|
||||||
self.minimum = min.into_value();
|
self.minimum = min.into_value();
|
||||||
self
|
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<T> Slider<T>
|
impl<T> Slider<T>
|
||||||
|
|
@ -102,8 +165,10 @@ where
|
||||||
+ 'static,
|
+ 'static,
|
||||||
{
|
{
|
||||||
fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {
|
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 track_color = styles.get(&TrackColor, context);
|
||||||
|
let inactive_track_color = styles.get(&InactiveTrackColor, context);
|
||||||
let knob_color = styles.get(&KnobColor, context);
|
let knob_color = styles.get(&KnobColor, context);
|
||||||
let knob_size = self.knob_size.into_signed();
|
let knob_size = self.knob_size.into_signed();
|
||||||
let track_size = styles
|
let track_size = styles
|
||||||
|
|
@ -112,7 +177,6 @@ where
|
||||||
.min(knob_size);
|
.min(knob_size);
|
||||||
|
|
||||||
let half_knob = knob_size / 2;
|
let half_knob = knob_size / 2;
|
||||||
let half_track = track_size / 2;
|
|
||||||
|
|
||||||
let mut value = self.value.get_tracked(context);
|
let mut value = self.value.get_tracked(context);
|
||||||
let min = self.minimum.get_tracked(context);
|
let min = self.minimum.get_tracked(context);
|
||||||
|
|
@ -140,55 +204,19 @@ where
|
||||||
let size = context.gfx.region().size;
|
let size = context.gfx.region().size;
|
||||||
self.horizontal = size.width >= size.height;
|
self.horizontal = size.width >= size.height;
|
||||||
|
|
||||||
if self.horizontal {
|
self.draw_track(
|
||||||
self.rendered_size = size.width;
|
&TrackSpec {
|
||||||
// Draw the track
|
size,
|
||||||
context.gfx.draw_shape(
|
percent: *percent,
|
||||||
&Shape::filled_rect(
|
half_knob,
|
||||||
Rect::new(
|
knob_size,
|
||||||
Point::new(half_knob, half_knob - half_track),
|
track_size,
|
||||||
Size::new(size.width - knob_size, track_size),
|
knob_color,
|
||||||
),
|
|
||||||
track_color,
|
track_color,
|
||||||
),
|
inactive_track_color,
|
||||||
Point::default(),
|
},
|
||||||
None,
|
context,
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn layout(
|
fn layout(
|
||||||
|
|
@ -263,6 +291,29 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct TrackSpec {
|
||||||
|
size: Size<Px>,
|
||||||
|
percent: f32,
|
||||||
|
half_knob: Px,
|
||||||
|
knob_size: Px,
|
||||||
|
track_size: Px,
|
||||||
|
knob_color: Color,
|
||||||
|
track_color: Color,
|
||||||
|
inactive_track_color: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flipped<T, Unit>(flip: bool, value: T) -> T
|
||||||
|
where
|
||||||
|
T: IntoComponents<Unit> + FromComponents<Unit>,
|
||||||
|
{
|
||||||
|
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.
|
/// The size of the track that the knob of a [`Slider`] traversesq.
|
||||||
pub struct TrackSize;
|
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;
|
pub struct TrackColor;
|
||||||
|
|
||||||
impl ComponentDefinition for TrackColor {
|
impl ComponentDefinition for TrackColor {
|
||||||
type ComponentType = Color;
|
type ComponentType = Color;
|
||||||
|
|
||||||
fn default_value(&self, context: &WidgetContext<'_, '_>) -> Self::ComponentType {
|
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"))
|
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",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue