mirror of
https://github.com/danbulant/cushy
synced 2026-07-06 03:30:38 +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]]
|
[[package]]
|
||||||
name = "appit"
|
name = "appit"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/khonsulabs/appit#043bfe2c78524d6a06ed159289ea1cd7a62b0fec"
|
source = "git+https://github.com/khonsulabs/appit#5ed0d923ded6520950d14b3b869cbcac89452f5c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"raw-window-handle 0.5.2",
|
"raw-window-handle 0.5.2",
|
||||||
"winit",
|
"winit",
|
||||||
|
|
@ -455,9 +455,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "etagere"
|
name = "etagere"
|
||||||
version = "0.2.9"
|
version = "0.2.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5bf70b9ea3a235a7432b4f481854815e2d4fb2fe824c1f5fb09b8985dd06b3e9"
|
checksum = "fcf22f748754352918e082e0039335ee92454a5d62bcaf69b5e8daf5907d9644"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"euclid",
|
"euclid",
|
||||||
"svg_fmt",
|
"svg_fmt",
|
||||||
|
|
@ -481,7 +481,7 @@ checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "figures"
|
name = "figures"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/khonsulabs/figures#f5b9ca5cf181b748897b269ad47d7a9f2d1f3eac"
|
source = "git+https://github.com/khonsulabs/figures#7b41393c44d4def606790e340c98450b603010b4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"euclid",
|
"euclid",
|
||||||
|
|
@ -880,7 +880,7 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kludgine"
|
name = "kludgine"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/khonsulabs/kludgine#a88961b726101ef9bb46bdae4737308d2dcb12a0"
|
source = "git+https://github.com/khonsulabs/kludgine#a26299823498dccbbbb3c28abc820b660fcc1289"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"alot",
|
"alot",
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ fn main() -> gooey::Result {
|
||||||
Lp::points(300)..Lp::points(600),
|
Lp::points(300)..Lp::points(600),
|
||||||
Stack::rows(username_row.and(password_row).and(buttons)),
|
Stack::rows(username_row.and(password_row).and(buttons)),
|
||||||
)
|
)
|
||||||
|
.scroll()
|
||||||
.centered()
|
.centered()
|
||||||
.expand()
|
.expand()
|
||||||
.run()
|
.run()
|
||||||
|
|
|
||||||
|
|
@ -5,5 +5,6 @@ use gooey::Run;
|
||||||
fn main() -> gooey::Result {
|
fn main() -> gooey::Result {
|
||||||
Label::new(include_str!("../src/widgets/scroll.rs"))
|
Label::new(include_str!("../src/widgets/scroll.rs"))
|
||||||
.scroll()
|
.scroll()
|
||||||
|
.expand()
|
||||||
.run()
|
.run()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,128 @@
|
||||||
use gooey::styles::components::TextColor;
|
use std::str::FromStr;
|
||||||
use gooey::styles::{ColorTheme, FixedTheme, InverseTheme, SurfaceTheme, Theme, ThemePair};
|
|
||||||
|
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::widget::MakeWidget;
|
||||||
use gooey::widgets::label::LabelBackground;
|
use gooey::widgets::{Input, Label, Scroll, Slider, Stack, Themed};
|
||||||
use gooey::widgets::{Label, Stack};
|
use gooey::window::ThemeMode;
|
||||||
use gooey::Run;
|
use gooey::Run;
|
||||||
use kludgine::Color;
|
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 {
|
fn main() -> gooey::Result {
|
||||||
let default_theme = ThemePair::default();
|
let (primary, primary_editor) = color_editor(PRIMARY_HUE, 0.8, "Primary");
|
||||||
Stack::columns(
|
let (secondary, secondary_editor) = color_editor(SECONDARY_HUE, 0.3, "Secondary");
|
||||||
theme(default_theme.dark, "Dark")
|
let (tertiary, tertiary_editor) = color_editor(TERTIARY_HUE, 0.3, "Tertiary");
|
||||||
.and(theme(default_theme.light, "Light"))
|
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(
|
.and(fixed_themes(
|
||||||
default_theme.primary_fixed,
|
default_theme.map_each(|theme| theme.primary_fixed),
|
||||||
default_theme.secondary_fixed,
|
default_theme.map_each(|theme| theme.secondary_fixed),
|
||||||
default_theme.tertiary_fixed,
|
default_theme.map_each(|theme| theme.tertiary_fixed),
|
||||||
)),
|
)),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.expand()
|
.expand()
|
||||||
|
.into_window()
|
||||||
|
.with_theme_mode(theme_mode)
|
||||||
.run()
|
.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(
|
fn fixed_themes(
|
||||||
primary: FixedTheme,
|
primary: Dynamic<FixedTheme>,
|
||||||
secondary: FixedTheme,
|
secondary: Dynamic<FixedTheme>,
|
||||||
tertiary: FixedTheme,
|
tertiary: Dynamic<FixedTheme>,
|
||||||
) -> impl MakeWidget {
|
) -> impl MakeWidget {
|
||||||
Stack::rows(
|
Stack::rows(
|
||||||
Label::new("Fixed")
|
Label::new("Fixed")
|
||||||
|
|
@ -35,85 +133,118 @@ fn fixed_themes(
|
||||||
.expand()
|
.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(
|
Stack::columns(
|
||||||
swatch(theme.color, &format!("{label} Fixed"), theme.on_color)
|
swatch(color.clone(), &format!("{label} Fixed"), on_color.clone())
|
||||||
.and(swatch(
|
.and(swatch(
|
||||||
theme.dim_color,
|
theme.map_each(|theme| theme.dim_color),
|
||||||
&format!("Dim {label}"),
|
&format!("Dim {label}"),
|
||||||
theme.on_color,
|
on_color.clone(),
|
||||||
))
|
))
|
||||||
.and(swatch(
|
.and(swatch(
|
||||||
theme.on_color,
|
on_color.clone(),
|
||||||
&format!("On {label} Fixed"),
|
&format!("On {label} Fixed"),
|
||||||
theme.color,
|
color.clone(),
|
||||||
))
|
))
|
||||||
.and(swatch(
|
.and(swatch(
|
||||||
theme.on_color_variant,
|
theme.map_each(|theme| theme.on_color_variant),
|
||||||
&format!("Variant On {label} Fixed"),
|
&format!("Variant On {label} Fixed"),
|
||||||
theme.color,
|
color,
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
.expand()
|
.expand()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn theme(theme: Theme, label: &str) -> impl MakeWidget {
|
fn theme(theme: Dynamic<Theme>, label: &str) -> impl MakeWidget {
|
||||||
Stack::rows(
|
Stack::rows(
|
||||||
Label::new(label)
|
Label::new(label)
|
||||||
.and(
|
.and(
|
||||||
Stack::columns(
|
Stack::columns(
|
||||||
color_theme(theme.primary, "Primary")
|
color_theme(theme.map_each(|theme| theme.primary), "Primary")
|
||||||
.and(color_theme(theme.secondary, "Secondary"))
|
.and(color_theme(
|
||||||
.and(color_theme(theme.tertiary, "Tertiary"))
|
theme.map_each(|theme| theme.secondary),
|
||||||
.and(color_theme(theme.error, "Error")),
|
"Secondary",
|
||||||
|
))
|
||||||
|
.and(color_theme(
|
||||||
|
theme.map_each(|theme| theme.tertiary),
|
||||||
|
"Tertiary",
|
||||||
|
))
|
||||||
|
.and(color_theme(theme.map_each(|theme| theme.error), "Error")),
|
||||||
)
|
)
|
||||||
.expand(),
|
.expand(),
|
||||||
)
|
)
|
||||||
.and(surface_and_inverse_themes(theme.surface, theme.inverse)),
|
.and(surface_theme(theme.map_each(|theme| theme.surface))),
|
||||||
)
|
)
|
||||||
.expand()
|
.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::rows(
|
||||||
Stack::columns(
|
Stack::columns(
|
||||||
swatch(theme.color, "Surface", theme.on_color)
|
swatch(color.clone(), "Surface", on_color.clone())
|
||||||
.and(swatch(theme.dim_color, "Dim Surface", theme.on_color))
|
.and(swatch(
|
||||||
.and(swatch(theme.bright_color, "Bright Surface", theme.on_color)),
|
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()
|
.expand()
|
||||||
.and(inverse_theme(inverse))
|
|
||||||
.and(
|
.and(
|
||||||
Stack::columns(
|
Stack::columns(
|
||||||
swatch(theme.lowest_container, "Lowest Container", theme.on_color)
|
swatch(
|
||||||
.and(swatch(theme.low_container, "Low Container", theme.on_color))
|
theme.map_each(|theme| theme.lowest_container),
|
||||||
.and(swatch(theme.container, "Container", theme.on_color))
|
"Lowest Container",
|
||||||
.and(swatch(
|
on_color.clone(),
|
||||||
theme.high_container,
|
)
|
||||||
"High Container",
|
.and(swatch(
|
||||||
theme.on_color,
|
theme.map_each(|theme| theme.low_container),
|
||||||
))
|
"Low Container",
|
||||||
.and(swatch(
|
on_color.clone(),
|
||||||
theme.highest_container,
|
))
|
||||||
"Highest Container",
|
.and(swatch(
|
||||||
theme.on_color,
|
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(),
|
.expand(),
|
||||||
)
|
)
|
||||||
.and(
|
.and(
|
||||||
Stack::columns(
|
Stack::columns(
|
||||||
swatch(theme.on_color, "On Surface", theme.color)
|
swatch(on_color.clone(), "On Surface", color.clone())
|
||||||
.and(swatch(
|
.and(swatch(
|
||||||
theme.on_color_variant,
|
theme.map_each(|theme| theme.on_color_variant),
|
||||||
"On Color Variant",
|
"On Color Variant",
|
||||||
theme.color,
|
color.clone(),
|
||||||
))
|
))
|
||||||
.and(swatch(theme.outline, "Outline", theme.color))
|
|
||||||
.and(swatch(
|
.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",
|
"Outline Variant",
|
||||||
theme.color,
|
color,
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
.expand(),
|
.expand(),
|
||||||
|
|
@ -122,42 +253,37 @@ fn surface_and_inverse_themes(theme: SurfaceTheme, inverse: InverseTheme) -> imp
|
||||||
.expand()
|
.expand()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn inverse_theme(theme: InverseTheme) -> impl MakeWidget {
|
fn color_theme(theme: Dynamic<ColorTheme>, label: &str) -> impl MakeWidget {
|
||||||
Stack::columns(
|
let color = theme.map_each(|theme| theme.color);
|
||||||
swatch(theme.surface, "Inverse Surface", theme.on_surface)
|
let on_color = theme.map_each(|theme| theme.on_color);
|
||||||
.and(swatch(
|
let container = theme.map_each(|theme| theme.container);
|
||||||
theme.on_surface,
|
let on_container = theme.map_each(|theme| theme.on_container);
|
||||||
"On Inverse Surface",
|
|
||||||
theme.surface,
|
|
||||||
))
|
|
||||||
.and(swatch(theme.primary, "Inverse Primary", theme.surface)),
|
|
||||||
)
|
|
||||||
.expand()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn color_theme(theme: ColorTheme, label: &str) -> impl MakeWidget {
|
|
||||||
Stack::rows(
|
Stack::rows(
|
||||||
swatch(theme.color, label, theme.on_color)
|
swatch(color.clone(), label, on_color.clone())
|
||||||
.and(swatch(theme.on_color, &format!("On {label}"), theme.color))
|
|
||||||
.and(swatch(
|
.and(swatch(
|
||||||
theme.container,
|
on_color.clone(),
|
||||||
&format!("{label} Container"),
|
&format!("On {label}"),
|
||||||
theme.on_container,
|
color.clone(),
|
||||||
))
|
))
|
||||||
.and(swatch(
|
.and(swatch(
|
||||||
theme.on_container,
|
container.clone(),
|
||||||
|
&format!("{label} Container"),
|
||||||
|
on_container.clone(),
|
||||||
|
))
|
||||||
|
.and(swatch(
|
||||||
|
on_container,
|
||||||
&format!("On {label} Container"),
|
&format!("On {label} Container"),
|
||||||
theme.container,
|
container,
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
.expand()
|
.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)
|
Label::new(label)
|
||||||
|
.with(&TextColor, text)
|
||||||
|
.with(&WidgetBackground, background)
|
||||||
.fit_horizontally()
|
.fit_horizontally()
|
||||||
.fit_vertically()
|
.fit_vertically()
|
||||||
.with(&TextColor, text)
|
|
||||||
.with(&LabelBackground, background)
|
|
||||||
.expand()
|
.expand()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
136
src/animation.rs
136
src/animation.rs
|
|
@ -39,15 +39,18 @@
|
||||||
|
|
||||||
pub mod easings;
|
pub mod easings;
|
||||||
|
|
||||||
use std::fmt::Debug;
|
use std::fmt::{Debug, Display};
|
||||||
use std::ops::{ControlFlow, Deref};
|
use std::ops::{ControlFlow, Deref, Div, Mul};
|
||||||
use std::panic::{RefUnwindSafe, UnwindSafe};
|
use std::panic::{RefUnwindSafe, UnwindSafe};
|
||||||
|
use std::str::FromStr;
|
||||||
use std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock, PoisonError};
|
use std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock, PoisonError};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use alot::{LotId, Lots};
|
use alot::{LotId, Lots};
|
||||||
|
use intentional::Cast;
|
||||||
use kempt::Set;
|
use kempt::Set;
|
||||||
|
use kludgine::figures::Ranged;
|
||||||
use kludgine::Color;
|
use kludgine::Color;
|
||||||
|
|
||||||
use crate::animation::easings::Linear;
|
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]
|
#[test]
|
||||||
fn integer_lerps() {
|
fn integer_lerps() {
|
||||||
#[track_caller]
|
#[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.
|
/// 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`.
|
/// Because of these restrictions, this type implements `Ord` and `Eq`.
|
||||||
|
|
@ -727,6 +800,12 @@ impl ZeroToOne {
|
||||||
Self(value.clamp(0., 1.))
|
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.
|
/// Returns the contained floating point value.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn into_f32(self) -> f32 {
|
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 {
|
impl Default for ZeroToOne {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::ZERO
|
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.
|
/// An easing function for customizing animations.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum EasingFunction {
|
pub enum EasingFunction {
|
||||||
|
|
|
||||||
113
src/context.rs
113
src/context.rs
|
|
@ -1,4 +1,5 @@
|
||||||
//! Types that provide access to the Gooey runtime.
|
//! Types that provide access to the Gooey runtime.
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
@ -6,19 +7,18 @@ use std::sync::Arc;
|
||||||
use kludgine::app::winit::event::{
|
use kludgine::app::winit::event::{
|
||||||
DeviceId, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase,
|
DeviceId, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase,
|
||||||
};
|
};
|
||||||
use kludgine::app::winit::window;
|
use kludgine::figures::units::{Lp, Px, UPx};
|
||||||
use kludgine::figures::units::{Px, UPx};
|
use kludgine::figures::{IntoSigned, Point, Rect, ScreenScale, Size};
|
||||||
use kludgine::figures::{IntoSigned, Point, Rect, Size};
|
|
||||||
use kludgine::shapes::{Shape, StrokeOptions};
|
use kludgine::shapes::{Shape, StrokeOptions};
|
||||||
use kludgine::Kludgine;
|
use kludgine::{Color, Kludgine};
|
||||||
|
|
||||||
use crate::graphics::Graphics;
|
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::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::widget::{EventHandling, ManagedWidget, WidgetId, WidgetInstance, WidgetRef};
|
||||||
use crate::window::sealed::WindowCommand;
|
use crate::window::sealed::WindowCommand;
|
||||||
use crate::window::RunningWindow;
|
use crate::window::{RunningWindow, ThemeMode};
|
||||||
use crate::ConstraintLimit;
|
use crate::ConstraintLimit;
|
||||||
|
|
||||||
/// A context to an event function.
|
/// A context to an event function.
|
||||||
|
|
@ -349,6 +349,9 @@ impl<'context, 'window> EventContext<'context, 'window> {
|
||||||
///
|
///
|
||||||
/// This widget does not need to be focused.
|
/// This widget does not need to be focused.
|
||||||
pub fn advance_focus(&mut self, direction: VisualOrder) {
|
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);
|
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.
|
/// Renders the default focus ring for this widget.
|
||||||
///
|
///
|
||||||
/// To ensure the correct color is used, include [`HighlightColor`] in the
|
/// 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let visible_rect = Rect::from(self.gfx.region().size - (Px(1), Px(1)));
|
let color = styles.get(&HighlightColor, self);
|
||||||
let focus_ring = Shape::stroked_rect(
|
self.stroke_outline::<Lp>(color, StrokeOptions::default());
|
||||||
visible_rect,
|
|
||||||
styles.get(&HighlightColor, self),
|
|
||||||
StrokeOptions::default(),
|
|
||||||
);
|
|
||||||
self.gfx
|
|
||||||
.draw_shape(&focus_ring, Point::default(), None, None);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Renders the default focus ring for this widget.
|
/// 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"
|
"redraw called without set_widget_layout"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let background = self.query_style(&WidgetBackground);
|
||||||
|
self.gfx.fill(background);
|
||||||
|
|
||||||
self.current_node
|
self.current_node
|
||||||
.tree
|
.tree
|
||||||
.note_widget_rendered(self.current_node.id());
|
.note_widget_rendered(self.current_node.id());
|
||||||
|
|
@ -662,8 +674,9 @@ pub struct WidgetContext<'context, 'window> {
|
||||||
current_node: ManagedWidget,
|
current_node: ManagedWidget,
|
||||||
redraw_status: &'context RedrawStatus,
|
redraw_status: &'context RedrawStatus,
|
||||||
window: &'context mut RunningWindow<'window>,
|
window: &'context mut RunningWindow<'window>,
|
||||||
theme: &'context ThemePair,
|
theme: Cow<'context, ThemePair>,
|
||||||
pending_state: PendingState<'context>,
|
pending_state: PendingState<'context>,
|
||||||
|
theme_mode: ThemeMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'context, 'window> WidgetContext<'context, 'window> {
|
impl<'context, 'window> WidgetContext<'context, 'window> {
|
||||||
|
|
@ -672,6 +685,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
|
||||||
redraw_status: &'context RedrawStatus,
|
redraw_status: &'context RedrawStatus,
|
||||||
theme: &'context ThemePair,
|
theme: &'context ThemePair,
|
||||||
window: &'context mut RunningWindow<'window>,
|
window: &'context mut RunningWindow<'window>,
|
||||||
|
theme_mode: ThemeMode,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
pending_state: PendingState::Owned(PendingWidgetState {
|
pending_state: PendingState::Owned(PendingWidgetState {
|
||||||
|
|
@ -686,7 +700,8 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
|
||||||
}),
|
}),
|
||||||
current_node,
|
current_node,
|
||||||
redraw_status,
|
redraw_status,
|
||||||
theme,
|
theme: Cow::Borrowed(theme),
|
||||||
|
theme_mode,
|
||||||
window,
|
window,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -697,8 +712,9 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
|
||||||
current_node: self.current_node.clone(),
|
current_node: self.current_node.clone(),
|
||||||
redraw_status: self.redraw_status,
|
redraw_status: self.redraw_status,
|
||||||
window: &mut *self.window,
|
window: &mut *self.window,
|
||||||
theme: self.theme,
|
theme: Cow::Borrowed(self.theme.as_ref()),
|
||||||
pending_state: self.pending_state.borrowed(),
|
pending_state: self.pending_state.borrowed(),
|
||||||
|
theme_mode: self.theme_mode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -711,12 +727,26 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
|
||||||
Widget: ManageWidget,
|
Widget: ManageWidget,
|
||||||
Widget::Managed: MapManagedWidget<WidgetContext<'child, 'window>>,
|
Widget::Managed: MapManagedWidget<WidgetContext<'child, 'window>>,
|
||||||
{
|
{
|
||||||
widget.manage(self).map(|current_node| WidgetContext {
|
widget.manage(self).map(|current_node| {
|
||||||
current_node,
|
let (theme, theme_mode) = current_node.overidden_theme();
|
||||||
redraw_status: self.redraw_status,
|
let theme = if let Some(theme) = theme {
|
||||||
window: &mut *self.window,
|
Cow::Owned(theme.get_tracked(self))
|
||||||
theme: self.theme,
|
} else {
|
||||||
pending_state: self.pending_state.borrowed(),
|
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
|
/// Style queries for children will return any values matching this
|
||||||
/// collection.
|
/// collection.
|
||||||
pub fn attach_styles(&self, styles: Styles) {
|
pub fn attach_styles(&self, styles: Value<Styles>) {
|
||||||
self.current_node.attach_styles(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.
|
/// Queries the widget hierarchy for matching style components.
|
||||||
///
|
///
|
||||||
/// This function traverses up the widget hierarchy looking for the
|
/// 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 {
|
pub fn query_styles(&self, query: &[&dyn ComponentDefaultvalue]) -> Styles {
|
||||||
self.current_node
|
self.current_node
|
||||||
.tree
|
.tree
|
||||||
.query_styles(&self.current_node, query)
|
.query_styles(&self.current_node, query, self)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Queries the widget hierarchy for a single style component.
|
/// 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.
|
/// Returns the theme pair for the window.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn theme_pair(&self) -> &ThemePair {
|
pub fn theme_pair(&self) -> &ThemePair {
|
||||||
self.theme
|
self.theme.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the current theme in either light or dark mode.
|
/// Returns the current theme in either light or dark mode.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn theme(&self) -> &Theme {
|
pub fn theme(&self) -> &Theme {
|
||||||
match self.window.theme() {
|
match self.theme_mode {
|
||||||
window::Theme::Light => &self.theme.light,
|
ThemeMode::Light => &self.theme.light,
|
||||||
window::Theme::Dark => &self.theme.dark,
|
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
|
/// 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
|
/// 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.
|
/// return the measured size.
|
||||||
pub fn fit_measured<Unit>(self, measured: Unit, scale: Fraction) -> UPx
|
pub fn fit_measured<Unit>(self, measured: Unit, scale: Fraction) -> UPx
|
||||||
where
|
where
|
||||||
|
|
|
||||||
163
src/styles.rs
163
src/styles.rs
|
|
@ -459,7 +459,7 @@ where
|
||||||
fn from(value: RangeInclusive<T>) -> Self {
|
fn from(value: RangeInclusive<T>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
start: Bound::Included(value.start().clone().into()),
|
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);
|
pub struct Group(Name);
|
||||||
|
|
||||||
impl Group {
|
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`.
|
/// Returns a new instance using the group name of `T`.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new<T>() -> Self
|
pub fn from_group<T>() -> Self
|
||||||
where
|
where
|
||||||
T: ComponentGroup,
|
T: ComponentGroup,
|
||||||
{
|
{
|
||||||
|
|
@ -626,7 +632,7 @@ impl ComponentName {
|
||||||
|
|
||||||
/// Returns a new instance using `G` and `name`.
|
/// Returns a new instance using `G` and `name`.
|
||||||
pub fn named<G: ComponentGroup>(name: impl Into<Name>) -> Self {
|
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.
|
/// The theme to color surfaces.
|
||||||
pub surface: SurfaceTheme,
|
pub surface: SurfaceTheme,
|
||||||
|
|
||||||
/// A theme of inverse colors to provide high contrast to other elements.
|
|
||||||
pub inverse: InverseTheme,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Theme {
|
impl Theme {
|
||||||
|
|
@ -943,7 +946,6 @@ impl Theme {
|
||||||
tertiary: ColorTheme::light_from_source(tertiary),
|
tertiary: ColorTheme::light_from_source(tertiary),
|
||||||
error: ColorTheme::light_from_source(error),
|
error: ColorTheme::light_from_source(error),
|
||||||
surface: SurfaceTheme::light_from_sources(neutral, neutral_variant),
|
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),
|
tertiary: ColorTheme::dark_from_source(tertiary),
|
||||||
error: ColorTheme::dark_from_source(error),
|
error: ColorTheme::dark_from_source(error),
|
||||||
surface: SurfaceTheme::dark_from_sources(neutral, neutral_variant),
|
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.
|
/// A source for [`Color`]s.
|
||||||
///
|
///
|
||||||
/// This type is a combination of an [`OklabHue`] and a saturation ranging from
|
/// 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
|
/// Returns a new source with the given hue (in degrees) and saturation (0.0
|
||||||
/// - 1.0).
|
/// - 1.0).
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new(hue: f32, saturation: f32) -> Self {
|
pub fn new(hue: impl Into<OklabHue>, saturation: impl Into<ZeroToOne>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
hue: OklabHue::new(hue),
|
hue: hue.into(),
|
||||||
saturation: ZeroToOne::new(saturation),
|
saturation: saturation.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1184,6 +1151,32 @@ impl ColorSource {
|
||||||
Okhsl::new(self.hue, *self.saturation, *lightness.into_lightness()).into_color();
|
Okhsl::new(self.hue, *self.saturation, *lightness.into_lightness()).into_color();
|
||||||
Color::new_f32(rgb.red, rgb.blue, rgb.green, 1.0)
|
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.
|
/// A value that can represent the lightness of a color.
|
||||||
|
|
@ -1231,29 +1224,31 @@ pub trait ColorExt: Copy {
|
||||||
self.into_source_and_lightness().1
|
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`.
|
/// Returns the color in `others` that contrasts the most from `self`.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
fn most_contrasting(self, others: &[Self]) -> Self
|
fn most_contrasting(self, others: &[Self]) -> Self
|
||||||
where
|
where
|
||||||
Self: Copy,
|
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ColorExt for Color {
|
impl ColorExt for Color {
|
||||||
|
|
@ -1268,4 +1263,44 @@ impl ColorExt for Color {
|
||||||
ZeroToOne::new(hsl.lightness * self.alpha_f32()),
|
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)
|
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::context::WidgetContext;
|
||||||
use crate::styles::components::VisualOrder;
|
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::widget::{ManagedWidget, WidgetId, WidgetInstance};
|
||||||
|
use crate::window::ThemeMode;
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub struct Tree {
|
pub struct Tree {
|
||||||
|
|
@ -31,6 +33,8 @@ impl Tree {
|
||||||
parent: parent.map(ManagedWidget::id),
|
parent: parent.map(ManagedWidget::id),
|
||||||
layout: None,
|
layout: None,
|
||||||
styles: None,
|
styles: None,
|
||||||
|
theme: None,
|
||||||
|
theme_mode: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if widget.is_default() {
|
if widget.is_default() {
|
||||||
|
|
@ -277,20 +281,47 @@ impl Tree {
|
||||||
data.nodes.get(&id).expect("missing widget").parent
|
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);
|
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);
|
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(
|
pub fn query_styles(
|
||||||
&self,
|
&self,
|
||||||
perspective: &ManagedWidget,
|
perspective: &ManagedWidget,
|
||||||
query: &[&dyn ComponentDefaultvalue],
|
query: &[&dyn ComponentDefaultvalue],
|
||||||
|
context: &WidgetContext<'_, '_>,
|
||||||
) -> Styles {
|
) -> Styles {
|
||||||
self.data
|
self.data
|
||||||
.lock()
|
.lock()
|
||||||
.map_or_else(PoisonError::into_inner, |g| g)
|
.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>(
|
pub fn query_style<Component: ComponentDefinition>(
|
||||||
|
|
@ -391,19 +422,22 @@ impl TreeData {
|
||||||
&self,
|
&self,
|
||||||
mut perspective: WidgetId,
|
mut perspective: WidgetId,
|
||||||
query: &[&dyn ComponentDefaultvalue],
|
query: &[&dyn ComponentDefaultvalue],
|
||||||
|
context: &WidgetContext<'_, '_>,
|
||||||
) -> Styles {
|
) -> Styles {
|
||||||
let mut query = query.iter().map(|n| n.name()).collect::<Vec<_>>();
|
let mut query = query.iter().map(|n| n.name()).collect::<Vec<_>>();
|
||||||
let mut resolved = Styles::new();
|
let mut resolved = Styles::new();
|
||||||
while !query.is_empty() {
|
while !query.is_empty() {
|
||||||
let node = &self.nodes[&perspective];
|
let node = &self.nodes[&perspective];
|
||||||
if let Some(styles) = &node.styles {
|
if let Some(styles) = &node.styles {
|
||||||
query.retain(|name| {
|
styles.map_tracked(context, |styles| {
|
||||||
if let Some(component) = styles.get_named(name) {
|
query.retain(|name| {
|
||||||
resolved.insert(name, component.clone());
|
if let Some(component) = styles.get_named(name) {
|
||||||
false
|
resolved.insert(name, component.clone());
|
||||||
} else {
|
false
|
||||||
true
|
} else {
|
||||||
}
|
true
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let Some(parent) = node.parent else { break };
|
let Some(parent) = node.parent else { break };
|
||||||
|
|
@ -422,13 +456,21 @@ impl TreeData {
|
||||||
loop {
|
loop {
|
||||||
let node = &self.nodes[&perspective];
|
let node = &self.nodes[&perspective];
|
||||||
if let Some(styles) = &node.styles {
|
if let Some(styles) = &node.styles {
|
||||||
if let Some(component) = styles.get_named(&name) {
|
match styles.map_tracked(context, |styles| {
|
||||||
let Ok(value) = <Component::ComponentType>::try_from_component(component.get())
|
if let Some(component) = styles.get_named(&name) {
|
||||||
else {
|
let Ok(value) =
|
||||||
break;
|
<Component::ComponentType>::try_from_component(component.get())
|
||||||
};
|
else {
|
||||||
component.redraw_when_changed(context);
|
return Err(());
|
||||||
return value;
|
};
|
||||||
|
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 };
|
let Some(parent) = node.parent else { break };
|
||||||
|
|
@ -443,5 +485,7 @@ pub struct Node {
|
||||||
pub children: Vec<WidgetId>,
|
pub children: Vec<WidgetId>,
|
||||||
pub parent: Option<WidgetId>,
|
pub parent: Option<WidgetId>,
|
||||||
pub layout: Option<Rect<Px>>,
|
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.
|
//! Types for storing and interacting with values in Widgets.
|
||||||
|
|
||||||
use std::fmt::Debug;
|
use std::cell::Cell;
|
||||||
|
use std::fmt::{Debug, Display};
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
use std::panic::AssertUnwindSafe;
|
use std::panic::AssertUnwindSafe;
|
||||||
use std::sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError};
|
use std::sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError, TryLockError};
|
||||||
use std::task::{Poll, Waker};
|
use std::task::{Poll, Waker};
|
||||||
|
use std::thread::ThreadId;
|
||||||
|
|
||||||
|
use intentional::Assert;
|
||||||
|
|
||||||
use crate::animation::{DynamicTransition, LinearInterpolate};
|
use crate::animation::{DynamicTransition, LinearInterpolate};
|
||||||
use crate::context::{WidgetContext, WindowHandle};
|
use crate::context::{WidgetContext, WindowHandle};
|
||||||
|
|
@ -30,21 +34,32 @@ impl<T> Dynamic<T> {
|
||||||
readers: 0,
|
readers: 0,
|
||||||
wakers: Vec::new(),
|
wakers: Vec::new(),
|
||||||
}),
|
}),
|
||||||
|
during_callback_state: Mutex::default(),
|
||||||
sync: AssertUnwindSafe(Condvar::new()),
|
sync: AssertUnwindSafe(Condvar::new()),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Maps the contents with read-only access.
|
/// Maps the contents with read-only access.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This function panics if this value is already locked by the current
|
||||||
|
/// thread.
|
||||||
pub fn map_ref<R>(&self, map: impl FnOnce(&T) -> R) -> R {
|
pub fn map_ref<R>(&self, map: impl FnOnce(&T) -> R) -> R {
|
||||||
let state = self.state();
|
let state = self.state().expect("deadlocked");
|
||||||
map(&state.wrapped.value)
|
map(&state.wrapped.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Maps the contents with exclusive access. Before returning from this
|
/// Maps the contents with exclusive access. Before returning from this
|
||||||
/// function, all observers will be notified that the contents have been
|
/// function, all observers will be notified that the contents have been
|
||||||
/// updated.
|
/// updated.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This function panics if this value is already locked by the current
|
||||||
|
/// thread.
|
||||||
pub fn map_mut<R>(&self, map: impl FnOnce(&mut T) -> R) -> R {
|
pub fn map_mut<R>(&self, map: impl FnOnce(&mut T) -> R) -> R {
|
||||||
self.0.map_mut(|value, _| map(value))
|
self.0.map_mut(|value, _| map(value)).expect("deadlocked")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a new dynamic that is updated using `U::from(T.clone())` each
|
/// Returns a new dynamic that is updated using `U::from(T.clone())` each
|
||||||
|
|
@ -99,6 +114,19 @@ impl<T> Dynamic<T> {
|
||||||
self.0.map_each(move |gen| map(&gen.value))
|
self.0.map_each(move |gen| map(&gen.value))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a new dynamic value that contains the result of invoking `map`
|
||||||
|
/// each time this value is changed.
|
||||||
|
///
|
||||||
|
/// This version of `map_each` uses [`Dynamic::try_update`] to prevent
|
||||||
|
/// deadlocks and debounce dependent values.
|
||||||
|
pub fn map_each_unique<R, F>(&self, mut map: F) -> Dynamic<R>
|
||||||
|
where
|
||||||
|
F: for<'a> FnMut(&'a T) -> R + Send + 'static,
|
||||||
|
R: Send + PartialEq + 'static,
|
||||||
|
{
|
||||||
|
self.0.map_each_unique(move |gen| map(&gen.value))
|
||||||
|
}
|
||||||
|
|
||||||
/// A helper function that invokes `with_clone` with a clone of self. This
|
/// A helper function that invokes `with_clone` with a clone of self. This
|
||||||
/// code may produce slightly more readable code.
|
/// code may produce slightly more readable code.
|
||||||
///
|
///
|
||||||
|
|
@ -131,16 +159,43 @@ impl<T> Dynamic<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a clone of the currently contained value.
|
/// Returns a clone of the currently contained value.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This function panics if this value is already locked by the current
|
||||||
|
/// thread.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn get(&self) -> T
|
pub fn get(&self) -> T
|
||||||
where
|
where
|
||||||
T: Clone,
|
T: Clone,
|
||||||
{
|
{
|
||||||
self.0.get().value
|
self.0.get().expect("deadlocked").value
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a clone of the currently contained value.
|
||||||
|
///
|
||||||
|
/// `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
|
/// Returns the currently stored value, replacing the current contents with
|
||||||
/// `T::default()`.
|
/// `T::default()`.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This function panics if this value is already locked by the current
|
||||||
|
/// thread.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn take(&self) -> T
|
pub fn take(&self) -> T
|
||||||
where
|
where
|
||||||
|
|
@ -151,6 +206,11 @@ impl<T> Dynamic<T> {
|
||||||
|
|
||||||
/// Checks if the currently stored value is different than `T::default()`,
|
/// Checks if the currently stored value is different than `T::default()`,
|
||||||
/// and if so, returns `Some(self.take())`.
|
/// and if so, returns `Some(self.take())`.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This function panics if this value is already locked by the current
|
||||||
|
/// thread.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn take_if_not_default(&self) -> Option<T>
|
pub fn take_if_not_default(&self) -> Option<T>
|
||||||
where
|
where
|
||||||
|
|
@ -168,44 +228,99 @@ impl<T> Dynamic<T> {
|
||||||
/// Replaces the contents with `new_value`, returning the previous contents.
|
/// Replaces the contents with `new_value`, returning the previous contents.
|
||||||
/// Before returning from this function, all observers will be notified that
|
/// Before returning from this function, all observers will be notified that
|
||||||
/// the contents have been updated.
|
/// the contents have been updated.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This function panics if this value is already locked by the current
|
||||||
|
/// thread.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn replace(&self, new_value: T) -> T {
|
pub fn replace(&self, new_value: T) -> T {
|
||||||
self.0
|
self.0
|
||||||
.map_mut(|value, _| std::mem::replace(value, new_value))
|
.map_mut(|value, _| std::mem::replace(value, new_value))
|
||||||
|
.expect("deadlocked")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stores `new_value` in this dynamic. Before returning from this function,
|
/// Stores `new_value` in this dynamic. Before returning from this function,
|
||||||
/// all observers will be notified that the contents have been updated.
|
/// all observers will be notified that the contents have been updated.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This function panics if this value is already locked by the current
|
||||||
|
/// thread.
|
||||||
pub fn set(&self, new_value: T) {
|
pub fn set(&self, new_value: T) {
|
||||||
let _old = self.replace(new_value);
|
let _old = self.replace(new_value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates this dynamic with `new_value`, but only if `new_value` is not
|
/// Updates this dynamic with `new_value`, but only if `new_value` is not
|
||||||
/// equal to the currently stored value.
|
/// equal to the currently stored value.
|
||||||
pub fn update(&self, new_value: T)
|
///
|
||||||
|
/// Returns true if the value was updated.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This function panics if this value is already locked by the current
|
||||||
|
/// thread.
|
||||||
|
pub fn update(&self, new_value: T) -> bool
|
||||||
where
|
where
|
||||||
T: PartialEq,
|
T: PartialEq,
|
||||||
{
|
{
|
||||||
self.0.map_mut(|value, changed| {
|
self.0
|
||||||
if *value == new_value {
|
.map_mut(|value, changed| {
|
||||||
*changed = false;
|
if *value == new_value {
|
||||||
} else {
|
*changed = false;
|
||||||
*value = new_value;
|
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.
|
/// Returns a new reference-based reader for this dynamic value.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This function panics if this value is already locked by the current
|
||||||
|
/// thread.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn create_reader(&self) -> DynamicReader<T> {
|
pub fn create_reader(&self) -> DynamicReader<T> {
|
||||||
self.state().readers += 1;
|
self.state().expect("deadlocked").readers += 1;
|
||||||
DynamicReader {
|
DynamicReader {
|
||||||
source: self.0.clone(),
|
source: self.0.clone(),
|
||||||
read_generation: self.0.state().wrapped.generation,
|
read_generation: self.0.state().expect("deadlocked").wrapped.generation,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts this [`Dynamic`] into a reader.
|
/// Converts this [`Dynamic`] into a reader.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This function panics if this value is already locked by the current
|
||||||
|
/// thread.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn into_reader(self) -> DynamicReader<T> {
|
pub fn into_reader(self) -> DynamicReader<T> {
|
||||||
self.create_reader()
|
self.create_reader()
|
||||||
|
|
@ -215,22 +330,32 @@ impl<T> Dynamic<T> {
|
||||||
///
|
///
|
||||||
/// This call will block until all other guards for this dynamic have been
|
/// This call will block until all other guards for this dynamic have been
|
||||||
/// dropped.
|
/// dropped.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This function panics if this value is already locked by the current
|
||||||
|
/// thread.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn lock(&self) -> DynamicGuard<'_, T> {
|
pub fn lock(&self) -> DynamicGuard<'_, T> {
|
||||||
DynamicGuard {
|
DynamicGuard {
|
||||||
guard: self.0.state(),
|
guard: self.0.state().expect("deadlocked"),
|
||||||
accessed_mut: false,
|
accessed_mut: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn state(&self) -> MutexGuard<'_, State<T>> {
|
fn state(&self) -> Result<DynamicMutexGuard<'_, T>, DeadlockError> {
|
||||||
self.0.state()
|
self.0.state()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the current generation of the value.
|
/// Returns the current generation of the value.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This function panics if this value is already locked by the current
|
||||||
|
/// thread.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn generation(&self) -> Generation {
|
pub fn generation(&self) -> Generation {
|
||||||
self.state().wrapped.generation
|
self.state().expect("deadlocked").wrapped.generation
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a pending transition for this value to `new_value`.
|
/// Returns a pending transition for this value to `new_value`.
|
||||||
|
|
@ -262,7 +387,7 @@ impl<T> Clone for Dynamic<T> {
|
||||||
|
|
||||||
impl<T> Drop for Dynamic<T> {
|
impl<T> Drop for Dynamic<T> {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
let state = self.state();
|
let state = self.state().expect("deadlocked");
|
||||||
if state.readers == 0 {
|
if state.readers == 0 {
|
||||||
drop(state);
|
drop(state);
|
||||||
self.0.sync.notify_all();
|
self.0.sync.notify_all();
|
||||||
|
|
@ -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)]
|
#[derive(Debug)]
|
||||||
struct DynamicData<T> {
|
struct DynamicData<T> {
|
||||||
state: Mutex<State<T>>,
|
state: Mutex<State<T>>,
|
||||||
|
during_callback_state: Mutex<Option<LockState>>,
|
||||||
|
|
||||||
// The AssertUnwindSafe is only needed on Mac. For some reason on
|
// The AssertUnwindSafe is only needed on Mac. For some reason on
|
||||||
// Mac OS, Condvar isn't RefUnwindSafe.
|
// Mac OS, Condvar isn't RefUnwindSafe.
|
||||||
|
|
@ -286,27 +449,56 @@ struct DynamicData<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> DynamicData<T> {
|
impl<T> DynamicData<T> {
|
||||||
fn state(&self) -> MutexGuard<'_, State<T>> {
|
fn state(&self) -> Result<DynamicMutexGuard<'_, T>, DeadlockError> {
|
||||||
self.state
|
let mut during_sync = self
|
||||||
|
.during_callback_state
|
||||||
.lock()
|
.lock()
|
||||||
.map_or_else(PoisonError::into_inner, |g| g)
|
.map_or_else(PoisonError::into_inner, |g| g);
|
||||||
|
|
||||||
|
let current_thread_id = std::thread::current().id();
|
||||||
|
let guard = loop {
|
||||||
|
match self.state.try_lock() {
|
||||||
|
Ok(g) => break g,
|
||||||
|
Err(TryLockError::Poisoned(poision)) => break poision.into_inner(),
|
||||||
|
Err(TryLockError::WouldBlock) => loop {
|
||||||
|
match &*during_sync {
|
||||||
|
Some(state) if state.locked_thread == current_thread_id => {
|
||||||
|
return Err(DeadlockError)
|
||||||
|
}
|
||||||
|
Some(_) => {
|
||||||
|
during_sync = self
|
||||||
|
.sync
|
||||||
|
.wait(during_sync)
|
||||||
|
.map_or_else(PoisonError::into_inner, |g| g);
|
||||||
|
}
|
||||||
|
None => break,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
*during_sync = Some(LockState {
|
||||||
|
locked_thread: current_thread_id,
|
||||||
|
});
|
||||||
|
Ok(DynamicMutexGuard {
|
||||||
|
dynamic: self,
|
||||||
|
guard,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn redraw_when_changed(&self, window: WindowHandle) {
|
pub fn redraw_when_changed(&self, window: WindowHandle) {
|
||||||
let mut state = self.state();
|
let mut state = self.state().expect("deadlocked");
|
||||||
state.windows.push(window);
|
state.windows.push(window);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
pub fn get(&self) -> Result<GenerationalValue<T>, DeadlockError>
|
||||||
pub fn get(&self) -> GenerationalValue<T>
|
|
||||||
where
|
where
|
||||||
T: Clone,
|
T: Clone,
|
||||||
{
|
{
|
||||||
self.state().wrapped.clone()
|
self.state().map(|state| state.wrapped.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn map_mut<R>(&self, map: impl FnOnce(&mut T, &mut bool) -> R) -> R {
|
pub fn map_mut<R>(&self, map: impl FnOnce(&mut T, &mut bool) -> R) -> Result<R, DeadlockError> {
|
||||||
let mut state = self.state();
|
let mut state = self.state()?;
|
||||||
let old = {
|
let old = {
|
||||||
let state = &mut *state;
|
let state = &mut *state;
|
||||||
let mut changed = true;
|
let mut changed = true;
|
||||||
|
|
@ -321,14 +513,14 @@ impl<T> DynamicData<T> {
|
||||||
|
|
||||||
self.sync.notify_all();
|
self.sync.notify_all();
|
||||||
|
|
||||||
old
|
Ok(old)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn for_each<F>(&self, map: F)
|
pub fn for_each<F>(&self, map: F)
|
||||||
where
|
where
|
||||||
F: for<'a> FnMut(&'a GenerationalValue<T>) + Send + 'static,
|
F: for<'a> FnMut(&'a GenerationalValue<T>) + Send + 'static,
|
||||||
{
|
{
|
||||||
let mut state = self.state();
|
let mut state = self.state().expect("deadlocked");
|
||||||
state.callbacks.push(Box::new(map));
|
state.callbacks.push(Box::new(map));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -337,7 +529,7 @@ impl<T> DynamicData<T> {
|
||||||
F: for<'a> FnMut(&'a GenerationalValue<T>) -> R + Send + 'static,
|
F: for<'a> FnMut(&'a GenerationalValue<T>) -> R + Send + 'static,
|
||||||
R: Send + 'static,
|
R: Send + 'static,
|
||||||
{
|
{
|
||||||
let mut state = self.state();
|
let mut state = self.state().expect("deadlocked");
|
||||||
let initial_value = map(&state.wrapped);
|
let initial_value = map(&state.wrapped);
|
||||||
let mapped_value = Dynamic::new(initial_value);
|
let mapped_value = Dynamic::new(initial_value);
|
||||||
let returned = mapped_value.clone();
|
let returned = mapped_value.clone();
|
||||||
|
|
@ -349,6 +541,39 @@ impl<T> DynamicData<T> {
|
||||||
|
|
||||||
returned
|
returned
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn map_each_unique<R, F>(&self, mut map: F) -> Dynamic<R>
|
||||||
|
where
|
||||||
|
F: for<'a> FnMut(&'a GenerationalValue<T>) -> R + Send + 'static,
|
||||||
|
R: PartialEq + Send + 'static,
|
||||||
|
{
|
||||||
|
let mut state = self.state().expect("deadlocked");
|
||||||
|
let initial_value = map(&state.wrapped);
|
||||||
|
let mapped_value = Dynamic::new(initial_value);
|
||||||
|
let returned = mapped_value.clone();
|
||||||
|
state
|
||||||
|
.callbacks
|
||||||
|
.push(Box::new(move |updated: &GenerationalValue<T>| {
|
||||||
|
let _deadlock = mapped_value.try_update(map(updated));
|
||||||
|
}));
|
||||||
|
|
||||||
|
returned
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A deadlock occurred accessing a [`Dynamic`].
|
||||||
|
///
|
||||||
|
/// Currently Gooey is only able to detect deadlocks where a single thread tries
|
||||||
|
/// to lock the same [`Dynamic`] multiple times.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct DeadlockError;
|
||||||
|
|
||||||
|
impl std::error::Error for DeadlockError {}
|
||||||
|
|
||||||
|
impl Display for DeadlockError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str("a deadlock was detected")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct State<T> {
|
struct State<T> {
|
||||||
|
|
@ -412,7 +637,7 @@ struct GenerationalValue<T> {
|
||||||
/// notified of a change when this guard is dropped.
|
/// notified of a change when this guard is dropped.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct DynamicGuard<'a, T> {
|
pub struct DynamicGuard<'a, T> {
|
||||||
guard: MutexGuard<'a, State<T>>,
|
guard: DynamicMutexGuard<'a, T>,
|
||||||
accessed_mut: bool,
|
accessed_mut: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -450,28 +675,43 @@ impl<T> DynamicReader<T> {
|
||||||
/// Maps the contents of the dynamic value and returns the result.
|
/// Maps the contents of the dynamic value and returns the result.
|
||||||
///
|
///
|
||||||
/// This function marks the currently stored value as being read.
|
/// This function marks the currently stored value as being read.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This function panics if this value is already locked by the current
|
||||||
|
/// thread.
|
||||||
pub fn map_ref<R>(&mut self, map: impl FnOnce(&T) -> R) -> R {
|
pub fn map_ref<R>(&mut self, map: impl FnOnce(&T) -> R) -> R {
|
||||||
let state = self.source.state();
|
let state = self.source.state().expect("deadlocked");
|
||||||
self.read_generation = state.wrapped.generation;
|
self.read_generation = state.wrapped.generation;
|
||||||
map(&state.wrapped.value)
|
map(&state.wrapped.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if the dynamic has been modified since the last time the
|
/// Returns true if the dynamic has been modified since the last time the
|
||||||
/// value was accessed through this reader.
|
/// value was accessed through this reader.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This function panics if this value is already locked by the current
|
||||||
|
/// thread.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn has_updated(&self) -> bool {
|
pub fn has_updated(&self) -> bool {
|
||||||
self.source.state().wrapped.generation != self.read_generation
|
self.source.state().expect("deadlocked").wrapped.generation != self.read_generation
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a clone of the currently contained value.
|
/// Returns a clone of the currently contained value.
|
||||||
///
|
///
|
||||||
/// This function marks the currently stored value as being read.
|
/// This function marks the currently stored value as being read.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This function panics if this value is already locked by the current
|
||||||
|
/// thread.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn get(&mut self) -> T
|
pub fn get(&mut self) -> T
|
||||||
where
|
where
|
||||||
T: Clone,
|
T: Clone,
|
||||||
{
|
{
|
||||||
let GenerationalValue { value, generation } = self.source.get();
|
let GenerationalValue { value, generation } = self.source.get().expect("deadlocked");
|
||||||
self.read_generation = generation;
|
self.read_generation = generation;
|
||||||
value
|
value
|
||||||
}
|
}
|
||||||
|
|
@ -480,19 +720,42 @@ impl<T> DynamicReader<T> {
|
||||||
/// there are no remaining writers for the value.
|
/// there are no remaining writers for the value.
|
||||||
///
|
///
|
||||||
/// Returns true if a newly updated value was discovered.
|
/// Returns true if a newly updated value was discovered.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This function panics if this value is already locked by the current
|
||||||
|
/// thread.
|
||||||
pub fn block_until_updated(&mut self) -> bool {
|
pub fn block_until_updated(&mut self) -> bool {
|
||||||
let mut state = self.source.state();
|
let mut deadlock_state = self
|
||||||
|
.source
|
||||||
|
.during_callback_state
|
||||||
|
.lock()
|
||||||
|
.map_or_else(PoisonError::into_inner, |g| g);
|
||||||
|
assert!(
|
||||||
|
deadlock_state
|
||||||
|
.as_ref()
|
||||||
|
.map_or(true, |state| state.locked_thread
|
||||||
|
!= std::thread::current().id()),
|
||||||
|
"deadlocked"
|
||||||
|
);
|
||||||
loop {
|
loop {
|
||||||
|
let state = self
|
||||||
|
.source
|
||||||
|
.state
|
||||||
|
.lock()
|
||||||
|
.map_or_else(PoisonError::into_inner, |g| g);
|
||||||
if state.wrapped.generation != self.read_generation {
|
if state.wrapped.generation != self.read_generation {
|
||||||
return true;
|
return true;
|
||||||
} else if state.readers == Arc::strong_count(&self.source) {
|
} else if state.readers == Arc::strong_count(&self.source) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
drop(state);
|
||||||
|
|
||||||
state = self
|
// Wait for a notification of a change, which is synch
|
||||||
|
deadlock_state = self
|
||||||
.source
|
.source
|
||||||
.sync
|
.sync
|
||||||
.wait(state)
|
.wait(deadlock_state)
|
||||||
.map_or_else(PoisonError::into_inner, |g| g);
|
.map_or_else(PoisonError::into_inner, |g| g);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -508,7 +771,7 @@ impl<T> DynamicReader<T> {
|
||||||
|
|
||||||
impl<T> Clone for DynamicReader<T> {
|
impl<T> Clone for DynamicReader<T> {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
self.source.state().readers += 1;
|
self.source.state().expect("deadlocked").readers += 1;
|
||||||
Self {
|
Self {
|
||||||
source: self.source.clone(),
|
source: self.source.clone(),
|
||||||
read_generation: self.read_generation,
|
read_generation: self.read_generation,
|
||||||
|
|
@ -518,7 +781,7 @@ impl<T> Clone for DynamicReader<T> {
|
||||||
|
|
||||||
impl<T> Drop for DynamicReader<T> {
|
impl<T> Drop for DynamicReader<T> {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
let mut state = self.source.state();
|
let mut state = self.source.state().expect("deadlocked");
|
||||||
state.readers -= 1;
|
state.readers -= 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -535,7 +798,7 @@ impl<'a, T> Future for BlockUntilUpdatedFuture<'a, T> {
|
||||||
type Output = bool;
|
type Output = bool;
|
||||||
|
|
||||||
fn poll(self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
|
fn poll(self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
|
||||||
let mut state = self.0.source.state();
|
let mut state = self.0.source.state().expect("deadlocked");
|
||||||
if state.wrapped.generation != self.0.read_generation {
|
if state.wrapped.generation != self.0.read_generation {
|
||||||
return Poll::Ready(true);
|
return Poll::Ready(true);
|
||||||
} else if state.readers == Arc::strong_count(&self.0.source) {
|
} else if state.readers == Arc::strong_count(&self.0.source) {
|
||||||
|
|
@ -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.
|
/// 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 {
|
pub fn map_mut<R>(&mut self, map: impl FnOnce(&mut T) -> R) -> R {
|
||||||
match self {
|
match self {
|
||||||
|
|
@ -685,6 +962,17 @@ impl<T> Value<T> {
|
||||||
self.map(Clone::clone)
|
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
|
/// Returns the current generation of the data stored, if the contained
|
||||||
/// value is [`Dynamic`].
|
/// value is [`Dynamic`].
|
||||||
pub fn generation(&self) -> Option<Generation> {
|
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::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext};
|
||||||
use crate::styles::components::VisualOrder;
|
use crate::styles::components::VisualOrder;
|
||||||
use crate::styles::{Component, NamedComponent, Styles};
|
use crate::styles::{IntoComponentValue, NamedComponent, Styles, ThemePair};
|
||||||
use crate::tree::Tree;
|
use crate::tree::Tree;
|
||||||
use crate::value::{IntoValue, Value};
|
use crate::value::{IntoValue, Value};
|
||||||
use crate::widgets::{Align, Expand, Scroll, Style};
|
use crate::widgets::{Align, Expand, Scroll, Style};
|
||||||
use crate::window::{RunningWindow, Window, WindowBehavior};
|
use crate::window::{RunningWindow, ThemeMode, Window, WindowBehavior};
|
||||||
use crate::{ConstraintLimit, Run};
|
use crate::{ConstraintLimit, Run};
|
||||||
|
|
||||||
/// A type that makes up a graphical user interface.
|
/// A type that makes up a graphical user interface.
|
||||||
|
|
@ -448,7 +448,7 @@ pub trait MakeWidget: Sized {
|
||||||
/// Associates `styles` with this widget.
|
/// Associates `styles` with this widget.
|
||||||
///
|
///
|
||||||
/// This is equivalent to `Style::new(styles, self)`.
|
/// 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
|
where
|
||||||
Self: Sized,
|
Self: Sized,
|
||||||
{
|
{
|
||||||
|
|
@ -456,7 +456,7 @@ pub trait MakeWidget: Sized {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Associates a style component with `self`.
|
/// 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();
|
let mut styles = Styles::new();
|
||||||
styles.insert(name, component);
|
styles.insert(name, component);
|
||||||
Style::new(styles, self)
|
Style::new(styles, self)
|
||||||
|
|
@ -957,10 +957,22 @@ impl ManagedWidget {
|
||||||
self.tree.parent(self.id()).is_some()
|
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);
|
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) {
|
pub(crate) fn reset_child_layouts(&self) {
|
||||||
self.tree.reset_child_layouts(self.id());
|
self.tree.reset_child_layouts(self.id());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,14 @@ mod canvas;
|
||||||
mod expand;
|
mod expand;
|
||||||
mod input;
|
mod input;
|
||||||
pub mod label;
|
pub mod label;
|
||||||
|
mod mode_switch;
|
||||||
mod resize;
|
mod resize;
|
||||||
pub mod scroll;
|
pub mod scroll;
|
||||||
|
mod slider;
|
||||||
mod space;
|
mod space;
|
||||||
pub mod stack;
|
pub mod stack;
|
||||||
mod style;
|
mod style;
|
||||||
|
mod themed;
|
||||||
mod tilemap;
|
mod tilemap;
|
||||||
|
|
||||||
pub use align::Align;
|
pub use align::Align;
|
||||||
|
|
@ -19,9 +22,12 @@ pub use canvas::Canvas;
|
||||||
pub use expand::Expand;
|
pub use expand::Expand;
|
||||||
pub use input::Input;
|
pub use input::Input;
|
||||||
pub use label::Label;
|
pub use label::Label;
|
||||||
|
pub use mode_switch::ModeSwitch;
|
||||||
pub use resize::Resize;
|
pub use resize::Resize;
|
||||||
pub use scroll::Scroll;
|
pub use scroll::Scroll;
|
||||||
|
pub use slider::Slider;
|
||||||
pub use space::Space;
|
pub use space::Space;
|
||||||
pub use stack::Stack;
|
pub use stack::Stack;
|
||||||
pub use style::Style;
|
pub use style::Style;
|
||||||
|
pub use themed::Themed;
|
||||||
pub use tilemap::TileMap;
|
pub use tilemap::TileMap;
|
||||||
|
|
|
||||||
|
|
@ -5,17 +5,19 @@ use std::time::Duration;
|
||||||
|
|
||||||
use kludgine::app::winit::event::{ElementState, Ime, KeyEvent};
|
use kludgine::app::winit::event::{ElementState, Ime, KeyEvent};
|
||||||
use kludgine::app::winit::keyboard::Key;
|
use kludgine::app::winit::keyboard::Key;
|
||||||
use kludgine::cosmic_text::{Action, Attrs, Buffer, Cursor, Edit, Editor, Metrics, Shaping};
|
use kludgine::cosmic_text::{
|
||||||
use kludgine::figures::units::{Px, UPx};
|
Action, Affinity, Attrs, Buffer, Cursor, Edit, Editor, Metrics, Shaping,
|
||||||
|
};
|
||||||
|
use kludgine::figures::units::{Lp, Px, UPx};
|
||||||
use kludgine::figures::{
|
use kludgine::figures::{
|
||||||
FloatConversion, IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size,
|
FloatConversion, IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size,
|
||||||
};
|
};
|
||||||
use kludgine::shapes::Shape;
|
use kludgine::shapes::{Shape, StrokeOptions};
|
||||||
use kludgine::text::TextOrigin;
|
use kludgine::text::TextOrigin;
|
||||||
use kludgine::{Color, Kludgine};
|
use kludgine::{Color, Kludgine};
|
||||||
|
|
||||||
use crate::context::{EventContext, LayoutContext, WidgetContext};
|
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::styles::Styles;
|
||||||
use crate::utils::ModifiersExt;
|
use crate::utils::ModifiersExt;
|
||||||
use crate::value::{Generation, IntoValue, Value};
|
use crate::value::{Generation, IntoValue, Value};
|
||||||
|
|
@ -32,6 +34,8 @@ pub struct Input {
|
||||||
on_key: Option<Callback<KeyEvent, EventHandling>>,
|
on_key: Option<Callback<KeyEvent, EventHandling>>,
|
||||||
editor: Option<LiveEditor>,
|
editor: Option<LiveEditor>,
|
||||||
cursor_state: CursorState,
|
cursor_state: CursorState,
|
||||||
|
needs_to_select_all: bool,
|
||||||
|
mouse_buttons_down: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Input {
|
impl Input {
|
||||||
|
|
@ -47,6 +51,8 @@ impl Input {
|
||||||
editor: None,
|
editor: None,
|
||||||
cursor_state: CursorState::default(),
|
cursor_state: CursorState::default(),
|
||||||
on_key: None,
|
on_key: None,
|
||||||
|
mouse_buttons_down: 0,
|
||||||
|
needs_to_select_all: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,6 +106,22 @@ impl Input {
|
||||||
fn styles(context: &WidgetContext<'_, '_>) -> Styles {
|
fn styles(context: &WidgetContext<'_, '_>) -> Styles {
|
||||||
context.query_styles(&[&TextColor, &TextSize, &LineHeight])
|
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 {
|
impl Default for Input {
|
||||||
|
|
@ -132,7 +154,9 @@ impl Widget for Input {
|
||||||
_button: kludgine::app::winit::event::MouseButton,
|
_button: kludgine::app::winit::event::MouseButton,
|
||||||
context: &mut EventContext<'_, '_>,
|
context: &mut EventContext<'_, '_>,
|
||||||
) -> EventHandling {
|
) -> EventHandling {
|
||||||
|
self.mouse_buttons_down += 1;
|
||||||
context.focus();
|
context.focus();
|
||||||
|
self.needs_to_select_all = false;
|
||||||
let styles = context.query_styles(&[&TextColor]);
|
let styles = context.query_styles(&[&TextColor]);
|
||||||
self.editor_mut(context.kludgine, &styles, &context.widget)
|
self.editor_mut(context.kludgine, &styles, &context.widget)
|
||||||
.action(
|
.action(
|
||||||
|
|
@ -166,12 +190,22 @@ impl Widget for Input {
|
||||||
context.set_needs_redraw();
|
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)]
|
#[allow(clippy::too_many_lines)]
|
||||||
fn redraw(&mut self, context: &mut crate::context::GraphicsContext<'_, '_, '_, '_, '_>) {
|
fn redraw(&mut self, context: &mut crate::context::GraphicsContext<'_, '_, '_, '_, '_>) {
|
||||||
self.cursor_state.update(context.elapsed());
|
self.cursor_state.update(context.elapsed());
|
||||||
let cursor_state = self.cursor_state;
|
let cursor_state = self.cursor_state;
|
||||||
let size = context.gfx.size();
|
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 highlight = styles.get(&HighlightColor, context);
|
||||||
let editor = self.editor_mut(&mut context.gfx, &styles, &context.widget);
|
let editor = self.editor_mut(&mut context.gfx, &styles, &context.widget);
|
||||||
let cursor = editor.cursor();
|
let cursor = editor.cursor();
|
||||||
|
|
@ -311,6 +345,9 @@ impl Widget for Input {
|
||||||
context.redraw_when_changed(context.window().focused());
|
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);
|
let text_color = styles.get(&TextColor, context);
|
||||||
|
|
@ -330,6 +367,10 @@ impl Widget for Input {
|
||||||
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
|
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
|
||||||
) -> Size<UPx> {
|
) -> Size<UPx> {
|
||||||
let styles = context.query_styles(&[&TextColor]);
|
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 editor = self.editor_mut(&mut context.graphics.gfx, &styles, &context.graphics.widget);
|
||||||
let buffer = editor.buffer_mut();
|
let buffer = editor.buffer_mut();
|
||||||
buffer.set_size(
|
buffer.set_size(
|
||||||
|
|
@ -362,7 +403,7 @@ impl Widget for Input {
|
||||||
// "Keyboard input: {:?}. {:?}, {:?}",
|
// "Keyboard input: {:?}. {:?}, {:?}",
|
||||||
// input.logical_key, input.text, input.physical_key
|
// 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), _) => {
|
(ElementState::Pressed, key @ (Key::Backspace | Key::Delete), _) => {
|
||||||
editor.action(
|
editor.action(
|
||||||
context.kludgine.font_system(),
|
context.kludgine.font_system(),
|
||||||
|
|
@ -400,6 +441,12 @@ impl Widget for Input {
|
||||||
);
|
);
|
||||||
(false, HANDLED)
|
(false, HANDLED)
|
||||||
}
|
}
|
||||||
|
(state, _, Some("a")) if context.modifiers().primary() => {
|
||||||
|
if state.is_pressed() {
|
||||||
|
self.select_all();
|
||||||
|
}
|
||||||
|
(false, HANDLED)
|
||||||
|
}
|
||||||
(state, _, Some(text))
|
(state, _, Some(text))
|
||||||
if !context.modifiers().primary()
|
if !context.modifiers().primary()
|
||||||
&& text != "\t" // tab
|
&& text != "\t" // tab
|
||||||
|
|
@ -408,7 +455,7 @@ impl Widget for Input {
|
||||||
=>
|
=>
|
||||||
{
|
{
|
||||||
if state.is_pressed() {
|
if state.is_pressed() {
|
||||||
editor.insert_string(&text, None);
|
editor.insert_string(text, None);
|
||||||
}
|
}
|
||||||
(state.is_pressed(), HANDLED)
|
(state.is_pressed(), HANDLED)
|
||||||
}
|
}
|
||||||
|
|
@ -456,6 +503,10 @@ impl Widget for Input {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn focus(&mut self, context: &mut EventContext<'_, '_>) {
|
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_ime_allowed(true);
|
||||||
context.set_needs_redraw();
|
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 kludgine::figures::{Fraction, IntoSigned, IntoUnsigned, Rect, ScreenScale, Size};
|
||||||
|
|
||||||
use crate::context::{AsEventContext, LayoutContext};
|
use crate::context::{AsEventContext, LayoutContext};
|
||||||
|
|
@ -81,7 +82,12 @@ impl WrapperWidget for Resize {
|
||||||
);
|
);
|
||||||
context.for_other(&child).layout(available_space)
|
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 {
|
) -> ConstraintLimit {
|
||||||
match constraint {
|
match constraint {
|
||||||
ConstraintLimit::Known(size) => ConstraintLimit::Known(range.clamp(size, scale)),
|
ConstraintLimit::Known(size) => ConstraintLimit::Known(range.clamp(size, scale)),
|
||||||
ConstraintLimit::ClippedAfter(clipped_after) => {
|
ConstraintLimit::ClippedAfter(clipped_after) => match (range.minimum(), range.maximum()) {
|
||||||
ConstraintLimit::ClippedAfter(range.clamp(clipped_after, scale))
|
(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<'_, '_, '_, '_, '_>) {
|
fn redraw(&mut self, context: &mut crate::context::GraphicsContext<'_, '_, '_, '_, '_>) {
|
||||||
context.redraw_when_changed(&self.scrollbar_opacity);
|
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());
|
let managed = self.contents.mounted(&mut context.as_event_context());
|
||||||
context.for_other(&managed).redraw();
|
context.for_other(&managed).redraw();
|
||||||
|
|
||||||
|
let size = context.gfx.region().size;
|
||||||
|
|
||||||
if self.horizontal_bar.amount_hidden > 0 {
|
if self.horizontal_bar.amount_hidden > 0 {
|
||||||
context.gfx.draw_shape(
|
context.gfx.draw_shape(
|
||||||
&Shape::filled_rect(
|
&Shape::filled_rect(
|
||||||
Rect::new(
|
Rect::new(
|
||||||
Point::new(
|
Point::new(self.horizontal_bar.offset, size.height - self.bar_width),
|
||||||
self.horizontal_bar.offset,
|
|
||||||
self.control_size.height - self.bar_width,
|
|
||||||
),
|
|
||||||
Size::new(self.horizontal_bar.size, 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()),
|
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(
|
context.gfx.draw_shape(
|
||||||
&Shape::filled_rect(
|
&Shape::filled_rect(
|
||||||
Rect::new(
|
Rect::new(
|
||||||
Point::new(
|
Point::new(size.width - self.bar_width, self.vertical_bar.offset),
|
||||||
visible_bottom_right.x - self.bar_width,
|
|
||||||
self.vertical_bar.offset,
|
|
||||||
),
|
|
||||||
Size::new(self.bar_width, self.vertical_bar.size),
|
Size::new(self.bar_width, self.vertical_bar.size),
|
||||||
),
|
),
|
||||||
Color::new_f32(1.0, 1.0, 1.0, *self.scrollbar_opacity.get()),
|
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 {
|
if self.enabled.x {
|
||||||
ConstraintLimit::ClippedAfter(UPx::MAX - scroll.x.into_unsigned())
|
ConstraintLimit::ClippedAfter(UPx::MAX - scroll.x.into_unsigned())
|
||||||
} else {
|
} else {
|
||||||
ConstraintLimit::Known(control_size.width.into_unsigned())
|
available_space.width
|
||||||
},
|
},
|
||||||
if self.enabled.y {
|
if self.enabled.y {
|
||||||
ConstraintLimit::ClippedAfter(UPx::MAX - scroll.y.into_unsigned())
|
ConstraintLimit::ClippedAfter(UPx::MAX - scroll.y.into_unsigned())
|
||||||
} else {
|
} else {
|
||||||
ConstraintLimit::Known(control_size.height.into_unsigned())
|
available_space.height
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
let managed = self.contents.mounted(&mut context.as_event_context());
|
let managed = self.contents.mounted(&mut context.as_event_context());
|
||||||
|
|
@ -255,7 +247,22 @@ impl Widget for Scroll {
|
||||||
);
|
);
|
||||||
context.set_child_layout(&managed, region);
|
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(
|
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>() {
|
if let Some(expand) = guard.downcast_ref::<Expand>() {
|
||||||
let weight = expand.weight;
|
let weight = expand.weight;
|
||||||
(
|
(
|
||||||
WidgetRef::Unmounted(widget.clone()),
|
expand.child().clone(),
|
||||||
StackDimension::Fractional { weight },
|
StackDimension::Fractional { weight },
|
||||||
)
|
)
|
||||||
} else if let Some((child, size)) =
|
} else if let Some((child, size)) =
|
||||||
|
|
@ -403,18 +403,14 @@ impl Layout {
|
||||||
// Measure the children that fit their content
|
// Measure the children that fit their content
|
||||||
for &id in &self.measured {
|
for &id in &self.measured {
|
||||||
let index = self.children.index_of_id(id).expect("child not found");
|
let index = self.children.index_of_id(id).expect("child not found");
|
||||||
if remaining > 0 {
|
let (measured, _) = self.orientation.split_size(measure(
|
||||||
let (measured, _) = self.orientation.split_size(measure(
|
index,
|
||||||
index,
|
self.orientation
|
||||||
self.orientation
|
.make_size(ConstraintLimit::ClippedAfter(remaining), other_constraint),
|
||||||
.make_size(ConstraintLimit::ClippedAfter(remaining), other_constraint),
|
false,
|
||||||
false,
|
));
|
||||||
));
|
self.layouts[index].size = measured;
|
||||||
self.layouts[index].size = measured;
|
remaining = remaining.saturating_sub(measured);
|
||||||
remaining = remaining.saturating_sub(measured);
|
|
||||||
} else {
|
|
||||||
self.layouts[index].size = UPx(0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Measure the weighted children within the remaining space
|
// Measure the weighted children within the remaining space
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,21 @@
|
||||||
use crate::context::EventContext;
|
use crate::context::EventContext;
|
||||||
use crate::styles::Styles;
|
use crate::styles::Styles;
|
||||||
|
use crate::value::{IntoValue, Value};
|
||||||
use crate::widget::{MakeWidget, WidgetRef, WrapperWidget};
|
use crate::widget::{MakeWidget, WidgetRef, WrapperWidget};
|
||||||
|
|
||||||
/// A widget that applies a set of [`Styles`] to all contained widgets.
|
/// A widget that applies a set of [`Styles`] to all contained widgets.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Style {
|
pub struct Style {
|
||||||
styles: Styles,
|
styles: Value<Styles>,
|
||||||
child: WidgetRef,
|
child: WidgetRef,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Style {
|
impl Style {
|
||||||
/// Returns a new widget that applies `styles` to `child` and any children
|
/// Returns a new widget that applies `styles` to `child` and any children
|
||||||
/// it may have.
|
/// 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 {
|
Self {
|
||||||
styles: styles.into(),
|
styles: styles.into_value(),
|
||||||
child: WidgetRef::new(child),
|
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,
|
DeviceId, ElementState, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase,
|
||||||
};
|
};
|
||||||
use kludgine::app::winit::keyboard::Key;
|
use kludgine::app::winit::keyboard::Key;
|
||||||
|
use kludgine::app::winit::window;
|
||||||
use kludgine::app::WindowBehavior as _;
|
use kludgine::app::WindowBehavior as _;
|
||||||
use kludgine::figures::units::{Px, UPx};
|
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::render::Drawing;
|
||||||
|
use kludgine::wgpu::CompositeAlphaMode;
|
||||||
use kludgine::Kludgine;
|
use kludgine::Kludgine;
|
||||||
use tracing::Level;
|
use tracing::Level;
|
||||||
|
|
||||||
|
use crate::animation::{LinearInterpolate, PercentBetween, ZeroToOne};
|
||||||
use crate::context::{
|
use crate::context::{
|
||||||
AsEventContext, EventContext, Exclusive, GraphicsContext, LayoutContext, RedrawStatus,
|
AsEventContext, EventContext, Exclusive, GraphicsContext, LayoutContext, RedrawStatus,
|
||||||
WidgetContext,
|
WidgetContext,
|
||||||
|
|
@ -31,7 +34,7 @@ use crate::styles::components::LayoutOrder;
|
||||||
use crate::styles::ThemePair;
|
use crate::styles::ThemePair;
|
||||||
use crate::tree::Tree;
|
use crate::tree::Tree;
|
||||||
use crate::utils::ModifiersExt;
|
use crate::utils::ModifiersExt;
|
||||||
use crate::value::{Dynamic, DynamicReader, IntoDynamic, Value};
|
use crate::value::{Dynamic, DynamicReader, IntoDynamic, IntoValue, Value};
|
||||||
use crate::widget::{
|
use crate::widget::{
|
||||||
EventHandling, ManagedWidget, Widget, WidgetId, WidgetInstance, HANDLED, IGNORED,
|
EventHandling, ManagedWidget, Widget, WidgetId, WidgetInstance, HANDLED, IGNORED,
|
||||||
};
|
};
|
||||||
|
|
@ -104,6 +107,7 @@ where
|
||||||
pub theme: Value<ThemePair>,
|
pub theme: Value<ThemePair>,
|
||||||
occluded: Option<Dynamic<bool>>,
|
occluded: Option<Dynamic<bool>>,
|
||||||
focused: Option<Dynamic<bool>>,
|
focused: Option<Dynamic<bool>>,
|
||||||
|
theme_mode: Option<Value<ThemeMode>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Behavior> Default for Window<Behavior>
|
impl<Behavior> Default for Window<Behavior>
|
||||||
|
|
@ -155,6 +159,24 @@ impl Window<WidgetInstance> {
|
||||||
self.occluded = Some(occluded);
|
self.occluded = Some(occluded);
|
||||||
self
|
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>
|
impl<Behavior> Window<Behavior>
|
||||||
|
|
@ -188,6 +210,7 @@ where
|
||||||
theme: Value::default(),
|
theme: Value::default(),
|
||||||
occluded: None,
|
occluded: None,
|
||||||
focused: None,
|
focused: None,
|
||||||
|
theme_mode: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -201,10 +224,12 @@ where
|
||||||
GooeyWindow::<Behavior>::run_with(AssertUnwindSafe(sealed::Context {
|
GooeyWindow::<Behavior>::run_with(AssertUnwindSafe(sealed::Context {
|
||||||
user: self.context,
|
user: self.context,
|
||||||
settings: RefCell::new(sealed::WindowSettings {
|
settings: RefCell::new(sealed::WindowSettings {
|
||||||
|
transparent: self.attributes.transparent,
|
||||||
attributes: Some(self.attributes),
|
attributes: Some(self.attributes),
|
||||||
occluded: self.occluded,
|
occluded: self.occluded,
|
||||||
focused: self.focused,
|
focused: self.focused,
|
||||||
theme: Some(self.theme),
|
theme: Some(self.theme),
|
||||||
|
theme_mode: self.theme_mode,
|
||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
@ -258,6 +283,8 @@ struct GooeyWindow<T> {
|
||||||
max_inner_size: Option<Size<UPx>>,
|
max_inner_size: Option<Size<UPx>>,
|
||||||
theme: Option<DynamicReader<ThemePair>>,
|
theme: Option<DynamicReader<ThemePair>>,
|
||||||
current_theme: ThemePair,
|
current_theme: ThemePair,
|
||||||
|
theme_mode: Dynamic<ThemeMode>,
|
||||||
|
transparent: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> GooeyWindow<T>
|
impl<T> GooeyWindow<T>
|
||||||
|
|
@ -286,6 +313,7 @@ where
|
||||||
&self.redraw_status,
|
&self.redraw_status,
|
||||||
&self.current_theme,
|
&self.current_theme,
|
||||||
window,
|
window,
|
||||||
|
self.theme_mode.get(),
|
||||||
),
|
),
|
||||||
kludgine,
|
kludgine,
|
||||||
)
|
)
|
||||||
|
|
@ -297,6 +325,7 @@ where
|
||||||
&self.redraw_status,
|
&self.redraw_status,
|
||||||
&self.current_theme,
|
&self.current_theme,
|
||||||
window,
|
window,
|
||||||
|
self.theme_mode.get(),
|
||||||
),
|
),
|
||||||
kludgine,
|
kludgine,
|
||||||
)
|
)
|
||||||
|
|
@ -310,6 +339,7 @@ where
|
||||||
&self.redraw_status,
|
&self.redraw_status,
|
||||||
&self.current_theme,
|
&self.current_theme,
|
||||||
window,
|
window,
|
||||||
|
self.theme_mode.get(),
|
||||||
),
|
),
|
||||||
kludgine,
|
kludgine,
|
||||||
)
|
)
|
||||||
|
|
@ -348,7 +378,7 @@ where
|
||||||
let new_min_size = (min_width > 0 || min_height > 0)
|
let new_min_size = (min_width > 0 || min_height > 0)
|
||||||
.then_some(Size::<Px>::new(min_width, min_height).into_unsigned());
|
.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);
|
window.set_min_inner_size(new_min_size);
|
||||||
self.min_inner_size = new_min_size;
|
self.min_inner_size = new_min_size;
|
||||||
}
|
}
|
||||||
|
|
@ -405,6 +435,15 @@ where
|
||||||
.theme
|
.theme
|
||||||
.take()
|
.take()
|
||||||
.expect("theme always present");
|
.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(
|
let mut behavior = T::initialize(
|
||||||
&mut RunningWindow::new(window, &focused, &occluded),
|
&mut RunningWindow::new(window, &focused, &occluded),
|
||||||
context.user,
|
context.user,
|
||||||
|
|
@ -435,6 +474,8 @@ where
|
||||||
max_inner_size: None,
|
max_inner_size: None,
|
||||||
current_theme,
|
current_theme,
|
||||||
theme,
|
theme,
|
||||||
|
theme_mode,
|
||||||
|
transparent,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -467,14 +508,19 @@ where
|
||||||
&self.redraw_status,
|
&self.redraw_status,
|
||||||
&self.current_theme,
|
&self.current_theme,
|
||||||
&mut window,
|
&mut window,
|
||||||
|
self.theme_mode.get(),
|
||||||
),
|
),
|
||||||
gfx: Exclusive::Owned(Graphics::new(graphics)),
|
gfx: Exclusive::Owned(Graphics::new(graphics)),
|
||||||
};
|
};
|
||||||
|
context.redraw_when_changed(&self.theme_mode);
|
||||||
let mut layout_context = LayoutContext::new(&mut context);
|
let mut layout_context = LayoutContext::new(&mut context);
|
||||||
let window_size = layout_context.gfx.size();
|
let window_size = layout_context.gfx.size();
|
||||||
|
|
||||||
let background_color = layout_context.theme().surface.color;
|
if !self.transparent {
|
||||||
layout_context.graphics.gfx.fill(background_color);
|
let background_color = layout_context.theme().surface.color;
|
||||||
|
layout_context.graphics.gfx.fill(background_color);
|
||||||
|
}
|
||||||
|
|
||||||
let actual_size = layout_context.layout(if is_expanded {
|
let actual_size = layout_context.layout(if is_expanded {
|
||||||
Size::new(
|
Size::new(
|
||||||
ConstraintLimit::Known(window_size.width),
|
ConstraintLimit::Known(window_size.width),
|
||||||
|
|
@ -549,12 +595,16 @@ where
|
||||||
fn initial_window_attributes(
|
fn initial_window_attributes(
|
||||||
context: &Self::Context,
|
context: &Self::Context,
|
||||||
) -> kludgine::app::WindowAttributes<WindowCommand> {
|
) -> kludgine::app::WindowAttributes<WindowCommand> {
|
||||||
context
|
let mut attrs = context
|
||||||
.settings
|
.settings
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
.attributes
|
.attributes
|
||||||
.take()
|
.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(
|
fn close_requested(
|
||||||
|
|
@ -577,9 +627,21 @@ where
|
||||||
// wgpu::Limits::downlevel_webgl2_defaults().using_resolution(adapter_limits)
|
// wgpu::Limits::downlevel_webgl2_defaults().using_resolution(adapter_limits)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// fn clear_color() -> Option<kludgine::Color> {
|
fn clear_color(&self) -> Option<kludgine::Color> {
|
||||||
// Some(kludgine::Color::BLACK)
|
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<'_, ()>) {}
|
// fn focus_changed(&mut self, window: kludgine::app::Window<'_, ()>) {}
|
||||||
|
|
||||||
|
|
@ -616,6 +678,7 @@ where
|
||||||
&self.redraw_status,
|
&self.redraw_status,
|
||||||
&self.current_theme,
|
&self.current_theme,
|
||||||
&mut window,
|
&mut window,
|
||||||
|
self.theme_mode.get(),
|
||||||
),
|
),
|
||||||
kludgine,
|
kludgine,
|
||||||
);
|
);
|
||||||
|
|
@ -645,6 +708,7 @@ where
|
||||||
&self.redraw_status,
|
&self.redraw_status,
|
||||||
&self.current_theme,
|
&self.current_theme,
|
||||||
&mut window,
|
&mut window,
|
||||||
|
self.theme_mode.get(),
|
||||||
),
|
),
|
||||||
kludgine,
|
kludgine,
|
||||||
);
|
);
|
||||||
|
|
@ -711,6 +775,7 @@ where
|
||||||
&self.redraw_status,
|
&self.redraw_status,
|
||||||
&self.current_theme,
|
&self.current_theme,
|
||||||
&mut window,
|
&mut window,
|
||||||
|
self.theme_mode.get(),
|
||||||
),
|
),
|
||||||
kludgine,
|
kludgine,
|
||||||
);
|
);
|
||||||
|
|
@ -745,6 +810,7 @@ where
|
||||||
&self.redraw_status,
|
&self.redraw_status,
|
||||||
&self.current_theme,
|
&self.current_theme,
|
||||||
&mut window,
|
&mut window,
|
||||||
|
self.theme_mode.get(),
|
||||||
),
|
),
|
||||||
kludgine,
|
kludgine,
|
||||||
);
|
);
|
||||||
|
|
@ -773,6 +839,7 @@ where
|
||||||
&self.redraw_status,
|
&self.redraw_status,
|
||||||
&self.current_theme,
|
&self.current_theme,
|
||||||
&mut window,
|
&mut window,
|
||||||
|
self.theme_mode.get(),
|
||||||
),
|
),
|
||||||
kludgine,
|
kludgine,
|
||||||
);
|
);
|
||||||
|
|
@ -787,6 +854,7 @@ where
|
||||||
&self.redraw_status,
|
&self.redraw_status,
|
||||||
&self.current_theme,
|
&self.current_theme,
|
||||||
&mut window,
|
&mut window,
|
||||||
|
self.theme_mode.get(),
|
||||||
),
|
),
|
||||||
kludgine,
|
kludgine,
|
||||||
);
|
);
|
||||||
|
|
@ -827,6 +895,7 @@ where
|
||||||
&self.redraw_status,
|
&self.redraw_status,
|
||||||
&self.current_theme,
|
&self.current_theme,
|
||||||
&mut window,
|
&mut window,
|
||||||
|
self.theme_mode.get(),
|
||||||
),
|
),
|
||||||
kludgine,
|
kludgine,
|
||||||
);
|
);
|
||||||
|
|
@ -851,6 +920,7 @@ where
|
||||||
&self.redraw_status,
|
&self.redraw_status,
|
||||||
&self.current_theme,
|
&self.current_theme,
|
||||||
&mut window,
|
&mut window,
|
||||||
|
self.theme_mode.get(),
|
||||||
),
|
),
|
||||||
kludgine,
|
kludgine,
|
||||||
)
|
)
|
||||||
|
|
@ -866,6 +936,7 @@ where
|
||||||
&self.redraw_status,
|
&self.redraw_status,
|
||||||
&self.current_theme,
|
&self.current_theme,
|
||||||
&mut window,
|
&mut window,
|
||||||
|
self.theme_mode.get(),
|
||||||
),
|
),
|
||||||
kludgine,
|
kludgine,
|
||||||
),
|
),
|
||||||
|
|
@ -900,6 +971,7 @@ where
|
||||||
&self.redraw_status,
|
&self.redraw_status,
|
||||||
&self.current_theme,
|
&self.current_theme,
|
||||||
&mut window,
|
&mut window,
|
||||||
|
self.theme_mode.get(),
|
||||||
),
|
),
|
||||||
kludgine,
|
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(
|
fn event(
|
||||||
&mut self,
|
&mut self,
|
||||||
mut window: kludgine::app::Window<'_, WindowCommand>,
|
mut window: kludgine::app::Window<'_, WindowCommand>,
|
||||||
|
|
@ -955,7 +1035,7 @@ pub(crate) mod sealed {
|
||||||
|
|
||||||
use crate::styles::ThemePair;
|
use crate::styles::ThemePair;
|
||||||
use crate::value::{Dynamic, Value};
|
use crate::value::{Dynamic, Value};
|
||||||
use crate::window::WindowAttributes;
|
use crate::window::{ThemeMode, WindowAttributes};
|
||||||
|
|
||||||
pub struct Context<C> {
|
pub struct Context<C> {
|
||||||
pub user: C,
|
pub user: C,
|
||||||
|
|
@ -967,6 +1047,8 @@ pub(crate) mod sealed {
|
||||||
pub occluded: Option<Dynamic<bool>>,
|
pub occluded: Option<Dynamic<bool>>,
|
||||||
pub focused: Option<Dynamic<bool>>,
|
pub focused: Option<Dynamic<bool>>,
|
||||||
pub theme: Option<Value<ThemePair>>,
|
pub theme: Option<Value<ThemePair>>,
|
||||||
|
pub theme_mode: Option<Value<ThemeMode>>,
|
||||||
|
pub transparent: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum WindowCommand {
|
pub enum WindowCommand {
|
||||||
|
|
@ -974,3 +1056,56 @@ pub(crate) mod sealed {
|
||||||
// RequestClose,
|
// 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