cushy/examples/theme.rs
Jonathan Johnson 3f8885efbe
Callback handles are now managed
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.
2023-12-01 13:31:42 -08:00

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()
}