mirror of
https://github.com/danbulant/cushy
synced 2026-06-12 19:11:21 +00:00
Installing a callback now returns a CallbackHandle. All map-style APIs install this handle automatically on the created dynamic, which keeps the callback installed until the dynamic is freed. All other APIs return the handle for the caller to either call persist() or store somewhere. Now, the dynamic system can be used for application-long data with almost no fear of leaking data due to how callbacks are being installed. Technically cycles are still possible by moving clones into the callbacks, so a WeakDynamic type might be worth exposing.
418 lines
12 KiB
Rust
418 lines
12 KiB
Rust
use gooey::styles::components::{TextColor, WidgetBackground};
|
|
use gooey::styles::{
|
|
ColorScheme, ColorSchemeBuilder, ColorSource, ColorTheme, FixedTheme, SurfaceTheme, Theme,
|
|
ThemePair,
|
|
};
|
|
use gooey::value::{Dynamic, MapEachCloned};
|
|
use gooey::widget::MakeWidget;
|
|
use gooey::widgets::checkbox::Checkable;
|
|
use gooey::widgets::input::InputValue;
|
|
use gooey::widgets::slider::Slidable;
|
|
use gooey::widgets::Space;
|
|
use gooey::window::ThemeMode;
|
|
use gooey::Run;
|
|
use kludgine::figures::units::Lp;
|
|
use kludgine::Color;
|
|
use palette::OklabHue;
|
|
|
|
struct Scheme<Primary, Other = Primary> {
|
|
primary: Primary,
|
|
secondary: Other,
|
|
tertiary: Other,
|
|
error: Other,
|
|
neutral: Other,
|
|
neutral_variant: Other,
|
|
}
|
|
|
|
impl From<ColorScheme> for Scheme<ColorSource> {
|
|
fn from(scheme: ColorScheme) -> Self {
|
|
Self {
|
|
primary: scheme.primary,
|
|
secondary: scheme.secondary,
|
|
tertiary: scheme.tertiary,
|
|
error: scheme.error,
|
|
neutral: scheme.neutral,
|
|
neutral_variant: scheme.neutral_variant,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<T> Scheme<T> {
|
|
pub fn map<R>(&self, mut map: impl FnMut(T) -> R) -> Scheme<R>
|
|
where
|
|
T: Clone,
|
|
{
|
|
Scheme {
|
|
primary: map(self.primary.clone()),
|
|
secondary: map(self.secondary.clone()),
|
|
tertiary: map(self.tertiary.clone()),
|
|
error: map(self.error.clone()),
|
|
neutral: map(self.neutral.clone()),
|
|
neutral_variant: map(self.neutral_variant.clone()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<Primary, Other> Scheme<Primary, Other> {
|
|
pub fn map_labeled<NewPrimary, NewOther>(
|
|
&self,
|
|
primary: impl FnOnce(Primary) -> NewPrimary,
|
|
mut map: impl FnMut(&str, Other) -> NewOther,
|
|
) -> Scheme<NewPrimary, NewOther>
|
|
where
|
|
Primary: Clone,
|
|
Other: Clone,
|
|
{
|
|
Scheme {
|
|
primary: primary(self.primary.clone()),
|
|
secondary: map("Secondary", self.secondary.clone()),
|
|
tertiary: map("Tertiary", self.tertiary.clone()),
|
|
error: map("Error", self.error.clone()),
|
|
neutral: map("Netural", self.neutral.clone()),
|
|
neutral_variant: map("Neutral Variant", self.neutral_variant.clone()),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn main() -> gooey::Result {
|
|
let (theme_mode, theme_switcher) = dark_mode_picker();
|
|
|
|
let scheme = Scheme::from(ColorScheme::default());
|
|
let sources = scheme.map(Dynamic::new);
|
|
let editors = sources.map_labeled(
|
|
|primary| {
|
|
swatch_label("Primary", &primary)
|
|
.and(color_editor(&primary))
|
|
.into_rows()
|
|
.make_widget()
|
|
},
|
|
|label, source| {
|
|
let (enabled, editor) = optional_editor(label, &source);
|
|
let opt_color =
|
|
(&enabled, &source).map_each_cloned(|(enabled, source)| enabled.then_some(source));
|
|
(opt_color, editor)
|
|
},
|
|
);
|
|
let color_scheme = (
|
|
&sources.primary,
|
|
&editors.secondary.0,
|
|
&editors.tertiary.0,
|
|
&editors.error.0,
|
|
&editors.neutral.0,
|
|
&editors.neutral_variant.0,
|
|
)
|
|
.map_each_cloned(
|
|
move |(primary, secondary, tertiary, error, neutral, neutral_variant)| {
|
|
let mut scheme = ColorSchemeBuilder::new(primary);
|
|
scheme.secondary = secondary;
|
|
scheme.tertiary = tertiary;
|
|
scheme.error = error;
|
|
scheme.neutral = neutral;
|
|
scheme.neutral_variant = neutral_variant;
|
|
scheme.build()
|
|
},
|
|
);
|
|
color_scheme
|
|
.for_each_cloned(move |scheme| {
|
|
sources.primary.set(scheme.primary);
|
|
sources.secondary.set(scheme.secondary);
|
|
sources.tertiary.set(scheme.tertiary);
|
|
sources.error.set(scheme.error);
|
|
sources.neutral.set(scheme.neutral);
|
|
sources.neutral_variant.set(scheme.neutral_variant);
|
|
})
|
|
.persist();
|
|
let theme = color_scheme.map_each_cloned(ThemePair::from);
|
|
|
|
let editors = theme_switcher
|
|
.and(editors.primary)
|
|
.and(editors.secondary.1)
|
|
.and(editors.tertiary.1)
|
|
.and(editors.error.1)
|
|
.and(editors.neutral.1)
|
|
.and(editors.neutral_variant.1)
|
|
.into_rows()
|
|
.vertical_scroll();
|
|
|
|
editors
|
|
.and(fixed_themes(
|
|
theme.map_each(|theme| theme.primary_fixed),
|
|
theme.map_each(|theme| theme.secondary_fixed),
|
|
theme.map_each(|theme| theme.tertiary_fixed),
|
|
))
|
|
.and(theme_preview(
|
|
theme.map_each(|theme| theme.dark),
|
|
ThemeMode::Dark,
|
|
))
|
|
.and(theme_preview(
|
|
theme.map_each(|theme| theme.light),
|
|
ThemeMode::Light,
|
|
))
|
|
.into_columns()
|
|
.themed(theme)
|
|
.pad()
|
|
.expand()
|
|
.into_window()
|
|
.themed_mode(theme_mode)
|
|
.run()
|
|
}
|
|
|
|
fn dark_mode_picker() -> (Dynamic<ThemeMode>, impl MakeWidget) {
|
|
let dark = Dynamic::new(true);
|
|
let theme_mode = dark.map_each(|dark| {
|
|
if *dark {
|
|
ThemeMode::Dark
|
|
} else {
|
|
ThemeMode::Light
|
|
}
|
|
});
|
|
|
|
(theme_mode.clone(), dark.into_checkbox("Dark Mode"))
|
|
}
|
|
|
|
fn swatch_label(label: &str, color: &Dynamic<ColorSource>) -> impl MakeWidget {
|
|
Space::colored(color.map_each(|source| source.color(0.5)))
|
|
.width(Lp::mm(1))
|
|
.and(label)
|
|
.into_columns()
|
|
}
|
|
|
|
fn optional_editor(label: &str, color: &Dynamic<ColorSource>) -> (Dynamic<bool>, impl MakeWidget) {
|
|
let enabled = Dynamic::new(false);
|
|
let hide_editor = enabled.map_each(|enabled| !enabled);
|
|
|
|
(
|
|
enabled.clone(),
|
|
enabled
|
|
.clone()
|
|
.into_checkbox(swatch_label(label, color))
|
|
.and(color_editor(color).collapse_vertically(hide_editor))
|
|
.into_rows(),
|
|
)
|
|
}
|
|
|
|
fn color_editor(color: &Dynamic<ColorSource>) -> impl MakeWidget {
|
|
let hue = color.map_each(|color| color.hue.into_positive_degrees());
|
|
hue.for_each_cloned({
|
|
let color = color.clone();
|
|
move |hue| {
|
|
let mut source = color.get();
|
|
source.hue = OklabHue::new(hue);
|
|
color.set(source);
|
|
}
|
|
})
|
|
.persist();
|
|
|
|
let hue_text = hue.linked_string();
|
|
let saturation = color.map_each(|color| color.saturation);
|
|
saturation
|
|
.for_each_cloned({
|
|
let color = color.clone();
|
|
move |saturation| {
|
|
let mut source = color.get();
|
|
source.saturation = saturation;
|
|
color.set(source);
|
|
}
|
|
})
|
|
.persist();
|
|
let saturation_text = saturation.linked_string();
|
|
|
|
hue.slider_between(0., 359.99)
|
|
.and(hue_text.into_input())
|
|
.and(saturation.slider())
|
|
.and(saturation_text.into_input())
|
|
.into_rows()
|
|
}
|
|
|
|
fn fixed_themes(
|
|
primary: Dynamic<FixedTheme>,
|
|
secondary: Dynamic<FixedTheme>,
|
|
tertiary: Dynamic<FixedTheme>,
|
|
) -> impl MakeWidget {
|
|
"Fixed"
|
|
.and(fixed_theme(primary, "Primary"))
|
|
.and(fixed_theme(secondary, "Secondary"))
|
|
.and(fixed_theme(tertiary, "Tertiary"))
|
|
.into_rows()
|
|
.contain()
|
|
.expand()
|
|
}
|
|
|
|
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);
|
|
|
|
swatch(color.clone(), &format!("{label} Fixed"), on_color.clone())
|
|
.and(swatch(
|
|
theme.map_each(|theme| theme.dim_color),
|
|
&format!("Dim {label}"),
|
|
on_color.clone(),
|
|
))
|
|
.and(swatch(
|
|
on_color.clone(),
|
|
&format!("On {label} Fixed"),
|
|
color.clone(),
|
|
))
|
|
.and(swatch(
|
|
theme.map_each(|theme| theme.on_color_variant),
|
|
&format!("Variant On {label} Fixed"),
|
|
color,
|
|
))
|
|
.into_columns()
|
|
.contain()
|
|
.expand()
|
|
}
|
|
|
|
fn theme_preview(theme: Dynamic<Theme>, mode: ThemeMode) -> impl MakeWidget {
|
|
match mode {
|
|
ThemeMode::Light => "Light",
|
|
ThemeMode::Dark => "Dark",
|
|
}
|
|
.and(
|
|
color_theme(theme.map_each(|theme| theme.primary), "Primary")
|
|
.and(color_theme(
|
|
theme.map_each(|theme| theme.secondary),
|
|
"Secondary",
|
|
))
|
|
.and(color_theme(
|
|
theme.map_each(|theme| theme.tertiary),
|
|
"Tertiary",
|
|
))
|
|
.and(color_theme(theme.map_each(|theme| theme.error), "Error"))
|
|
.into_columns()
|
|
.contain()
|
|
.expand(),
|
|
)
|
|
.and(surface_theme(theme.map_each(|theme| theme.surface)))
|
|
.into_rows()
|
|
.contain()
|
|
.themed_mode(mode)
|
|
.expand()
|
|
}
|
|
|
|
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);
|
|
|
|
swatch(color.clone(), "Surface", on_color.clone())
|
|
.and(swatch(
|
|
theme.map_each(|theme| theme.bright_color),
|
|
"Bright Surface",
|
|
on_color.clone(),
|
|
))
|
|
.and(swatch(
|
|
theme.map_each(|theme| theme.dim_color),
|
|
"Dim Surface",
|
|
on_color.clone(),
|
|
))
|
|
.into_columns()
|
|
.contain()
|
|
.expand()
|
|
.and(
|
|
swatch(
|
|
theme.map_each(|theme| theme.lowest_container),
|
|
"Lowest Container",
|
|
on_color.clone(),
|
|
)
|
|
.and(swatch(
|
|
theme.map_each(|theme| theme.low_container),
|
|
"Low Container",
|
|
on_color.clone(),
|
|
))
|
|
.and(swatch(
|
|
theme.map_each(|theme| theme.container),
|
|
"Container",
|
|
on_color.clone(),
|
|
))
|
|
.and(swatch(
|
|
theme.map_each(|theme| theme.high_container),
|
|
"High Container",
|
|
on_color.clone(),
|
|
))
|
|
.and(swatch(
|
|
theme.map_each(|theme| theme.highest_container),
|
|
"Highest Container",
|
|
on_color.clone(),
|
|
))
|
|
.into_columns()
|
|
.contain()
|
|
.expand(),
|
|
)
|
|
.and(
|
|
swatch(on_color.clone(), "On Surface", color.clone())
|
|
.and(swatch(
|
|
theme.map_each(|theme| theme.on_color_variant),
|
|
"On Color Variant",
|
|
color.clone(),
|
|
))
|
|
.and(swatch(
|
|
theme.map_each(|theme| theme.outline),
|
|
"Outline",
|
|
color.clone(),
|
|
))
|
|
.and(swatch(
|
|
theme.map_each(|theme| theme.outline_variant),
|
|
"Outline Variant",
|
|
color,
|
|
))
|
|
.and(swatch(
|
|
theme.map_each(|theme| theme.opaque_widget),
|
|
"Opaque Widget",
|
|
on_color,
|
|
))
|
|
.into_columns()
|
|
.contain()
|
|
.expand(),
|
|
)
|
|
.into_rows()
|
|
.contain()
|
|
.expand()
|
|
}
|
|
|
|
fn color_theme(theme: Dynamic<ColorTheme>, label: &str) -> impl MakeWidget {
|
|
let color = theme.map_each(|theme| theme.color);
|
|
let dim_color = theme.map_each(|theme| theme.color_dim);
|
|
let bright_color = theme.map_each(|theme| theme.color_bright);
|
|
let on_color = theme.map_each(|theme| theme.on_color);
|
|
let container = theme.map_each(|theme| theme.container);
|
|
let on_container = theme.map_each(|theme| theme.on_container);
|
|
|
|
swatch(color.clone(), label, on_color.clone())
|
|
.and(swatch(
|
|
dim_color.clone(),
|
|
&format!("{label} Dim"),
|
|
on_color.clone(),
|
|
))
|
|
.and(swatch(
|
|
bright_color.clone(),
|
|
&format!("{label} bright"),
|
|
on_color.clone(),
|
|
))
|
|
.and(swatch(
|
|
on_color.clone(),
|
|
&format!("On {label}"),
|
|
color.clone(),
|
|
))
|
|
.and(swatch(
|
|
container.clone(),
|
|
&format!("{label} Container"),
|
|
on_container.clone(),
|
|
))
|
|
.and(swatch(
|
|
on_container,
|
|
&format!("On {label} Container"),
|
|
container,
|
|
))
|
|
.into_rows()
|
|
.contain()
|
|
.expand()
|
|
}
|
|
|
|
fn swatch(background: Dynamic<Color>, label: &str, text: Dynamic<Color>) -> impl MakeWidget {
|
|
label
|
|
.with(&TextColor, text)
|
|
.with(&WidgetBackground, background)
|
|
.fit_horizontally()
|
|
.fit_vertically()
|
|
.expand()
|
|
}
|