mirror of
https://github.com/danbulant/cushy
synced 2026-06-19 14:31:04 +00:00
Merge branch 'main' into button-fun
This commit is contained in:
commit
6220394df2
22 changed files with 1729 additions and 296 deletions
10
Cargo.lock
generated
10
Cargo.lock
generated
|
|
@ -105,7 +105,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "appit"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/khonsulabs/appit#043bfe2c78524d6a06ed159289ea1cd7a62b0fec"
|
||||
source = "git+https://github.com/khonsulabs/appit#5ed0d923ded6520950d14b3b869cbcac89452f5c"
|
||||
dependencies = [
|
||||
"raw-window-handle 0.5.2",
|
||||
"winit",
|
||||
|
|
@ -455,9 +455,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "etagere"
|
||||
version = "0.2.9"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5bf70b9ea3a235a7432b4f481854815e2d4fb2fe824c1f5fb09b8985dd06b3e9"
|
||||
checksum = "fcf22f748754352918e082e0039335ee92454a5d62bcaf69b5e8daf5907d9644"
|
||||
dependencies = [
|
||||
"euclid",
|
||||
"svg_fmt",
|
||||
|
|
@ -481,7 +481,7 @@ checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1"
|
|||
[[package]]
|
||||
name = "figures"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/khonsulabs/figures#f5b9ca5cf181b748897b269ad47d7a9f2d1f3eac"
|
||||
source = "git+https://github.com/khonsulabs/figures#7b41393c44d4def606790e340c98450b603010b4"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"euclid",
|
||||
|
|
@ -880,7 +880,7 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
|
|||
[[package]]
|
||||
name = "kludgine"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/khonsulabs/kludgine#a88961b726101ef9bb46bdae4737308d2dcb12a0"
|
||||
source = "git+https://github.com/khonsulabs/kludgine#a26299823498dccbbbb3c28abc820b660fcc1289"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"alot",
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ fn main() -> gooey::Result {
|
|||
Lp::points(300)..Lp::points(600),
|
||||
Stack::rows(username_row.and(password_row).and(buttons)),
|
||||
)
|
||||
.scroll()
|
||||
.centered()
|
||||
.expand()
|
||||
.run()
|
||||
|
|
|
|||
|
|
@ -5,5 +5,6 @@ use gooey::Run;
|
|||
fn main() -> gooey::Result {
|
||||
Label::new(include_str!("../src/widgets/scroll.rs"))
|
||||
.scroll()
|
||||
.expand()
|
||||
.run()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +1,128 @@
|
|||
use gooey::styles::components::TextColor;
|
||||
use gooey::styles::{ColorTheme, FixedTheme, InverseTheme, SurfaceTheme, Theme, ThemePair};
|
||||
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::LabelBackground;
|
||||
use gooey::widgets::{Label, Stack};
|
||||
use gooey::widgets::{Input, Label, Scroll, Slider, Stack, Themed};
|
||||
use gooey::window::ThemeMode;
|
||||
use gooey::Run;
|
||||
use kludgine::Color;
|
||||
|
||||
const PRIMARY_HUE: f32 = 240.;
|
||||
const SECONDARY_HUE: f32 = 0.;
|
||||
const TERTIARY_HUE: f32 = 330.;
|
||||
const ERROR_HUE: f32 = 30.;
|
||||
|
||||
fn main() -> gooey::Result {
|
||||
let default_theme = ThemePair::default();
|
||||
Stack::columns(
|
||||
theme(default_theme.dark, "Dark")
|
||||
.and(theme(default_theme.light, "Light"))
|
||||
let (primary, primary_editor) = color_editor(PRIMARY_HUE, 0.8, "Primary");
|
||||
let (secondary, secondary_editor) = color_editor(SECONDARY_HUE, 0.3, "Secondary");
|
||||
let (tertiary, tertiary_editor) = color_editor(TERTIARY_HUE, 0.3, "Tertiary");
|
||||
let (error, error_editor) = color_editor(ERROR_HUE, 0.8, "Error");
|
||||
let (neutral, neutral_editor) = color_editor(PRIMARY_HUE, 0.001, "Neutral");
|
||||
let (neutral_variant, neutral_variant_editor) =
|
||||
color_editor(PRIMARY_HUE, 0.001, "Neutral Variant");
|
||||
let (theme_mode, theme_switcher) = dark_mode_slider();
|
||||
|
||||
let default_theme = (
|
||||
&primary,
|
||||
&secondary,
|
||||
&tertiary,
|
||||
&error,
|
||||
&neutral,
|
||||
&neutral_variant,
|
||||
)
|
||||
.map_each(
|
||||
|(primary, secondary, tertiary, error, neutral, neutral_variant)| {
|
||||
ThemePair::from_sources(
|
||||
*primary,
|
||||
*secondary,
|
||||
*tertiary,
|
||||
*error,
|
||||
*neutral,
|
||||
*neutral_variant,
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
Themed::new(
|
||||
default_theme.clone(),
|
||||
Stack::columns(
|
||||
Scroll::vertical(Stack::rows(
|
||||
theme_switcher
|
||||
.and(primary_editor)
|
||||
.and(secondary_editor)
|
||||
.and(tertiary_editor)
|
||||
.and(error_editor)
|
||||
.and(neutral_editor)
|
||||
.and(neutral_variant_editor),
|
||||
))
|
||||
.and(theme(default_theme.map_each(|theme| theme.dark), "Dark"))
|
||||
.and(theme(default_theme.map_each(|theme| theme.light), "Light"))
|
||||
.and(fixed_themes(
|
||||
default_theme.primary_fixed,
|
||||
default_theme.secondary_fixed,
|
||||
default_theme.tertiary_fixed,
|
||||
default_theme.map_each(|theme| theme.primary_fixed),
|
||||
default_theme.map_each(|theme| theme.secondary_fixed),
|
||||
default_theme.map_each(|theme| theme.tertiary_fixed),
|
||||
)),
|
||||
),
|
||||
)
|
||||
.expand()
|
||||
.into_window()
|
||||
.with_theme_mode(theme_mode)
|
||||
.run()
|
||||
}
|
||||
|
||||
fn dark_mode_slider() -> (Dynamic<ThemeMode>, impl MakeWidget) {
|
||||
let theme_mode = Dynamic::default();
|
||||
|
||||
(
|
||||
theme_mode.clone(),
|
||||
Stack::rows(Label::new("Theme Mode").and(Slider::<ThemeMode>::from_value(theme_mode))),
|
||||
)
|
||||
}
|
||||
|
||||
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(
|
||||
initial_hue: f32,
|
||||
initial_saturation: impl Into<ZeroToOne>,
|
||||
label: &str,
|
||||
) -> (Dynamic<ColorSource>, impl MakeWidget) {
|
||||
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));
|
||||
|
||||
(
|
||||
color,
|
||||
Stack::rows(
|
||||
Label::new(label)
|
||||
.and(Slider::<f32>::new(hue, 0., 360.))
|
||||
.and(Input::new(hue_text))
|
||||
.and(Slider::<ZeroToOne>::from_value(saturation))
|
||||
.and(Input::new(saturation_text)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn fixed_themes(
|
||||
primary: FixedTheme,
|
||||
secondary: FixedTheme,
|
||||
tertiary: FixedTheme,
|
||||
primary: Dynamic<FixedTheme>,
|
||||
secondary: Dynamic<FixedTheme>,
|
||||
tertiary: Dynamic<FixedTheme>,
|
||||
) -> impl MakeWidget {
|
||||
Stack::rows(
|
||||
Label::new("Fixed")
|
||||
|
|
@ -35,85 +133,118 @@ fn fixed_themes(
|
|||
.expand()
|
||||
}
|
||||
|
||||
fn fixed_theme(theme: FixedTheme, label: &str) -> impl MakeWidget {
|
||||
fn fixed_theme(theme: Dynamic<FixedTheme>, label: &str) -> impl MakeWidget {
|
||||
let color = theme.map_each(|theme| theme.color);
|
||||
let on_color = theme.map_each(|theme| theme.on_color);
|
||||
Stack::columns(
|
||||
swatch(theme.color, &format!("{label} Fixed"), theme.on_color)
|
||||
swatch(color.clone(), &format!("{label} Fixed"), on_color.clone())
|
||||
.and(swatch(
|
||||
theme.dim_color,
|
||||
theme.map_each(|theme| theme.dim_color),
|
||||
&format!("Dim {label}"),
|
||||
theme.on_color,
|
||||
on_color.clone(),
|
||||
))
|
||||
.and(swatch(
|
||||
theme.on_color,
|
||||
on_color.clone(),
|
||||
&format!("On {label} Fixed"),
|
||||
theme.color,
|
||||
color.clone(),
|
||||
))
|
||||
.and(swatch(
|
||||
theme.on_color_variant,
|
||||
theme.map_each(|theme| theme.on_color_variant),
|
||||
&format!("Variant On {label} Fixed"),
|
||||
theme.color,
|
||||
color,
|
||||
)),
|
||||
)
|
||||
.expand()
|
||||
}
|
||||
|
||||
fn theme(theme: Theme, label: &str) -> impl MakeWidget {
|
||||
fn theme(theme: Dynamic<Theme>, label: &str) -> impl MakeWidget {
|
||||
Stack::rows(
|
||||
Label::new(label)
|
||||
.and(
|
||||
Stack::columns(
|
||||
color_theme(theme.primary, "Primary")
|
||||
.and(color_theme(theme.secondary, "Secondary"))
|
||||
.and(color_theme(theme.tertiary, "Tertiary"))
|
||||
.and(color_theme(theme.error, "Error")),
|
||||
color_theme(theme.map_each(|theme| theme.primary), "Primary")
|
||||
.and(color_theme(
|
||||
theme.map_each(|theme| theme.secondary),
|
||||
"Secondary",
|
||||
))
|
||||
.and(color_theme(
|
||||
theme.map_each(|theme| theme.tertiary),
|
||||
"Tertiary",
|
||||
))
|
||||
.and(color_theme(theme.map_each(|theme| theme.error), "Error")),
|
||||
)
|
||||
.expand(),
|
||||
)
|
||||
.and(surface_and_inverse_themes(theme.surface, theme.inverse)),
|
||||
.and(surface_theme(theme.map_each(|theme| theme.surface))),
|
||||
)
|
||||
.expand()
|
||||
}
|
||||
|
||||
fn surface_and_inverse_themes(theme: SurfaceTheme, inverse: InverseTheme) -> impl MakeWidget {
|
||||
fn surface_theme(theme: Dynamic<SurfaceTheme>) -> impl MakeWidget {
|
||||
let color = theme.map_each(|theme| theme.color);
|
||||
let on_color = theme.map_each(|theme| theme.on_color);
|
||||
Stack::rows(
|
||||
Stack::columns(
|
||||
swatch(theme.color, "Surface", theme.on_color)
|
||||
.and(swatch(theme.dim_color, "Dim Surface", theme.on_color))
|
||||
.and(swatch(theme.bright_color, "Bright Surface", theme.on_color)),
|
||||
swatch(color.clone(), "Surface", on_color.clone())
|
||||
.and(swatch(
|
||||
theme.map_each(|theme| theme.dim_color),
|
||||
"Dim Surface",
|
||||
on_color.clone(),
|
||||
))
|
||||
.and(swatch(
|
||||
theme.map_each(|theme| theme.bright_color),
|
||||
"Bright Surface",
|
||||
on_color.clone(),
|
||||
)),
|
||||
)
|
||||
.expand()
|
||||
.and(inverse_theme(inverse))
|
||||
.and(
|
||||
Stack::columns(
|
||||
swatch(theme.lowest_container, "Lowest Container", theme.on_color)
|
||||
.and(swatch(theme.low_container, "Low Container", theme.on_color))
|
||||
.and(swatch(theme.container, "Container", theme.on_color))
|
||||
.and(swatch(
|
||||
theme.high_container,
|
||||
"High Container",
|
||||
theme.on_color,
|
||||
))
|
||||
.and(swatch(
|
||||
theme.highest_container,
|
||||
"Highest Container",
|
||||
theme.on_color,
|
||||
)),
|
||||
swatch(
|
||||
theme.map_each(|theme| theme.lowest_container),
|
||||
"Lowest Container",
|
||||
on_color.clone(),
|
||||
)
|
||||
.and(swatch(
|
||||
theme.map_each(|theme| theme.low_container),
|
||||
"Low Container",
|
||||
on_color.clone(),
|
||||
))
|
||||
.and(swatch(
|
||||
theme.map_each(|theme| theme.container),
|
||||
"Container",
|
||||
on_color.clone(),
|
||||
))
|
||||
.and(swatch(
|
||||
theme.map_each(|theme| theme.high_container),
|
||||
"High Container",
|
||||
on_color.clone(),
|
||||
))
|
||||
.and(swatch(
|
||||
theme.map_each(|theme| theme.highest_container),
|
||||
"Highest Container",
|
||||
on_color.clone(),
|
||||
)),
|
||||
)
|
||||
.expand(),
|
||||
)
|
||||
.and(
|
||||
Stack::columns(
|
||||
swatch(theme.on_color, "On Surface", theme.color)
|
||||
swatch(on_color.clone(), "On Surface", color.clone())
|
||||
.and(swatch(
|
||||
theme.on_color_variant,
|
||||
theme.map_each(|theme| theme.on_color_variant),
|
||||
"On Color Variant",
|
||||
theme.color,
|
||||
color.clone(),
|
||||
))
|
||||
.and(swatch(theme.outline, "Outline", theme.color))
|
||||
.and(swatch(
|
||||
theme.outline_variant,
|
||||
theme.map_each(|theme| theme.outline),
|
||||
"Outline",
|
||||
color.clone(),
|
||||
))
|
||||
.and(swatch(
|
||||
theme.map_each(|theme| theme.outline_variant),
|
||||
"Outline Variant",
|
||||
theme.color,
|
||||
color,
|
||||
)),
|
||||
)
|
||||
.expand(),
|
||||
|
|
@ -122,42 +253,37 @@ fn surface_and_inverse_themes(theme: SurfaceTheme, inverse: InverseTheme) -> imp
|
|||
.expand()
|
||||
}
|
||||
|
||||
fn inverse_theme(theme: InverseTheme) -> impl MakeWidget {
|
||||
Stack::columns(
|
||||
swatch(theme.surface, "Inverse Surface", theme.on_surface)
|
||||
.and(swatch(
|
||||
theme.on_surface,
|
||||
"On Inverse Surface",
|
||||
theme.surface,
|
||||
))
|
||||
.and(swatch(theme.primary, "Inverse Primary", theme.surface)),
|
||||
)
|
||||
.expand()
|
||||
}
|
||||
|
||||
fn color_theme(theme: ColorTheme, label: &str) -> impl MakeWidget {
|
||||
fn color_theme(theme: Dynamic<ColorTheme>, label: &str) -> impl MakeWidget {
|
||||
let color = theme.map_each(|theme| theme.color);
|
||||
let on_color = theme.map_each(|theme| theme.on_color);
|
||||
let container = theme.map_each(|theme| theme.container);
|
||||
let on_container = theme.map_each(|theme| theme.on_container);
|
||||
Stack::rows(
|
||||
swatch(theme.color, label, theme.on_color)
|
||||
.and(swatch(theme.on_color, &format!("On {label}"), theme.color))
|
||||
swatch(color.clone(), label, on_color.clone())
|
||||
.and(swatch(
|
||||
theme.container,
|
||||
&format!("{label} Container"),
|
||||
theme.on_container,
|
||||
on_color.clone(),
|
||||
&format!("On {label}"),
|
||||
color.clone(),
|
||||
))
|
||||
.and(swatch(
|
||||
theme.on_container,
|
||||
container.clone(),
|
||||
&format!("{label} Container"),
|
||||
on_container.clone(),
|
||||
))
|
||||
.and(swatch(
|
||||
on_container,
|
||||
&format!("On {label} Container"),
|
||||
theme.container,
|
||||
container,
|
||||
)),
|
||||
)
|
||||
.expand()
|
||||
}
|
||||
|
||||
fn swatch(background: Color, label: &str, text: Color) -> impl MakeWidget {
|
||||
fn swatch(background: Dynamic<Color>, label: &str, text: Dynamic<Color>) -> impl MakeWidget {
|
||||
Label::new(label)
|
||||
.with(&TextColor, text)
|
||||
.with(&WidgetBackground, background)
|
||||
.fit_horizontally()
|
||||
.fit_vertically()
|
||||
.with(&TextColor, text)
|
||||
.with(&LabelBackground, background)
|
||||
.expand()
|
||||
}
|
||||
|
|
|
|||
136
src/animation.rs
136
src/animation.rs
|
|
@ -39,15 +39,18 @@
|
|||
|
||||
pub mod easings;
|
||||
|
||||
use std::fmt::Debug;
|
||||
use std::ops::{ControlFlow, Deref};
|
||||
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};
|
||||
|
||||
use alot::{LotId, Lots};
|
||||
use intentional::Cast;
|
||||
use kempt::Set;
|
||||
use kludgine::figures::Ranged;
|
||||
use kludgine::Color;
|
||||
|
||||
use crate::animation::easings::Linear;
|
||||
|
|
@ -672,6 +675,26 @@ impl LinearInterpolate for f64 {
|
|||
}
|
||||
}
|
||||
|
||||
impl LinearInterpolate for bool {
|
||||
fn lerp(&self, target: &Self, percent: f32) -> Self {
|
||||
if percent >= 0.5 {
|
||||
*target
|
||||
} else {
|
||||
*self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PercentBetween for bool {
|
||||
fn percent_between(&self, min: &Self, max: &Self) -> ZeroToOne {
|
||||
if *min == *max || *self == *min {
|
||||
ZeroToOne::ZERO
|
||||
} else {
|
||||
ZeroToOne::ONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integer_lerps() {
|
||||
#[track_caller]
|
||||
|
|
@ -703,6 +726,56 @@ impl LinearInterpolate for Color {
|
|||
}
|
||||
}
|
||||
|
||||
/// Calculates the ratio of one value against a minimum and maximum.
|
||||
pub trait PercentBetween {
|
||||
/// Return the percentage that `self` is between `min` and `max`.
|
||||
fn percent_between(&self, min: &Self, max: &Self) -> ZeroToOne;
|
||||
}
|
||||
|
||||
macro_rules! impl_percent_between {
|
||||
($type:ident, $float:ident) => {
|
||||
impl PercentBetween for $type {
|
||||
fn percent_between(&self, min: &Self, max: &Self) -> ZeroToOne {
|
||||
let range = *max - *min;
|
||||
ZeroToOne::from(*self as $float / range as $float)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_percent_between!(u8, f32);
|
||||
impl_percent_between!(u16, f32);
|
||||
impl_percent_between!(u32, f32);
|
||||
impl_percent_between!(u64, f32);
|
||||
impl_percent_between!(u128, f64);
|
||||
impl_percent_between!(usize, f64);
|
||||
impl_percent_between!(i8, f32);
|
||||
impl_percent_between!(i16, f32);
|
||||
impl_percent_between!(i32, f32);
|
||||
impl_percent_between!(i64, f32);
|
||||
impl_percent_between!(i128, f64);
|
||||
impl_percent_between!(isize, f64);
|
||||
impl_percent_between!(f32, f32);
|
||||
impl_percent_between!(f64, f64);
|
||||
|
||||
impl PercentBetween for Color {
|
||||
fn percent_between(&self, min: &Self, max: &Self) -> ZeroToOne {
|
||||
fn channel_percent(
|
||||
value: Color,
|
||||
min: Color,
|
||||
max: Color,
|
||||
func: impl Fn(Color) -> u8,
|
||||
) -> ZeroToOne {
|
||||
func(value).percent_between(&func(min), &func(max))
|
||||
}
|
||||
|
||||
channel_percent(*self, *min, *max, Color::red)
|
||||
* channel_percent(*self, *min, *max, Color::green)
|
||||
* channel_percent(*self, *min, *max, Color::blue)
|
||||
* channel_percent(*self, *min, *max, Color::alpha)
|
||||
}
|
||||
}
|
||||
|
||||
/// An `f32` that is clamped between 0.0 and 1.0 and cannot be NaN or Infinity.
|
||||
///
|
||||
/// Because of these restrictions, this type implements `Ord` and `Eq`.
|
||||
|
|
@ -727,6 +800,12 @@ impl ZeroToOne {
|
|||
Self(value.clamp(0., 1.))
|
||||
}
|
||||
|
||||
/// Returns the difference between `self` and `other` as a positive number.
|
||||
#[must_use]
|
||||
pub fn difference_between(self, other: Self) -> Self {
|
||||
Self((self.0 - other.0).abs())
|
||||
}
|
||||
|
||||
/// Returns the contained floating point value.
|
||||
#[must_use]
|
||||
pub fn into_f32(self) -> f32 {
|
||||
|
|
@ -734,6 +813,32 @@ 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 {
|
||||
fn from(value: f32) -> Self {
|
||||
Self::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f64> for ZeroToOne {
|
||||
fn from(value: f64) -> Self {
|
||||
Self::new(value.cast())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ZeroToOne {
|
||||
fn default() -> Self {
|
||||
Self::ZERO
|
||||
|
|
@ -787,6 +892,33 @@ impl LinearInterpolate for ZeroToOne {
|
|||
}
|
||||
}
|
||||
|
||||
impl PercentBetween for ZeroToOne {
|
||||
fn percent_between(&self, min: &Self, max: &Self) -> ZeroToOne {
|
||||
self.0.percent_between(&min.0, &max.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul for ZeroToOne {
|
||||
type Output = Self;
|
||||
|
||||
fn mul(self, rhs: Self) -> Self::Output {
|
||||
Self(self.0 * rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Div for ZeroToOne {
|
||||
type Output = Self;
|
||||
|
||||
fn div(self, rhs: Self) -> Self::Output {
|
||||
Self(self.0 / rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for ZeroToOne {
|
||||
const MAX: Self = Self::ONE;
|
||||
const MIN: Self = Self::ZERO;
|
||||
}
|
||||
|
||||
/// An easing function for customizing animations.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum EasingFunction {
|
||||
|
|
|
|||
113
src/context.rs
113
src/context.rs
|
|
@ -1,4 +1,5 @@
|
|||
//! Types that provide access to the Gooey runtime.
|
||||
use std::borrow::Cow;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
|
@ -6,19 +7,18 @@ use std::sync::Arc;
|
|||
use kludgine::app::winit::event::{
|
||||
DeviceId, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase,
|
||||
};
|
||||
use kludgine::app::winit::window;
|
||||
use kludgine::figures::units::{Px, UPx};
|
||||
use kludgine::figures::{IntoSigned, Point, Rect, Size};
|
||||
use kludgine::figures::units::{Lp, Px, UPx};
|
||||
use kludgine::figures::{IntoSigned, Point, Rect, ScreenScale, Size};
|
||||
use kludgine::shapes::{Shape, StrokeOptions};
|
||||
use kludgine::Kludgine;
|
||||
use kludgine::{Color, Kludgine};
|
||||
|
||||
use crate::graphics::Graphics;
|
||||
use crate::styles::components::{HighlightColor, VisualOrder};
|
||||
use crate::styles::components::{HighlightColor, VisualOrder, WidgetBackground};
|
||||
use crate::styles::{ComponentDefaultvalue, ComponentDefinition, Styles, Theme, ThemePair};
|
||||
use crate::value::Dynamic;
|
||||
use crate::value::{Dynamic, Value};
|
||||
use crate::widget::{EventHandling, ManagedWidget, WidgetId, WidgetInstance, WidgetRef};
|
||||
use crate::window::sealed::WindowCommand;
|
||||
use crate::window::RunningWindow;
|
||||
use crate::window::{RunningWindow, ThemeMode};
|
||||
use crate::ConstraintLimit;
|
||||
|
||||
/// A context to an event function.
|
||||
|
|
@ -349,6 +349,9 @@ impl<'context, 'window> EventContext<'context, 'window> {
|
|||
///
|
||||
/// This widget does not need to be focused.
|
||||
pub fn advance_focus(&mut self, direction: VisualOrder) {
|
||||
// TODO check to see if the current node has an explicit next_focus (or
|
||||
// if we're going in the opposite direction, previous_focus).
|
||||
|
||||
self.pending_state.focus = self.next_focus_after(self.current_node.clone(), direction);
|
||||
}
|
||||
}
|
||||
|
|
@ -447,6 +450,18 @@ impl<'context, 'window, 'clip, 'gfx, 'pass> GraphicsContext<'context, 'window, '
|
|||
}
|
||||
}
|
||||
|
||||
/// Strokes an outline around this widget's contents.
|
||||
pub fn stroke_outline<Unit>(&mut self, color: Color, options: StrokeOptions<Unit>)
|
||||
where
|
||||
Unit: ScreenScale<Px = Px, Lp = Lp>,
|
||||
{
|
||||
let visible_rect = Rect::from(self.gfx.region().size - (Px(1), Px(1)));
|
||||
let focus_ring =
|
||||
Shape::stroked_rect(visible_rect, color, options.into_px(self.gfx.scale()));
|
||||
self.gfx
|
||||
.draw_shape(&focus_ring, Point::default(), None, None);
|
||||
}
|
||||
|
||||
/// Renders the default focus ring for this widget.
|
||||
///
|
||||
/// To ensure the correct color is used, include [`HighlightColor`] in the
|
||||
|
|
@ -457,14 +472,8 @@ impl<'context, 'window, 'clip, 'gfx, 'pass> GraphicsContext<'context, 'window, '
|
|||
return;
|
||||
}
|
||||
|
||||
let visible_rect = Rect::from(self.gfx.region().size - (Px(1), Px(1)));
|
||||
let focus_ring = Shape::stroked_rect(
|
||||
visible_rect,
|
||||
styles.get(&HighlightColor, self),
|
||||
StrokeOptions::default(),
|
||||
);
|
||||
self.gfx
|
||||
.draw_shape(&focus_ring, Point::default(), None, None);
|
||||
let color = styles.get(&HighlightColor, self);
|
||||
self.stroke_outline::<Lp>(color, StrokeOptions::default());
|
||||
}
|
||||
|
||||
/// Renders the default focus ring for this widget.
|
||||
|
|
@ -485,6 +494,9 @@ impl<'context, 'window, 'clip, 'gfx, 'pass> GraphicsContext<'context, 'window, '
|
|||
"redraw called without set_widget_layout"
|
||||
);
|
||||
|
||||
let background = self.query_style(&WidgetBackground);
|
||||
self.gfx.fill(background);
|
||||
|
||||
self.current_node
|
||||
.tree
|
||||
.note_widget_rendered(self.current_node.id());
|
||||
|
|
@ -662,8 +674,9 @@ pub struct WidgetContext<'context, 'window> {
|
|||
current_node: ManagedWidget,
|
||||
redraw_status: &'context RedrawStatus,
|
||||
window: &'context mut RunningWindow<'window>,
|
||||
theme: &'context ThemePair,
|
||||
theme: Cow<'context, ThemePair>,
|
||||
pending_state: PendingState<'context>,
|
||||
theme_mode: ThemeMode,
|
||||
}
|
||||
|
||||
impl<'context, 'window> WidgetContext<'context, 'window> {
|
||||
|
|
@ -672,6 +685,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
|
|||
redraw_status: &'context RedrawStatus,
|
||||
theme: &'context ThemePair,
|
||||
window: &'context mut RunningWindow<'window>,
|
||||
theme_mode: ThemeMode,
|
||||
) -> Self {
|
||||
Self {
|
||||
pending_state: PendingState::Owned(PendingWidgetState {
|
||||
|
|
@ -686,7 +700,8 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
|
|||
}),
|
||||
current_node,
|
||||
redraw_status,
|
||||
theme,
|
||||
theme: Cow::Borrowed(theme),
|
||||
theme_mode,
|
||||
window,
|
||||
}
|
||||
}
|
||||
|
|
@ -697,8 +712,9 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
|
|||
current_node: self.current_node.clone(),
|
||||
redraw_status: self.redraw_status,
|
||||
window: &mut *self.window,
|
||||
theme: self.theme,
|
||||
theme: Cow::Borrowed(self.theme.as_ref()),
|
||||
pending_state: self.pending_state.borrowed(),
|
||||
theme_mode: self.theme_mode,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -711,12 +727,26 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
|
|||
Widget: ManageWidget,
|
||||
Widget::Managed: MapManagedWidget<WidgetContext<'child, 'window>>,
|
||||
{
|
||||
widget.manage(self).map(|current_node| WidgetContext {
|
||||
current_node,
|
||||
redraw_status: self.redraw_status,
|
||||
window: &mut *self.window,
|
||||
theme: self.theme,
|
||||
pending_state: self.pending_state.borrowed(),
|
||||
widget.manage(self).map(|current_node| {
|
||||
let (theme, theme_mode) = current_node.overidden_theme();
|
||||
let theme = if let Some(theme) = theme {
|
||||
Cow::Owned(theme.get_tracked(self))
|
||||
} else {
|
||||
Cow::Borrowed(self.theme.as_ref())
|
||||
};
|
||||
let theme_mode = if let Some(mode) = theme_mode {
|
||||
mode.get_tracked(self)
|
||||
} else {
|
||||
self.theme_mode
|
||||
};
|
||||
WidgetContext {
|
||||
current_node,
|
||||
redraw_status: self.redraw_status,
|
||||
window: &mut *self.window,
|
||||
theme,
|
||||
pending_state: self.pending_state.borrowed(),
|
||||
theme_mode,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -860,10 +890,24 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
|
|||
///
|
||||
/// Style queries for children will return any values matching this
|
||||
/// collection.
|
||||
pub fn attach_styles(&self, styles: Styles) {
|
||||
pub fn attach_styles(&self, styles: Value<Styles>) {
|
||||
self.current_node.attach_styles(styles);
|
||||
}
|
||||
|
||||
/// Attaches `theme` to the widget hierarchy for this widget.
|
||||
///
|
||||
/// All children nodes will access this theme in their contexts.
|
||||
pub fn attach_theme(&self, theme: Value<ThemePair>) {
|
||||
self.current_node.attach_theme(theme);
|
||||
}
|
||||
|
||||
/// Attaches `theme_mode` to the widget hierarchy for this widget.
|
||||
///
|
||||
/// All children nodes will use this theme mode.
|
||||
pub fn attach_theme_mode(&self, theme_mode: Value<ThemeMode>) {
|
||||
self.current_node.attach_theme_mode(theme_mode);
|
||||
}
|
||||
|
||||
/// Queries the widget hierarchy for matching style components.
|
||||
///
|
||||
/// This function traverses up the widget hierarchy looking for the
|
||||
|
|
@ -878,7 +922,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
|
|||
pub fn query_styles(&self, query: &[&dyn ComponentDefaultvalue]) -> Styles {
|
||||
self.current_node
|
||||
.tree
|
||||
.query_styles(&self.current_node, query)
|
||||
.query_styles(&self.current_node, query, self)
|
||||
}
|
||||
|
||||
/// Queries the widget hierarchy for a single style component.
|
||||
|
|
@ -919,15 +963,24 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
|
|||
/// Returns the theme pair for the window.
|
||||
#[must_use]
|
||||
pub fn theme_pair(&self) -> &ThemePair {
|
||||
self.theme
|
||||
self.theme.as_ref()
|
||||
}
|
||||
|
||||
/// Returns the current theme in either light or dark mode.
|
||||
#[must_use]
|
||||
pub fn theme(&self) -> &Theme {
|
||||
match self.window.theme() {
|
||||
window::Theme::Light => &self.theme.light,
|
||||
window::Theme::Dark => &self.theme.dark,
|
||||
match self.theme_mode {
|
||||
ThemeMode::Light => &self.theme.light,
|
||||
ThemeMode::Dark => &self.theme.dark,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the opposite theme of [`Self::theme()`].
|
||||
#[must_use]
|
||||
pub fn inverse_theme(&self) -> &Theme {
|
||||
match self.theme_mode {
|
||||
ThemeMode::Light => &self.theme.dark,
|
||||
ThemeMode::Dark => &self.theme.light,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,10 +48,10 @@ impl ConstraintLimit {
|
|||
}
|
||||
|
||||
/// Converts `measured` to unsigned pixels, and adjusts it according to the
|
||||
/// contraint's intentions.
|
||||
/// constraint's intentions.
|
||||
///
|
||||
/// If this constraint is of a known size, it will return the maximum of the
|
||||
/// measured size and the contraint. If it is of an unknown size, it will
|
||||
/// measured size and the constraint. If it is of an unknown size, it will
|
||||
/// return the measured size.
|
||||
pub fn fit_measured<Unit>(self, measured: Unit, scale: Fraction) -> UPx
|
||||
where
|
||||
|
|
|
|||
163
src/styles.rs
163
src/styles.rs
|
|
@ -459,7 +459,7 @@ where
|
|||
fn from(value: RangeInclusive<T>) -> Self {
|
||||
Self {
|
||||
start: Bound::Included(value.start().clone().into()),
|
||||
end: Bound::Excluded(value.end().clone().into()),
|
||||
end: Bound::Included(value.end().clone().into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -572,9 +572,15 @@ where
|
|||
pub struct Group(Name);
|
||||
|
||||
impl Group {
|
||||
/// Returns a new group with `name`.
|
||||
#[must_use]
|
||||
pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
|
||||
Self(Name::new(name))
|
||||
}
|
||||
|
||||
/// Returns a new instance using the group name of `T`.
|
||||
#[must_use]
|
||||
pub fn new<T>() -> Self
|
||||
pub fn from_group<T>() -> Self
|
||||
where
|
||||
T: ComponentGroup,
|
||||
{
|
||||
|
|
@ -626,7 +632,7 @@ impl ComponentName {
|
|||
|
||||
/// Returns a new instance using `G` and `name`.
|
||||
pub fn named<G: ComponentGroup>(name: impl Into<Name>) -> Self {
|
||||
Self::new(Group::new::<G>(), name)
|
||||
Self::new(Group::from_group::<G>(), name)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -921,9 +927,6 @@ pub struct Theme {
|
|||
|
||||
/// The theme to color surfaces.
|
||||
pub surface: SurfaceTheme,
|
||||
|
||||
/// A theme of inverse colors to provide high contrast to other elements.
|
||||
pub inverse: InverseTheme,
|
||||
}
|
||||
|
||||
impl Theme {
|
||||
|
|
@ -943,7 +946,6 @@ impl Theme {
|
|||
tertiary: ColorTheme::light_from_source(tertiary),
|
||||
error: ColorTheme::light_from_source(error),
|
||||
surface: SurfaceTheme::light_from_sources(neutral, neutral_variant),
|
||||
inverse: InverseTheme::light_from_sources(primary, neutral),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -963,7 +965,6 @@ impl Theme {
|
|||
tertiary: ColorTheme::dark_from_source(tertiary),
|
||||
error: ColorTheme::dark_from_source(error),
|
||||
surface: SurfaceTheme::dark_from_sources(neutral, neutral_variant),
|
||||
inverse: InverseTheme::dark_from_sources(primary, neutral),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1106,40 +1107,6 @@ impl FixedTheme {
|
|||
}
|
||||
}
|
||||
|
||||
/// An inverse color theme for displaying highly contrasted elements.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct InverseTheme {
|
||||
/// An inverse surface color.
|
||||
pub surface: Color,
|
||||
/// The default color for content atop an inverted surface.
|
||||
pub on_surface: Color,
|
||||
/// The inverted primary color.
|
||||
pub primary: Color,
|
||||
// TODO why not inverse for the other colorthemes?
|
||||
}
|
||||
|
||||
impl InverseTheme {
|
||||
/// Returns the light-mode, inverse theme for given sources.
|
||||
#[must_use]
|
||||
pub fn light_from_sources(primary: ColorSource, surface: ColorSource) -> Self {
|
||||
Self {
|
||||
surface: surface.color(30),
|
||||
on_surface: surface.color(90),
|
||||
primary: primary.color(80),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the dark-mode, inverse theme for given sources.
|
||||
#[must_use]
|
||||
pub fn dark_from_sources(primary: ColorSource, surface: ColorSource) -> Self {
|
||||
Self {
|
||||
surface: surface.color(90),
|
||||
on_surface: surface.color(10),
|
||||
primary: primary.color(40),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A source for [`Color`]s.
|
||||
///
|
||||
/// This type is a combination of an [`OklabHue`] and a saturation ranging from
|
||||
|
|
@ -1170,10 +1137,10 @@ impl ColorSource {
|
|||
/// Returns a new source with the given hue (in degrees) and saturation (0.0
|
||||
/// - 1.0).
|
||||
#[must_use]
|
||||
pub fn new(hue: f32, saturation: f32) -> Self {
|
||||
pub fn new(hue: impl Into<OklabHue>, saturation: impl Into<ZeroToOne>) -> Self {
|
||||
Self {
|
||||
hue: OklabHue::new(hue),
|
||||
saturation: ZeroToOne::new(saturation),
|
||||
hue: hue.into(),
|
||||
saturation: saturation.into(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1184,6 +1151,32 @@ impl ColorSource {
|
|||
Okhsl::new(self.hue, *self.saturation, *lightness.into_lightness()).into_color();
|
||||
Color::new_f32(rgb.red, rgb.blue, rgb.green, 1.0)
|
||||
}
|
||||
|
||||
/// Calculates an approximate ratio between 0.0 and 1.0 of how contrasting
|
||||
/// these colors are, with perfect constrast being two clors that are
|
||||
/// opposite of each other on the hue circle and one fully desaturated and
|
||||
/// the other fully saturated.
|
||||
#[must_use]
|
||||
pub fn contrast_between(self, other: Self) -> ZeroToOne {
|
||||
let saturation_delta = self.saturation.difference_between(other.saturation);
|
||||
let self_hue = self.hue.into_positive_degrees();
|
||||
let other_hue = other.hue.into_positive_degrees();
|
||||
// Calculate the shortest distance between the hues, taking into account
|
||||
// that 0 and 359 are one degree apart.
|
||||
let hue_delta = ZeroToOne::new(
|
||||
if self_hue < other_hue {
|
||||
let hue_delta_a = other_hue - self_hue;
|
||||
let hue_delta_b = self_hue + 360. - other_hue;
|
||||
hue_delta_a.min(hue_delta_b)
|
||||
} else {
|
||||
let hue_delta_a = self_hue - other_hue;
|
||||
let hue_delta_b = other_hue + 360. - self_hue;
|
||||
hue_delta_a.min(hue_delta_b)
|
||||
} / 180.,
|
||||
);
|
||||
|
||||
saturation_delta * hue_delta
|
||||
}
|
||||
}
|
||||
|
||||
/// A value that can represent the lightness of a color.
|
||||
|
|
@ -1231,29 +1224,31 @@ pub trait ColorExt: Copy {
|
|||
self.into_source_and_lightness().1
|
||||
}
|
||||
|
||||
/// Returns the contrast between this color and the components provided.
|
||||
///
|
||||
/// To achieve a contrast of 1.0:
|
||||
///
|
||||
/// - `self`'s hue and `check_source.hue` must be 180 degrees apart.
|
||||
/// - `self`'s saturation and `check_source.saturation` must be different by
|
||||
/// 1.0.
|
||||
/// - `self`'s lightness and `check_lightness` must be different by 1.0.
|
||||
/// - `self`'s alpha and `check_alpha` must be different by 1.0.
|
||||
///
|
||||
/// The algorithm currently used is purposely left undocumented as it will
|
||||
/// likely change. It should be a reasonable heuristic until someone smarter
|
||||
/// than @ecton comes along.
|
||||
fn contrast_between(
|
||||
self,
|
||||
check_source: ColorSource,
|
||||
check_lightness: ZeroToOne,
|
||||
check_alpha: ZeroToOne,
|
||||
) -> ZeroToOne;
|
||||
|
||||
/// Returns the color in `others` that contrasts the most from `self`.
|
||||
#[must_use]
|
||||
fn most_contrasting(self, others: &[Self]) -> Self
|
||||
where
|
||||
Self: Copy,
|
||||
{
|
||||
// TODO this currently only checks lightness. We should probably factor
|
||||
// in hue/saturation changes too.
|
||||
let check = self.lightness();
|
||||
|
||||
let mut others = others.iter().copied();
|
||||
let mut most_contrasting = others.next().expect("at least one comparison");
|
||||
let mut most_contrast_amount = (*most_contrasting.lightness() - *check).abs();
|
||||
for other in others {
|
||||
let contrast_amount = (*other.lightness() - *check).abs();
|
||||
if contrast_amount > most_contrast_amount {
|
||||
most_contrasting = other;
|
||||
most_contrast_amount = contrast_amount;
|
||||
}
|
||||
}
|
||||
|
||||
most_contrasting
|
||||
}
|
||||
Self: Copy;
|
||||
}
|
||||
|
||||
impl ColorExt for Color {
|
||||
|
|
@ -1268,4 +1263,44 @@ impl ColorExt for Color {
|
|||
ZeroToOne::new(hsl.lightness * self.alpha_f32()),
|
||||
)
|
||||
}
|
||||
|
||||
fn contrast_between(
|
||||
self,
|
||||
check_source: ColorSource,
|
||||
check_lightness: ZeroToOne,
|
||||
check_alpha: ZeroToOne,
|
||||
) -> ZeroToOne {
|
||||
let (other_source, other_lightness) = self.into_source_and_lightness();
|
||||
let lightness_delta = other_lightness.difference_between(check_lightness);
|
||||
|
||||
let source_change = check_source.contrast_between(other_source);
|
||||
|
||||
let other_alpha = ZeroToOne::new(self.alpha_f32());
|
||||
let alpha_delta = check_alpha.difference_between(other_alpha);
|
||||
|
||||
lightness_delta * source_change * alpha_delta
|
||||
}
|
||||
|
||||
fn most_contrasting(self, others: &[Self]) -> Self
|
||||
where
|
||||
Self: Copy,
|
||||
{
|
||||
let (check_source, check_lightness) = self.into_source_and_lightness();
|
||||
let check_alpha = ZeroToOne::new(self.alpha_f32());
|
||||
|
||||
let mut others = others.iter().copied();
|
||||
let mut most_contrasting = others.next().expect("at least one comparison");
|
||||
let mut most_contrast_amount =
|
||||
most_contrasting.contrast_between(check_source, check_lightness, check_alpha);
|
||||
for other in others {
|
||||
let contrast_amount =
|
||||
other.contrast_between(check_source, check_lightness, check_alpha);
|
||||
if contrast_amount > most_contrast_amount {
|
||||
most_contrasting = other;
|
||||
most_contrast_amount = contrast_amount;
|
||||
}
|
||||
}
|
||||
|
||||
most_contrasting
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -422,3 +422,57 @@ impl FocusableWidgets {
|
|||
matches!(self, Self::OnlyTextual)
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`Color`] to be used as a highlight color.
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
|
||||
pub struct WidgetBackground;
|
||||
|
||||
impl NamedComponent for WidgetBackground {
|
||||
fn name(&self) -> Cow<'_, ComponentName> {
|
||||
Cow::Owned(ComponentName::named::<Global>("widget_background_color"))
|
||||
}
|
||||
}
|
||||
|
||||
impl ComponentDefinition for WidgetBackground {
|
||||
type ComponentType = Color;
|
||||
|
||||
fn default_value(&self, _context: &WidgetContext<'_, '_>) -> Color {
|
||||
Color::CLEAR_WHITE
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`Color`] to be used as an outline color.
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
|
||||
pub struct OutlineColor;
|
||||
|
||||
impl NamedComponent for OutlineColor {
|
||||
fn name(&self) -> Cow<'_, ComponentName> {
|
||||
Cow::Owned(ComponentName::named::<Global>("outline_color"))
|
||||
}
|
||||
}
|
||||
|
||||
impl ComponentDefinition for OutlineColor {
|
||||
type ComponentType = Color;
|
||||
|
||||
fn default_value(&self, context: &WidgetContext<'_, '_>) -> Color {
|
||||
context.theme().surface.outline
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`Color`] to be used as an outline color.
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
|
||||
pub struct DisabledOutlineColor;
|
||||
|
||||
impl NamedComponent for DisabledOutlineColor {
|
||||
fn name(&self) -> Cow<'_, ComponentName> {
|
||||
Cow::Owned(ComponentName::named::<Global>("disabled_outline_color"))
|
||||
}
|
||||
}
|
||||
|
||||
impl ComponentDefinition for DisabledOutlineColor {
|
||||
type ComponentType = Color;
|
||||
|
||||
fn default_value(&self, context: &WidgetContext<'_, '_>) -> Color {
|
||||
context.theme().surface.outline_variant
|
||||
}
|
||||
}
|
||||
|
|
|
|||
80
src/tree.rs
80
src/tree.rs
|
|
@ -7,8 +7,10 @@ use kludgine::figures::{Point, Rect};
|
|||
|
||||
use crate::context::WidgetContext;
|
||||
use crate::styles::components::VisualOrder;
|
||||
use crate::styles::{ComponentDefaultvalue, ComponentDefinition, ComponentType, Styles};
|
||||
use crate::styles::{ComponentDefaultvalue, ComponentDefinition, ComponentType, Styles, ThemePair};
|
||||
use crate::value::Value;
|
||||
use crate::widget::{ManagedWidget, WidgetId, WidgetInstance};
|
||||
use crate::window::ThemeMode;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Tree {
|
||||
|
|
@ -31,6 +33,8 @@ impl Tree {
|
|||
parent: parent.map(ManagedWidget::id),
|
||||
layout: None,
|
||||
styles: None,
|
||||
theme: None,
|
||||
theme_mode: None,
|
||||
},
|
||||
);
|
||||
if widget.is_default() {
|
||||
|
|
@ -277,20 +281,47 @@ impl Tree {
|
|||
data.nodes.get(&id).expect("missing widget").parent
|
||||
}
|
||||
|
||||
pub(crate) fn attach_styles(&self, id: WidgetId, styles: Styles) {
|
||||
pub(crate) fn attach_styles(&self, id: WidgetId, styles: Value<Styles>) {
|
||||
let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
|
||||
data.nodes.get_mut(&id).expect("missing widget").styles = Some(styles);
|
||||
}
|
||||
|
||||
pub(crate) fn attach_theme(&self, id: WidgetId, theme: Value<ThemePair>) {
|
||||
let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
|
||||
data.nodes.get_mut(&id).expect("missing widget").theme = Some(theme);
|
||||
}
|
||||
|
||||
pub(crate) fn attach_theme_mode(&self, id: WidgetId, theme: Value<ThemeMode>) {
|
||||
let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
|
||||
data.nodes.get_mut(&id).expect("missing widget").theme_mode = Some(theme);
|
||||
}
|
||||
|
||||
pub(crate) fn overriden_theme(
|
||||
&self,
|
||||
id: WidgetId,
|
||||
) -> (Option<Value<ThemePair>>, Option<Value<ThemeMode>>) {
|
||||
let data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g);
|
||||
|
||||
(
|
||||
data.nodes.get(&id).expect("missing widget").theme.clone(),
|
||||
data.nodes
|
||||
.get(&id)
|
||||
.expect("missing widget")
|
||||
.theme_mode
|
||||
.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn query_styles(
|
||||
&self,
|
||||
perspective: &ManagedWidget,
|
||||
query: &[&dyn ComponentDefaultvalue],
|
||||
context: &WidgetContext<'_, '_>,
|
||||
) -> Styles {
|
||||
self.data
|
||||
.lock()
|
||||
.map_or_else(PoisonError::into_inner, |g| g)
|
||||
.query_styles(perspective.id(), query)
|
||||
.query_styles(perspective.id(), query, context)
|
||||
}
|
||||
|
||||
pub fn query_style<Component: ComponentDefinition>(
|
||||
|
|
@ -391,19 +422,22 @@ impl TreeData {
|
|||
&self,
|
||||
mut perspective: WidgetId,
|
||||
query: &[&dyn ComponentDefaultvalue],
|
||||
context: &WidgetContext<'_, '_>,
|
||||
) -> Styles {
|
||||
let mut query = query.iter().map(|n| n.name()).collect::<Vec<_>>();
|
||||
let mut resolved = Styles::new();
|
||||
while !query.is_empty() {
|
||||
let node = &self.nodes[&perspective];
|
||||
if let Some(styles) = &node.styles {
|
||||
query.retain(|name| {
|
||||
if let Some(component) = styles.get_named(name) {
|
||||
resolved.insert(name, component.clone());
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
styles.map_tracked(context, |styles| {
|
||||
query.retain(|name| {
|
||||
if let Some(component) = styles.get_named(name) {
|
||||
resolved.insert(name, component.clone());
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
let Some(parent) = node.parent else { break };
|
||||
|
|
@ -422,13 +456,21 @@ impl TreeData {
|
|||
loop {
|
||||
let node = &self.nodes[&perspective];
|
||||
if let Some(styles) = &node.styles {
|
||||
if let Some(component) = styles.get_named(&name) {
|
||||
let Ok(value) = <Component::ComponentType>::try_from_component(component.get())
|
||||
else {
|
||||
break;
|
||||
};
|
||||
component.redraw_when_changed(context);
|
||||
return value;
|
||||
match styles.map_tracked(context, |styles| {
|
||||
if let Some(component) = styles.get_named(&name) {
|
||||
let Ok(value) =
|
||||
<Component::ComponentType>::try_from_component(component.get())
|
||||
else {
|
||||
return Err(());
|
||||
};
|
||||
component.redraw_when_changed(context);
|
||||
return Ok(Some(value));
|
||||
}
|
||||
Ok(None)
|
||||
}) {
|
||||
Ok(Some(value)) => return value,
|
||||
Ok(None) => {}
|
||||
Err(()) => break,
|
||||
}
|
||||
}
|
||||
let Some(parent) = node.parent else { break };
|
||||
|
|
@ -443,5 +485,7 @@ pub struct Node {
|
|||
pub children: Vec<WidgetId>,
|
||||
pub parent: Option<WidgetId>,
|
||||
pub layout: Option<Rect<Px>>,
|
||||
pub styles: Option<Styles>,
|
||||
pub styles: Option<Value<Styles>>,
|
||||
pub theme: Option<Value<ThemePair>>,
|
||||
pub theme_mode: Option<Value<ThemeMode>>,
|
||||
}
|
||||
|
|
|
|||
370
src/value.rs
370
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<T> Dynamic<T> {
|
|||
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<R>(&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<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
|
||||
|
|
@ -99,6 +114,19 @@ impl<T> Dynamic<T> {
|
|||
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
|
||||
/// code may produce slightly more readable code.
|
||||
///
|
||||
|
|
@ -131,16 +159,43 @@ impl<T> Dynamic<T> {
|
|||
}
|
||||
|
||||
/// 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
|
||||
T: Clone,
|
||||
{
|
||||
context.redraw_when_changed(self);
|
||||
self.get()
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
|
@ -151,6 +206,11 @@ impl<T> Dynamic<T> {
|
|||
|
||||
/// 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<T>
|
||||
where
|
||||
|
|
@ -168,44 +228,99 @@ impl<T> Dynamic<T> {
|
|||
/// 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<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.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This function panics if this value is already locked by the current
|
||||
/// thread.
|
||||
#[must_use]
|
||||
pub fn create_reader(&self) -> DynamicReader<T> {
|
||||
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<T> {
|
||||
self.create_reader()
|
||||
|
|
@ -215,22 +330,32 @@ impl<T> Dynamic<T> {
|
|||
///
|
||||
/// 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<T>> {
|
||||
fn state(&self) -> Result<DynamicMutexGuard<'_, T>, 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`.
|
||||
|
|
@ -262,7 +387,7 @@ impl<T> Clone for Dynamic<T> {
|
|||
|
||||
impl<T> Drop for Dynamic<T> {
|
||||
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();
|
||||
|
|
@ -276,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)]
|
||||
struct DynamicData<T> {
|
||||
state: Mutex<State<T>>,
|
||||
during_callback_state: Mutex<Option<LockState>>,
|
||||
|
||||
// The AssertUnwindSafe is only needed on Mac. For some reason on
|
||||
// Mac OS, Condvar isn't RefUnwindSafe.
|
||||
|
|
@ -286,27 +449,56 @@ struct DynamicData<T> {
|
|||
}
|
||||
|
||||
impl<T> DynamicData<T> {
|
||||
fn state(&self) -> MutexGuard<'_, State<T>> {
|
||||
self.state
|
||||
fn state(&self) -> Result<DynamicMutexGuard<'_, T>, 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<T>
|
||||
pub fn get(&self) -> Result<GenerationalValue<T>, DeadlockError>
|
||||
where
|
||||
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 {
|
||||
let mut state = self.state();
|
||||
pub fn map_mut<R>(&self, map: impl FnOnce(&mut T, &mut bool) -> R) -> Result<R, DeadlockError> {
|
||||
let mut state = self.state()?;
|
||||
let old = {
|
||||
let state = &mut *state;
|
||||
let mut changed = true;
|
||||
|
|
@ -321,14 +513,14 @@ impl<T> DynamicData<T> {
|
|||
|
||||
self.sync.notify_all();
|
||||
|
||||
old
|
||||
Ok(old)
|
||||
}
|
||||
|
||||
pub fn for_each<F>(&self, map: F)
|
||||
where
|
||||
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));
|
||||
}
|
||||
|
||||
|
|
@ -337,7 +529,7 @@ impl<T> DynamicData<T> {
|
|||
F: for<'a> FnMut(&'a GenerationalValue<T>) -> 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();
|
||||
|
|
@ -349,6 +541,39 @@ impl<T> DynamicData<T> {
|
|||
|
||||
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> {
|
||||
|
|
@ -412,7 +637,7 @@ struct GenerationalValue<T> {
|
|||
/// notified of a change when this guard is dropped.
|
||||
#[derive(Debug)]
|
||||
pub struct DynamicGuard<'a, T> {
|
||||
guard: MutexGuard<'a, State<T>>,
|
||||
guard: DynamicMutexGuard<'a, T>,
|
||||
accessed_mut: bool,
|
||||
}
|
||||
|
||||
|
|
@ -450,28 +675,43 @@ impl<T> DynamicReader<T> {
|
|||
/// 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<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;
|
||||
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
|
||||
}
|
||||
|
|
@ -480,19 +720,42 @@ impl<T> DynamicReader<T> {
|
|||
/// 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -508,7 +771,7 @@ impl<T> DynamicReader<T> {
|
|||
|
||||
impl<T> Clone for DynamicReader<T> {
|
||||
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,
|
||||
|
|
@ -518,7 +781,7 @@ impl<T> Clone for DynamicReader<T> {
|
|||
|
||||
impl<T> Drop for DynamicReader<T> {
|
||||
fn drop(&mut self) {
|
||||
let mut state = self.source.state();
|
||||
let mut state = self.source.state().expect("deadlocked");
|
||||
state.readers -= 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -535,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<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 {
|
||||
return Poll::Ready(true);
|
||||
} else if state.readers == Arc::strong_count(&self.0.source) {
|
||||
|
|
@ -669,6 +932,20 @@ impl<T> Value<T> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Maps the current contents to `map` and returns the result.
|
||||
///
|
||||
/// If `self` is a dynamic, `context` will be invalidated when the value is
|
||||
/// updated.
|
||||
pub fn map_tracked<R>(&self, context: &WidgetContext<'_, '_>, map: impl FnOnce(&T) -> R) -> R {
|
||||
match self {
|
||||
Value::Constant(value) => map(value),
|
||||
Value::Dynamic(dynamic) => {
|
||||
context.redraw_when_changed(dynamic);
|
||||
dynamic.map_ref(map)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Maps the current contents with exclusive access and returns the result.
|
||||
pub fn map_mut<R>(&mut self, map: impl FnOnce(&mut T) -> R) -> R {
|
||||
match self {
|
||||
|
|
@ -685,6 +962,17 @@ impl<T> Value<T> {
|
|||
self.map(Clone::clone)
|
||||
}
|
||||
|
||||
/// Returns a clone of the currently stored value.
|
||||
///
|
||||
/// If `self` is a dynamic, `context` will be invalidated when the value is
|
||||
/// updated.
|
||||
pub fn get_tracked(&self, context: &WidgetContext<'_, '_>) -> T
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
self.map_tracked(context, Clone::clone)
|
||||
}
|
||||
|
||||
/// Returns the current generation of the data stored, if the contained
|
||||
/// value is [`Dynamic`].
|
||||
pub fn generation(&self) -> Option<Generation> {
|
||||
|
|
|
|||
|
|
@ -16,11 +16,11 @@ use kludgine::figures::{IntoSigned, IntoUnsigned, Point, Rect, Size};
|
|||
|
||||
use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext};
|
||||
use crate::styles::components::VisualOrder;
|
||||
use crate::styles::{Component, NamedComponent, Styles};
|
||||
use crate::styles::{IntoComponentValue, NamedComponent, Styles, ThemePair};
|
||||
use crate::tree::Tree;
|
||||
use crate::value::{IntoValue, Value};
|
||||
use crate::widgets::{Align, Expand, Scroll, Style};
|
||||
use crate::window::{RunningWindow, Window, WindowBehavior};
|
||||
use crate::window::{RunningWindow, ThemeMode, Window, WindowBehavior};
|
||||
use crate::{ConstraintLimit, Run};
|
||||
|
||||
/// A type that makes up a graphical user interface.
|
||||
|
|
@ -448,7 +448,7 @@ pub trait MakeWidget: Sized {
|
|||
/// Associates `styles` with this widget.
|
||||
///
|
||||
/// This is equivalent to `Style::new(styles, self)`.
|
||||
fn with_styles(self, styles: impl Into<Styles>) -> Style
|
||||
fn with_styles(self, styles: impl IntoValue<Styles>) -> Style
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
|
|
@ -456,7 +456,7 @@ pub trait MakeWidget: Sized {
|
|||
}
|
||||
|
||||
/// Associates a style component with `self`.
|
||||
fn with(self, name: &impl NamedComponent, component: impl Into<Component>) -> Style {
|
||||
fn with(self, name: &impl NamedComponent, component: impl IntoComponentValue) -> Style {
|
||||
let mut styles = Styles::new();
|
||||
styles.insert(name, component);
|
||||
Style::new(styles, self)
|
||||
|
|
@ -957,10 +957,22 @@ impl ManagedWidget {
|
|||
self.tree.parent(self.id()).is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn attach_styles(&self, styles: Styles) {
|
||||
pub(crate) fn attach_styles(&self, styles: Value<Styles>) {
|
||||
self.tree.attach_styles(self.id(), styles);
|
||||
}
|
||||
|
||||
pub(crate) fn attach_theme(&self, theme: Value<ThemePair>) {
|
||||
self.tree.attach_theme(self.id(), theme);
|
||||
}
|
||||
|
||||
pub(crate) fn attach_theme_mode(&self, theme: Value<ThemeMode>) {
|
||||
self.tree.attach_theme_mode(self.id(), theme);
|
||||
}
|
||||
|
||||
pub(crate) fn overidden_theme(&self) -> (Option<Value<ThemePair>>, Option<Value<ThemeMode>>) {
|
||||
self.tree.overriden_theme(self.id())
|
||||
}
|
||||
|
||||
pub(crate) fn reset_child_layouts(&self) {
|
||||
self.tree.reset_child_layouts(self.id());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,11 +6,14 @@ mod canvas;
|
|||
mod expand;
|
||||
mod input;
|
||||
pub mod label;
|
||||
mod mode_switch;
|
||||
mod resize;
|
||||
pub mod scroll;
|
||||
mod slider;
|
||||
mod space;
|
||||
pub mod stack;
|
||||
mod style;
|
||||
mod themed;
|
||||
mod tilemap;
|
||||
|
||||
pub use align::Align;
|
||||
|
|
@ -19,9 +22,12 @@ pub use canvas::Canvas;
|
|||
pub use expand::Expand;
|
||||
pub use input::Input;
|
||||
pub use label::Label;
|
||||
pub use mode_switch::ModeSwitch;
|
||||
pub use resize::Resize;
|
||||
pub use scroll::Scroll;
|
||||
pub use slider::Slider;
|
||||
pub use space::Space;
|
||||
pub use stack::Stack;
|
||||
pub use style::Style;
|
||||
pub use themed::Themed;
|
||||
pub use tilemap::TileMap;
|
||||
|
|
|
|||
|
|
@ -5,17 +5,19 @@ use std::time::Duration;
|
|||
|
||||
use kludgine::app::winit::event::{ElementState, Ime, KeyEvent};
|
||||
use kludgine::app::winit::keyboard::Key;
|
||||
use kludgine::cosmic_text::{Action, Attrs, Buffer, Cursor, Edit, Editor, Metrics, Shaping};
|
||||
use kludgine::figures::units::{Px, UPx};
|
||||
use kludgine::cosmic_text::{
|
||||
Action, Affinity, Attrs, Buffer, Cursor, Edit, Editor, Metrics, Shaping,
|
||||
};
|
||||
use kludgine::figures::units::{Lp, Px, UPx};
|
||||
use kludgine::figures::{
|
||||
FloatConversion, IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size,
|
||||
};
|
||||
use kludgine::shapes::Shape;
|
||||
use kludgine::shapes::{Shape, StrokeOptions};
|
||||
use kludgine::text::TextOrigin;
|
||||
use kludgine::{Color, Kludgine};
|
||||
|
||||
use crate::context::{EventContext, LayoutContext, WidgetContext};
|
||||
use crate::styles::components::{HighlightColor, LineHeight, TextColor, TextSize};
|
||||
use crate::styles::components::{HighlightColor, LineHeight, OutlineColor, TextColor, TextSize};
|
||||
use crate::styles::Styles;
|
||||
use crate::utils::ModifiersExt;
|
||||
use crate::value::{Generation, IntoValue, Value};
|
||||
|
|
@ -32,6 +34,8 @@ pub struct Input {
|
|||
on_key: Option<Callback<KeyEvent, EventHandling>>,
|
||||
editor: Option<LiveEditor>,
|
||||
cursor_state: CursorState,
|
||||
needs_to_select_all: bool,
|
||||
mouse_buttons_down: usize,
|
||||
}
|
||||
|
||||
impl Input {
|
||||
|
|
@ -47,6 +51,8 @@ impl Input {
|
|||
editor: None,
|
||||
cursor_state: CursorState::default(),
|
||||
on_key: None,
|
||||
mouse_buttons_down: 0,
|
||||
needs_to_select_all: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -100,6 +106,22 @@ impl Input {
|
|||
fn styles(context: &WidgetContext<'_, '_>) -> Styles {
|
||||
context.query_styles(&[&TextColor, &TextSize, &LineHeight])
|
||||
}
|
||||
|
||||
fn select_all(&mut self) {
|
||||
let Some(editor) = self.editor.as_mut().map(|editor| &mut editor.editor) else {
|
||||
return;
|
||||
};
|
||||
if !editor.buffer().lines.is_empty() {
|
||||
let line = editor.buffer().lines.len() - 1;
|
||||
let end = Cursor::new_with_affinity(
|
||||
line,
|
||||
editor.buffer().lines[line].text().len(),
|
||||
Affinity::After,
|
||||
);
|
||||
editor.set_cursor(end);
|
||||
editor.set_select_opt(Some(Cursor::new_with_affinity(0, 0, Affinity::Before)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Input {
|
||||
|
|
@ -132,7 +154,9 @@ impl Widget for Input {
|
|||
_button: kludgine::app::winit::event::MouseButton,
|
||||
context: &mut EventContext<'_, '_>,
|
||||
) -> EventHandling {
|
||||
self.mouse_buttons_down += 1;
|
||||
context.focus();
|
||||
self.needs_to_select_all = false;
|
||||
let styles = context.query_styles(&[&TextColor]);
|
||||
self.editor_mut(context.kludgine, &styles, &context.widget)
|
||||
.action(
|
||||
|
|
@ -166,12 +190,22 @@ impl Widget for Input {
|
|||
context.set_needs_redraw();
|
||||
}
|
||||
|
||||
fn mouse_up(
|
||||
&mut self,
|
||||
_location: Option<Point<Px>>,
|
||||
_device_id: kludgine::app::winit::event::DeviceId,
|
||||
_button: kludgine::app::winit::event::MouseButton,
|
||||
_context: &mut EventContext<'_, '_>,
|
||||
) {
|
||||
self.mouse_buttons_down -= 1;
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn redraw(&mut self, context: &mut crate::context::GraphicsContext<'_, '_, '_, '_, '_>) {
|
||||
self.cursor_state.update(context.elapsed());
|
||||
let cursor_state = self.cursor_state;
|
||||
let size = context.gfx.size();
|
||||
let styles = context.query_styles(&[&TextColor, &HighlightColor]);
|
||||
let styles = context.query_styles(&[&TextColor, &HighlightColor, &OutlineColor]);
|
||||
let highlight = styles.get(&HighlightColor, context);
|
||||
let editor = self.editor_mut(&mut context.gfx, &styles, &context.widget);
|
||||
let cursor = editor.cursor();
|
||||
|
|
@ -311,6 +345,9 @@ impl Widget for Input {
|
|||
context.redraw_when_changed(context.window().focused());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let outline_color = styles.get(&OutlineColor, context);
|
||||
context.stroke_outline::<Lp>(outline_color, StrokeOptions::default());
|
||||
}
|
||||
|
||||
let text_color = styles.get(&TextColor, context);
|
||||
|
|
@ -330,6 +367,10 @@ impl Widget for Input {
|
|||
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
|
||||
) -> Size<UPx> {
|
||||
let styles = context.query_styles(&[&TextColor]);
|
||||
if self.needs_to_select_all {
|
||||
self.needs_to_select_all = false;
|
||||
self.select_all();
|
||||
}
|
||||
let editor = self.editor_mut(&mut context.graphics.gfx, &styles, &context.graphics.widget);
|
||||
let buffer = editor.buffer_mut();
|
||||
buffer.set_size(
|
||||
|
|
@ -362,7 +403,7 @@ impl Widget for Input {
|
|||
// "Keyboard input: {:?}. {:?}, {:?}",
|
||||
// input.logical_key, input.text, input.physical_key
|
||||
// );
|
||||
let (text_changed, handled) = match (input.state, input.logical_key, input.text) {
|
||||
let (text_changed, handled) = match (input.state, input.logical_key, input.text.as_deref()) {
|
||||
(ElementState::Pressed, key @ (Key::Backspace | Key::Delete), _) => {
|
||||
editor.action(
|
||||
context.kludgine.font_system(),
|
||||
|
|
@ -400,6 +441,12 @@ impl Widget for Input {
|
|||
);
|
||||
(false, HANDLED)
|
||||
}
|
||||
(state, _, Some("a")) if context.modifiers().primary() => {
|
||||
if state.is_pressed() {
|
||||
self.select_all();
|
||||
}
|
||||
(false, HANDLED)
|
||||
}
|
||||
(state, _, Some(text))
|
||||
if !context.modifiers().primary()
|
||||
&& text != "\t" // tab
|
||||
|
|
@ -408,7 +455,7 @@ impl Widget for Input {
|
|||
=>
|
||||
{
|
||||
if state.is_pressed() {
|
||||
editor.insert_string(&text, None);
|
||||
editor.insert_string(text, None);
|
||||
}
|
||||
(state.is_pressed(), HANDLED)
|
||||
}
|
||||
|
|
@ -456,6 +503,10 @@ impl Widget for Input {
|
|||
}
|
||||
|
||||
fn focus(&mut self, context: &mut EventContext<'_, '_>) {
|
||||
if self.mouse_buttons_down == 0 {
|
||||
self.needs_to_select_all = true;
|
||||
}
|
||||
|
||||
context.set_ime_allowed(true);
|
||||
context.set_needs_redraw();
|
||||
}
|
||||
|
|
|
|||
31
src/widgets/mode_switch.rs
Normal file
31
src/widgets/mode_switch.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
use crate::context::EventContext;
|
||||
use crate::value::{IntoValue, Value};
|
||||
use crate::widget::{MakeWidget, WidgetRef, WrapperWidget};
|
||||
use crate::window::ThemeMode;
|
||||
|
||||
/// A widget that applies a set of [`Styles`] to all contained widgets.
|
||||
#[derive(Debug)]
|
||||
pub struct ModeSwitch {
|
||||
mode: Value<ThemeMode>,
|
||||
child: WidgetRef,
|
||||
}
|
||||
|
||||
impl ModeSwitch {
|
||||
/// Returns a new widget that applies `mode` to all of its children.
|
||||
pub fn new(mode: impl IntoValue<ThemeMode>, child: impl MakeWidget) -> Self {
|
||||
Self {
|
||||
mode: mode.into_value(),
|
||||
child: WidgetRef::new(child),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WrapperWidget for ModeSwitch {
|
||||
fn child_mut(&mut self) -> &mut WidgetRef {
|
||||
&mut self.child
|
||||
}
|
||||
|
||||
fn mounted(&mut self, context: &mut EventContext<'_, '_>) {
|
||||
context.attach_theme_mode(self.mode.clone());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
use kludgine::figures::units::UPx;
|
||||
use kludgine::figures::{Fraction, IntoSigned, IntoUnsigned, Rect, ScreenScale, Size};
|
||||
|
||||
use crate::context::{AsEventContext, LayoutContext};
|
||||
|
|
@ -81,7 +82,12 @@ impl WrapperWidget for Resize {
|
|||
);
|
||||
context.for_other(&child).layout(available_space)
|
||||
};
|
||||
Rect::from(size.into_signed())
|
||||
Size::<UPx>::new(
|
||||
self.width.clamp(size.width, context.gfx.scale()),
|
||||
self.height.clamp(size.height, context.gfx.scale()),
|
||||
)
|
||||
.into_signed()
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -92,8 +98,11 @@ fn override_constraint(
|
|||
) -> ConstraintLimit {
|
||||
match constraint {
|
||||
ConstraintLimit::Known(size) => ConstraintLimit::Known(range.clamp(size, scale)),
|
||||
ConstraintLimit::ClippedAfter(clipped_after) => {
|
||||
ConstraintLimit::ClippedAfter(range.clamp(clipped_after, scale))
|
||||
}
|
||||
ConstraintLimit::ClippedAfter(clipped_after) => match (range.minimum(), range.maximum()) {
|
||||
(Some(min), Some(max)) if min == max => {
|
||||
ConstraintLimit::Known(min.into_px(scale).into_unsigned())
|
||||
}
|
||||
_ => ConstraintLimit::ClippedAfter(range.clamp(clipped_after, scale)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,22 +126,17 @@ impl Widget for Scroll {
|
|||
|
||||
fn redraw(&mut self, context: &mut crate::context::GraphicsContext<'_, '_, '_, '_, '_>) {
|
||||
context.redraw_when_changed(&self.scrollbar_opacity);
|
||||
let Some(visible_rect) = context.gfx.visible_rect() else {
|
||||
return;
|
||||
};
|
||||
let visible_bottom_right = visible_rect.into_signed().extent();
|
||||
|
||||
let managed = self.contents.mounted(&mut context.as_event_context());
|
||||
context.for_other(&managed).redraw();
|
||||
|
||||
let size = context.gfx.region().size;
|
||||
|
||||
if self.horizontal_bar.amount_hidden > 0 {
|
||||
context.gfx.draw_shape(
|
||||
&Shape::filled_rect(
|
||||
Rect::new(
|
||||
Point::new(
|
||||
self.horizontal_bar.offset,
|
||||
self.control_size.height - self.bar_width,
|
||||
),
|
||||
Point::new(self.horizontal_bar.offset, size.height - self.bar_width),
|
||||
Size::new(self.horizontal_bar.size, self.bar_width),
|
||||
),
|
||||
Color::new_f32(1.0, 1.0, 1.0, *self.scrollbar_opacity.get()),
|
||||
|
|
@ -156,10 +151,7 @@ impl Widget for Scroll {
|
|||
context.gfx.draw_shape(
|
||||
&Shape::filled_rect(
|
||||
Rect::new(
|
||||
Point::new(
|
||||
visible_bottom_right.x - self.bar_width,
|
||||
self.vertical_bar.offset,
|
||||
),
|
||||
Point::new(size.width - self.bar_width, self.vertical_bar.offset),
|
||||
Size::new(self.bar_width, self.vertical_bar.size),
|
||||
),
|
||||
Color::new_f32(1.0, 1.0, 1.0, *self.scrollbar_opacity.get()),
|
||||
|
|
@ -193,12 +185,12 @@ impl Widget for Scroll {
|
|||
if self.enabled.x {
|
||||
ConstraintLimit::ClippedAfter(UPx::MAX - scroll.x.into_unsigned())
|
||||
} else {
|
||||
ConstraintLimit::Known(control_size.width.into_unsigned())
|
||||
available_space.width
|
||||
},
|
||||
if self.enabled.y {
|
||||
ConstraintLimit::ClippedAfter(UPx::MAX - scroll.y.into_unsigned())
|
||||
} else {
|
||||
ConstraintLimit::Known(control_size.height.into_unsigned())
|
||||
available_space.height
|
||||
},
|
||||
);
|
||||
let managed = self.contents.mounted(&mut context.as_event_context());
|
||||
|
|
@ -255,7 +247,22 @@ impl Widget for Scroll {
|
|||
);
|
||||
context.set_child_layout(&managed, region);
|
||||
|
||||
Size::new(available_space.width.max(), available_space.height.max())
|
||||
Size::new(
|
||||
if self.enabled.x {
|
||||
available_space
|
||||
.width
|
||||
.fit_measured(self.content_size.width, context.gfx.scale())
|
||||
} else {
|
||||
self.content_size.width.into_unsigned()
|
||||
},
|
||||
if self.enabled.y {
|
||||
available_space
|
||||
.height
|
||||
.fit_measured(self.content_size.height, context.gfx.scale())
|
||||
} else {
|
||||
self.content_size.height.into_unsigned()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn mouse_wheel(
|
||||
|
|
|
|||
420
src/widgets/slider.rs
Normal file
420
src/widgets/slider.rs
Normal file
|
|
@ -0,0 +1,420 @@
|
|||
use std::borrow::Cow;
|
||||
use std::fmt::Debug;
|
||||
use std::panic::UnwindSafe;
|
||||
|
||||
use kludgine::app::winit::event::{DeviceId, MouseButton};
|
||||
use kludgine::figures::units::{Lp, Px, UPx};
|
||||
use kludgine::figures::{
|
||||
FloatConversion, FromComponents, IntoComponents, IntoSigned, IntoUnsigned, Point, Ranged, Rect,
|
||||
ScreenScale, Size,
|
||||
};
|
||||
use kludgine::shapes::Shape;
|
||||
use kludgine::{Color, Origin};
|
||||
|
||||
use crate::animation::{LinearInterpolate, PercentBetween};
|
||||
use crate::context::{EventContext, GraphicsContext, LayoutContext, WidgetContext};
|
||||
use crate::styles::{ComponentDefinition, ComponentName, Dimension, Group, NamedComponent};
|
||||
use crate::value::{Dynamic, IntoDynamic, IntoValue, Value};
|
||||
use crate::widget::{EventHandling, Widget, HANDLED};
|
||||
use crate::ConstraintLimit;
|
||||
|
||||
/// A widget that allows sliding between two values.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Slider<T> {
|
||||
/// The current value.
|
||||
pub value: Dynamic<T>,
|
||||
/// The minimum value represented by this slider.
|
||||
pub minimum: Value<T>,
|
||||
/// The maximum value represented by this slider.
|
||||
pub maximum: Value<T>,
|
||||
knob_size: UPx,
|
||||
horizontal: bool,
|
||||
rendered_size: Px,
|
||||
}
|
||||
|
||||
impl<T> Slider<T>
|
||||
where
|
||||
T: 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)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Slider<T> {
|
||||
/// 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 {
|
||||
Self {
|
||||
value: value.into_dynamic(),
|
||||
minimum: min.into_value(),
|
||||
maximum: max.into_value(),
|
||||
knob_size: UPx(0),
|
||||
horizontal: true,
|
||||
rendered_size: Px(0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the maximum value of this slider to `max` and returns self.
|
||||
#[must_use]
|
||||
pub fn maximum(mut self, max: impl IntoValue<T>) -> 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 {
|
||||
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<T> Slider<T>
|
||||
where
|
||||
T: LinearInterpolate + Clone,
|
||||
{
|
||||
fn update_from_click(&mut self, position: Point<Px>) {
|
||||
let position = if self.horizontal {
|
||||
position.x
|
||||
} else {
|
||||
position.y
|
||||
};
|
||||
let position = position.clamp(Px(0), self.rendered_size);
|
||||
let percent = position.into_float() / self.rendered_size.into_float();
|
||||
let min = self.minimum.get();
|
||||
let max = self.maximum.get();
|
||||
self.value.update(min.lerp(&max, percent));
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Widget for Slider<T>
|
||||
where
|
||||
T: Clone
|
||||
+ Debug
|
||||
+ PartialOrd
|
||||
+ LinearInterpolate
|
||||
+ PercentBetween
|
||||
+ UnwindSafe
|
||||
+ Send
|
||||
+ 'static,
|
||||
{
|
||||
fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {
|
||||
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
|
||||
.get(&TrackSize, context)
|
||||
.into_px(context.gfx.scale())
|
||||
.min(knob_size);
|
||||
|
||||
let half_knob = knob_size / 2;
|
||||
|
||||
let mut value = self.value.get_tracked(context);
|
||||
let min = self.minimum.get_tracked(context);
|
||||
let mut max = self.maximum.get_tracked(context);
|
||||
|
||||
if max < min {
|
||||
self.maximum.map_mut(|max| *max = min.clone());
|
||||
max = min.clone();
|
||||
}
|
||||
let mut value_clamped = false;
|
||||
if value < min {
|
||||
value_clamped = true;
|
||||
value = min.clone();
|
||||
} else if value > max {
|
||||
value_clamped = true;
|
||||
value = max.clone();
|
||||
}
|
||||
|
||||
if value_clamped {
|
||||
self.value.map_mut(|v| *v = value.clone());
|
||||
}
|
||||
|
||||
let percent = value.percent_between(&min, &max);
|
||||
|
||||
let size = context.gfx.region().size;
|
||||
self.horizontal = size.width >= size.height;
|
||||
|
||||
self.draw_track(
|
||||
&TrackSpec {
|
||||
size,
|
||||
percent: *percent,
|
||||
half_knob,
|
||||
knob_size,
|
||||
track_size,
|
||||
knob_color,
|
||||
track_color,
|
||||
inactive_track_color,
|
||||
},
|
||||
context,
|
||||
);
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
available_space: Size<ConstraintLimit>,
|
||||
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
|
||||
) -> Size<UPx> {
|
||||
let styles = context.query_styles(&[&KnobSize, &MinimumSliderSize]);
|
||||
self.knob_size = styles
|
||||
.get(&KnobSize, context)
|
||||
.into_px(context.gfx.scale())
|
||||
.into_unsigned();
|
||||
let minimum_size = styles
|
||||
.get(&MinimumSliderSize, context)
|
||||
.into_px(context.gfx.scale())
|
||||
.into_unsigned();
|
||||
|
||||
match (available_space.width, available_space.height) {
|
||||
(ConstraintLimit::Known(width), ConstraintLimit::Known(height)) => {
|
||||
// This comparison is done such that if width == height, we end
|
||||
// up with a horizontal slider.
|
||||
if width < height {
|
||||
// Vertical slider
|
||||
Size::new(self.knob_size, height.max(minimum_size))
|
||||
} else {
|
||||
// Horizontal slider
|
||||
Size::new(width.max(minimum_size), self.knob_size)
|
||||
}
|
||||
}
|
||||
(ConstraintLimit::Known(width), ConstraintLimit::ClippedAfter(_)) => {
|
||||
Size::new(width.max(minimum_size), self.knob_size)
|
||||
}
|
||||
(ConstraintLimit::ClippedAfter(_), ConstraintLimit::Known(height)) => {
|
||||
Size::new(self.knob_size, height.max(minimum_size))
|
||||
}
|
||||
(ConstraintLimit::ClippedAfter(width), ConstraintLimit::ClippedAfter(_)) => {
|
||||
// When we have no limit on our, we still want to be draggable.
|
||||
// Since we have no limit in both directions, we have to make a
|
||||
// choice: horizontal or vertical. It seems to @ecton at the
|
||||
// time of writing this that when there is no intent from the
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn hit_test(&mut self, _location: Point<Px>, _context: &mut EventContext<'_, '_>) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn mouse_down(
|
||||
&mut self,
|
||||
location: Point<Px>,
|
||||
_device_id: DeviceId,
|
||||
_button: MouseButton,
|
||||
_context: &mut EventContext<'_, '_>,
|
||||
) -> EventHandling {
|
||||
self.update_from_click(location);
|
||||
HANDLED
|
||||
}
|
||||
|
||||
fn mouse_drag(
|
||||
&mut self,
|
||||
location: Point<Px>,
|
||||
_device_id: DeviceId,
|
||||
_button: MouseButton,
|
||||
_context: &mut EventContext<'_, '_>,
|
||||
) {
|
||||
self.update_from_click(location);
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
pub struct TrackSize;
|
||||
|
||||
impl ComponentDefinition for TrackSize {
|
||||
type ComponentType = Dimension;
|
||||
|
||||
fn default_value(&self, _context: &WidgetContext<'_, '_>) -> Self::ComponentType {
|
||||
Dimension::Lp(Lp::points(5))
|
||||
}
|
||||
}
|
||||
|
||||
impl NamedComponent for TrackSize {
|
||||
fn name(&self) -> Cow<'_, ComponentName> {
|
||||
Cow::Owned(ComponentName::new(Group::new("Slider"), "track_size"))
|
||||
}
|
||||
}
|
||||
|
||||
/// The width and height of the draggable portion of a [`Slider`].
|
||||
pub struct KnobSize;
|
||||
|
||||
impl ComponentDefinition for KnobSize {
|
||||
type ComponentType = Dimension;
|
||||
|
||||
fn default_value(&self, _context: &WidgetContext<'_, '_>) -> Self::ComponentType {
|
||||
Dimension::Lp(Lp::points(14))
|
||||
}
|
||||
}
|
||||
|
||||
impl NamedComponent for KnobSize {
|
||||
fn name(&self) -> Cow<'_, ComponentName> {
|
||||
Cow::Owned(ComponentName::new(Group::new("Slider"), "knob_size"))
|
||||
}
|
||||
}
|
||||
|
||||
/// The minimum length of the slidable dimension.
|
||||
pub struct MinimumSliderSize;
|
||||
|
||||
impl ComponentDefinition for MinimumSliderSize {
|
||||
type ComponentType = Dimension;
|
||||
|
||||
fn default_value(&self, _context: &WidgetContext<'_, '_>) -> Self::ComponentType {
|
||||
Dimension::Lp(Lp::points(14))
|
||||
}
|
||||
}
|
||||
|
||||
impl NamedComponent for MinimumSliderSize {
|
||||
fn name(&self) -> Cow<'_, ComponentName> {
|
||||
Cow::Owned(ComponentName::new(Group::new("Slider"), "minimum_size"))
|
||||
}
|
||||
}
|
||||
|
||||
/// The color of the draggable portion of the knob.
|
||||
pub struct KnobColor;
|
||||
|
||||
impl ComponentDefinition for KnobColor {
|
||||
type ComponentType = Color;
|
||||
|
||||
fn default_value(&self, context: &WidgetContext<'_, '_>) -> Self::ComponentType {
|
||||
context.theme().primary.color
|
||||
}
|
||||
}
|
||||
|
||||
impl NamedComponent for KnobColor {
|
||||
fn name(&self) -> Cow<'_, ComponentName> {
|
||||
Cow::Owned(ComponentName::new(Group::new("Slider"), "knob_color"))
|
||||
}
|
||||
}
|
||||
|
||||
/// 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().primary.color
|
||||
}
|
||||
}
|
||||
|
||||
impl NamedComponent for TrackColor {
|
||||
fn name(&self) -> Cow<'_, ComponentName> {
|
||||
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",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
@ -89,7 +89,7 @@ impl Stack {
|
|||
if let Some(expand) = guard.downcast_ref::<Expand>() {
|
||||
let weight = expand.weight;
|
||||
(
|
||||
WidgetRef::Unmounted(widget.clone()),
|
||||
expand.child().clone(),
|
||||
StackDimension::Fractional { weight },
|
||||
)
|
||||
} else if let Some((child, size)) =
|
||||
|
|
@ -403,18 +403,14 @@ impl Layout {
|
|||
// Measure the children that fit their content
|
||||
for &id in &self.measured {
|
||||
let index = self.children.index_of_id(id).expect("child not found");
|
||||
if remaining > 0 {
|
||||
let (measured, _) = self.orientation.split_size(measure(
|
||||
index,
|
||||
self.orientation
|
||||
.make_size(ConstraintLimit::ClippedAfter(remaining), other_constraint),
|
||||
false,
|
||||
));
|
||||
self.layouts[index].size = measured;
|
||||
remaining = remaining.saturating_sub(measured);
|
||||
} else {
|
||||
self.layouts[index].size = UPx(0);
|
||||
}
|
||||
let (measured, _) = self.orientation.split_size(measure(
|
||||
index,
|
||||
self.orientation
|
||||
.make_size(ConstraintLimit::ClippedAfter(remaining), other_constraint),
|
||||
false,
|
||||
));
|
||||
self.layouts[index].size = measured;
|
||||
remaining = remaining.saturating_sub(measured);
|
||||
}
|
||||
|
||||
// Measure the weighted children within the remaining space
|
||||
|
|
|
|||
|
|
@ -1,20 +1,21 @@
|
|||
use crate::context::EventContext;
|
||||
use crate::styles::Styles;
|
||||
use crate::value::{IntoValue, Value};
|
||||
use crate::widget::{MakeWidget, WidgetRef, WrapperWidget};
|
||||
|
||||
/// A widget that applies a set of [`Styles`] to all contained widgets.
|
||||
#[derive(Debug)]
|
||||
pub struct Style {
|
||||
styles: Styles,
|
||||
styles: Value<Styles>,
|
||||
child: WidgetRef,
|
||||
}
|
||||
|
||||
impl Style {
|
||||
/// Returns a new widget that applies `styles` to `child` and any children
|
||||
/// it may have.
|
||||
pub fn new(styles: impl Into<Styles>, child: impl MakeWidget) -> Self {
|
||||
pub fn new(styles: impl IntoValue<Styles>, child: impl MakeWidget) -> Self {
|
||||
Self {
|
||||
styles: styles.into(),
|
||||
styles: styles.into_value(),
|
||||
child: WidgetRef::new(child),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
31
src/widgets/themed.rs
Normal file
31
src/widgets/themed.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
use crate::context::EventContext;
|
||||
use crate::styles::ThemePair;
|
||||
use crate::value::{IntoValue, Value};
|
||||
use crate::widget::{MakeWidget, WidgetRef, WrapperWidget};
|
||||
|
||||
/// A widget that applies a set of [`Styles`] to all contained widgets.
|
||||
#[derive(Debug)]
|
||||
pub struct Themed {
|
||||
theme: Value<ThemePair>,
|
||||
child: WidgetRef,
|
||||
}
|
||||
|
||||
impl Themed {
|
||||
/// Returns a new widget that applies `theme` to all of its children.
|
||||
pub fn new(theme: impl IntoValue<ThemePair>, child: impl MakeWidget) -> Self {
|
||||
Self {
|
||||
theme: theme.into_value(),
|
||||
child: WidgetRef::new(child),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WrapperWidget for Themed {
|
||||
fn child_mut(&mut self) -> &mut WidgetRef {
|
||||
&mut self.child
|
||||
}
|
||||
|
||||
fn mounted(&mut self, context: &mut EventContext<'_, '_>) {
|
||||
context.attach_theme(self.theme.clone());
|
||||
}
|
||||
}
|
||||
157
src/window.rs
157
src/window.rs
|
|
@ -15,13 +15,16 @@ use kludgine::app::winit::event::{
|
|||
DeviceId, ElementState, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase,
|
||||
};
|
||||
use kludgine::app::winit::keyboard::Key;
|
||||
use kludgine::app::winit::window;
|
||||
use kludgine::app::WindowBehavior as _;
|
||||
use kludgine::figures::units::{Px, UPx};
|
||||
use kludgine::figures::{IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size};
|
||||
use kludgine::figures::{IntoSigned, IntoUnsigned, Point, Ranged, Rect, ScreenScale, Size};
|
||||
use kludgine::render::Drawing;
|
||||
use kludgine::wgpu::CompositeAlphaMode;
|
||||
use kludgine::Kludgine;
|
||||
use tracing::Level;
|
||||
|
||||
use crate::animation::{LinearInterpolate, PercentBetween, ZeroToOne};
|
||||
use crate::context::{
|
||||
AsEventContext, EventContext, Exclusive, GraphicsContext, LayoutContext, RedrawStatus,
|
||||
WidgetContext,
|
||||
|
|
@ -31,7 +34,7 @@ use crate::styles::components::LayoutOrder;
|
|||
use crate::styles::ThemePair;
|
||||
use crate::tree::Tree;
|
||||
use crate::utils::ModifiersExt;
|
||||
use crate::value::{Dynamic, DynamicReader, IntoDynamic, Value};
|
||||
use crate::value::{Dynamic, DynamicReader, IntoDynamic, IntoValue, Value};
|
||||
use crate::widget::{
|
||||
EventHandling, ManagedWidget, Widget, WidgetId, WidgetInstance, HANDLED, IGNORED,
|
||||
};
|
||||
|
|
@ -104,6 +107,7 @@ where
|
|||
pub theme: Value<ThemePair>,
|
||||
occluded: Option<Dynamic<bool>>,
|
||||
focused: Option<Dynamic<bool>>,
|
||||
theme_mode: Option<Value<ThemeMode>>,
|
||||
}
|
||||
|
||||
impl<Behavior> Default for Window<Behavior>
|
||||
|
|
@ -155,6 +159,24 @@ impl Window<WidgetInstance> {
|
|||
self.occluded = Some(occluded);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`ThemeMode`] for this window.
|
||||
///
|
||||
/// If a [`ThemeMode`] is provided, the window will be set to this theme
|
||||
/// mode upon creation and will not be updated while the window is running.
|
||||
///
|
||||
/// If a [`Dynamic`] is provided, the initial value will be ignored and the
|
||||
/// dynamic will be updated when the window opens with the user's current
|
||||
/// theme mode. The dynamic will also be updated any time the user's theme
|
||||
/// mode changes.
|
||||
///
|
||||
/// Setting the [`Dynamic`]'s value will also update the window with the new
|
||||
/// mode until a mode change is detected, upon which the new mode will be
|
||||
/// stored.
|
||||
pub fn with_theme_mode(mut self, theme_mode: impl IntoValue<ThemeMode>) -> Self {
|
||||
self.theme_mode = Some(theme_mode.into_value());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<Behavior> Window<Behavior>
|
||||
|
|
@ -188,6 +210,7 @@ where
|
|||
theme: Value::default(),
|
||||
occluded: None,
|
||||
focused: None,
|
||||
theme_mode: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -201,10 +224,12 @@ where
|
|||
GooeyWindow::<Behavior>::run_with(AssertUnwindSafe(sealed::Context {
|
||||
user: self.context,
|
||||
settings: RefCell::new(sealed::WindowSettings {
|
||||
transparent: self.attributes.transparent,
|
||||
attributes: Some(self.attributes),
|
||||
occluded: self.occluded,
|
||||
focused: self.focused,
|
||||
theme: Some(self.theme),
|
||||
theme_mode: self.theme_mode,
|
||||
}),
|
||||
}))
|
||||
}
|
||||
|
|
@ -258,6 +283,8 @@ struct GooeyWindow<T> {
|
|||
max_inner_size: Option<Size<UPx>>,
|
||||
theme: Option<DynamicReader<ThemePair>>,
|
||||
current_theme: ThemePair,
|
||||
theme_mode: Dynamic<ThemeMode>,
|
||||
transparent: bool,
|
||||
}
|
||||
|
||||
impl<T> GooeyWindow<T>
|
||||
|
|
@ -286,6 +313,7 @@ where
|
|||
&self.redraw_status,
|
||||
&self.current_theme,
|
||||
window,
|
||||
self.theme_mode.get(),
|
||||
),
|
||||
kludgine,
|
||||
)
|
||||
|
|
@ -297,6 +325,7 @@ where
|
|||
&self.redraw_status,
|
||||
&self.current_theme,
|
||||
window,
|
||||
self.theme_mode.get(),
|
||||
),
|
||||
kludgine,
|
||||
)
|
||||
|
|
@ -310,6 +339,7 @@ where
|
|||
&self.redraw_status,
|
||||
&self.current_theme,
|
||||
window,
|
||||
self.theme_mode.get(),
|
||||
),
|
||||
kludgine,
|
||||
)
|
||||
|
|
@ -348,7 +378,7 @@ where
|
|||
let new_min_size = (min_width > 0 || min_height > 0)
|
||||
.then_some(Size::<Px>::new(min_width, min_height).into_unsigned());
|
||||
|
||||
if new_min_size != self.min_inner_size {
|
||||
if new_min_size != self.min_inner_size && resizable {
|
||||
window.set_min_inner_size(new_min_size);
|
||||
self.min_inner_size = new_min_size;
|
||||
}
|
||||
|
|
@ -405,6 +435,15 @@ where
|
|||
.theme
|
||||
.take()
|
||||
.expect("theme always present");
|
||||
|
||||
let theme_mode = match context.settings.borrow_mut().theme_mode.take() {
|
||||
Some(Value::Dynamic(dynamic)) => {
|
||||
dynamic.update(window.theme().into());
|
||||
dynamic
|
||||
}
|
||||
Some(Value::Constant(_)) | None => Dynamic::new(window.theme().into()),
|
||||
};
|
||||
let transparent = context.settings.borrow().transparent;
|
||||
let mut behavior = T::initialize(
|
||||
&mut RunningWindow::new(window, &focused, &occluded),
|
||||
context.user,
|
||||
|
|
@ -435,6 +474,8 @@ where
|
|||
max_inner_size: None,
|
||||
current_theme,
|
||||
theme,
|
||||
theme_mode,
|
||||
transparent,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -467,14 +508,19 @@ where
|
|||
&self.redraw_status,
|
||||
&self.current_theme,
|
||||
&mut window,
|
||||
self.theme_mode.get(),
|
||||
),
|
||||
gfx: Exclusive::Owned(Graphics::new(graphics)),
|
||||
};
|
||||
context.redraw_when_changed(&self.theme_mode);
|
||||
let mut layout_context = LayoutContext::new(&mut context);
|
||||
let window_size = layout_context.gfx.size();
|
||||
|
||||
let background_color = layout_context.theme().surface.color;
|
||||
layout_context.graphics.gfx.fill(background_color);
|
||||
if !self.transparent {
|
||||
let background_color = layout_context.theme().surface.color;
|
||||
layout_context.graphics.gfx.fill(background_color);
|
||||
}
|
||||
|
||||
let actual_size = layout_context.layout(if is_expanded {
|
||||
Size::new(
|
||||
ConstraintLimit::Known(window_size.width),
|
||||
|
|
@ -549,12 +595,16 @@ where
|
|||
fn initial_window_attributes(
|
||||
context: &Self::Context,
|
||||
) -> kludgine::app::WindowAttributes<WindowCommand> {
|
||||
context
|
||||
let mut attrs = context
|
||||
.settings
|
||||
.borrow_mut()
|
||||
.attributes
|
||||
.take()
|
||||
.expect("called more than once")
|
||||
.expect("called more than once");
|
||||
if let Some(Value::Constant(theme_mode)) = &context.settings.borrow().theme_mode {
|
||||
attrs.preferred_theme = Some((*theme_mode).into());
|
||||
}
|
||||
attrs
|
||||
}
|
||||
|
||||
fn close_requested(
|
||||
|
|
@ -577,9 +627,21 @@ where
|
|||
// wgpu::Limits::downlevel_webgl2_defaults().using_resolution(adapter_limits)
|
||||
// }
|
||||
|
||||
// fn clear_color() -> Option<kludgine::Color> {
|
||||
// Some(kludgine::Color::BLACK)
|
||||
// }
|
||||
fn clear_color(&self) -> Option<kludgine::Color> {
|
||||
Some(if self.transparent {
|
||||
kludgine::Color::CLEAR_BLACK
|
||||
} else {
|
||||
kludgine::Color::BLACK
|
||||
})
|
||||
}
|
||||
|
||||
fn composite_alpha_mode(&self, supported_modes: &[CompositeAlphaMode]) -> CompositeAlphaMode {
|
||||
if self.transparent && supported_modes.contains(&CompositeAlphaMode::PreMultiplied) {
|
||||
CompositeAlphaMode::PreMultiplied
|
||||
} else {
|
||||
CompositeAlphaMode::Auto
|
||||
}
|
||||
}
|
||||
|
||||
// fn focus_changed(&mut self, window: kludgine::app::Window<'_, ()>) {}
|
||||
|
||||
|
|
@ -616,6 +678,7 @@ where
|
|||
&self.redraw_status,
|
||||
&self.current_theme,
|
||||
&mut window,
|
||||
self.theme_mode.get(),
|
||||
),
|
||||
kludgine,
|
||||
);
|
||||
|
|
@ -645,6 +708,7 @@ where
|
|||
&self.redraw_status,
|
||||
&self.current_theme,
|
||||
&mut window,
|
||||
self.theme_mode.get(),
|
||||
),
|
||||
kludgine,
|
||||
);
|
||||
|
|
@ -711,6 +775,7 @@ where
|
|||
&self.redraw_status,
|
||||
&self.current_theme,
|
||||
&mut window,
|
||||
self.theme_mode.get(),
|
||||
),
|
||||
kludgine,
|
||||
);
|
||||
|
|
@ -745,6 +810,7 @@ where
|
|||
&self.redraw_status,
|
||||
&self.current_theme,
|
||||
&mut window,
|
||||
self.theme_mode.get(),
|
||||
),
|
||||
kludgine,
|
||||
);
|
||||
|
|
@ -773,6 +839,7 @@ where
|
|||
&self.redraw_status,
|
||||
&self.current_theme,
|
||||
&mut window,
|
||||
self.theme_mode.get(),
|
||||
),
|
||||
kludgine,
|
||||
);
|
||||
|
|
@ -787,6 +854,7 @@ where
|
|||
&self.redraw_status,
|
||||
&self.current_theme,
|
||||
&mut window,
|
||||
self.theme_mode.get(),
|
||||
),
|
||||
kludgine,
|
||||
);
|
||||
|
|
@ -827,6 +895,7 @@ where
|
|||
&self.redraw_status,
|
||||
&self.current_theme,
|
||||
&mut window,
|
||||
self.theme_mode.get(),
|
||||
),
|
||||
kludgine,
|
||||
);
|
||||
|
|
@ -851,6 +920,7 @@ where
|
|||
&self.redraw_status,
|
||||
&self.current_theme,
|
||||
&mut window,
|
||||
self.theme_mode.get(),
|
||||
),
|
||||
kludgine,
|
||||
)
|
||||
|
|
@ -866,6 +936,7 @@ where
|
|||
&self.redraw_status,
|
||||
&self.current_theme,
|
||||
&mut window,
|
||||
self.theme_mode.get(),
|
||||
),
|
||||
kludgine,
|
||||
),
|
||||
|
|
@ -900,6 +971,7 @@ where
|
|||
&self.redraw_status,
|
||||
&self.current_theme,
|
||||
&mut window,
|
||||
self.theme_mode.get(),
|
||||
),
|
||||
kludgine,
|
||||
);
|
||||
|
|
@ -917,6 +989,14 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
fn theme_changed(
|
||||
&mut self,
|
||||
window: kludgine::app::Window<'_, WindowCommand>,
|
||||
_kludgine: &mut Kludgine,
|
||||
) {
|
||||
self.theme_mode.update(window.theme().into());
|
||||
}
|
||||
|
||||
fn event(
|
||||
&mut self,
|
||||
mut window: kludgine::app::Window<'_, WindowCommand>,
|
||||
|
|
@ -955,7 +1035,7 @@ pub(crate) mod sealed {
|
|||
|
||||
use crate::styles::ThemePair;
|
||||
use crate::value::{Dynamic, Value};
|
||||
use crate::window::WindowAttributes;
|
||||
use crate::window::{ThemeMode, WindowAttributes};
|
||||
|
||||
pub struct Context<C> {
|
||||
pub user: C,
|
||||
|
|
@ -967,6 +1047,8 @@ pub(crate) mod sealed {
|
|||
pub occluded: Option<Dynamic<bool>>,
|
||||
pub focused: Option<Dynamic<bool>>,
|
||||
pub theme: Option<Value<ThemePair>>,
|
||||
pub theme_mode: Option<Value<ThemeMode>>,
|
||||
pub transparent: bool,
|
||||
}
|
||||
|
||||
pub enum WindowCommand {
|
||||
|
|
@ -974,3 +1056,56 @@ pub(crate) mod sealed {
|
|||
// RequestClose,
|
||||
}
|
||||
}
|
||||
|
||||
/// Controls whether the light or dark theme is applied.
|
||||
#[derive(Default, Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||
pub enum ThemeMode {
|
||||
/// Applies the light theme
|
||||
Light,
|
||||
/// Applies the dark theme
|
||||
#[default]
|
||||
Dark,
|
||||
}
|
||||
|
||||
impl From<window::Theme> for ThemeMode {
|
||||
fn from(value: window::Theme) -> Self {
|
||||
match value {
|
||||
window::Theme::Light => Self::Light,
|
||||
window::Theme::Dark => Self::Dark,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ThemeMode> for window::Theme {
|
||||
fn from(value: ThemeMode) -> Self {
|
||||
match value {
|
||||
ThemeMode::Light => Self::Light,
|
||||
ThemeMode::Dark => Self::Dark,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LinearInterpolate for ThemeMode {
|
||||
fn lerp(&self, target: &Self, percent: f32) -> Self {
|
||||
if percent >= 0.5 {
|
||||
*target
|
||||
} else {
|
||||
*self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PercentBetween for ThemeMode {
|
||||
fn percent_between(&self, min: &Self, max: &Self) -> ZeroToOne {
|
||||
if *min == *max || *self == *min {
|
||||
ZeroToOne::ZERO
|
||||
} else {
|
||||
ZeroToOne::ONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for ThemeMode {
|
||||
const MAX: Self = Self::Dark;
|
||||
const MIN: Self = Self::Light;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue