mirror of
https://github.com/danbulant/cushy
synced 2026-06-15 04:21:06 +00:00
Refs #98 This refactor overhauls the reactive system to move all the reactive methods to traits. The side effect of this change is that now DynamicReader's API is the same as Dynamic's API, but because it only implements Source<T>, DynamicReader does not offer any mutation functions. While it's unfortunate to have more traits to include to use Cushy, this seems like the best option, and it offers a path to try to integrate this into the tuple ForEach/MapEach traits. Unfortunately, my attempt at doing those in this set of changes led to issues specifying generic associated lifetimes for the DynamicGuard. But, I was also in the middle of this larger refactoring, so it might be that a fresh attempt will succeed.
497 lines
15 KiB
Rust
497 lines
15 KiB
Rust
use std::fmt::Write;
|
|
|
|
use cushy::styles::components::{TextColor, WidgetBackground};
|
|
use cushy::styles::{
|
|
ColorScheme, ColorSchemeBuilder, ColorSource, ColorTheme, FixedTheme, SurfaceTheme, Theme,
|
|
ThemePair,
|
|
};
|
|
use cushy::value::{Destination, Dynamic, MapEachCloned, Source};
|
|
use cushy::widget::MakeWidget;
|
|
use cushy::widgets::checkbox::Checkable;
|
|
use cushy::widgets::color::ColorSourcePicker;
|
|
use cushy::widgets::input::InputValue;
|
|
use cushy::widgets::slider::Slidable;
|
|
use cushy::widgets::Space;
|
|
use cushy::window::ThemeMode;
|
|
use cushy::{Open, PendingApp};
|
|
use figures::units::Lp;
|
|
use kludgine::Color;
|
|
use palette::OklabHue;
|
|
|
|
fn main() -> cushy::Result {
|
|
let app = PendingApp::default();
|
|
|
|
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_builder = (
|
|
&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
|
|
},
|
|
);
|
|
let color_scheme = color_scheme_builder.map_each_cloned(|builder| builder.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)
|
|
.and("Copy to Clipboard".into_button().on_click({
|
|
let cushy = app.cushy().clone();
|
|
move |()| {
|
|
if let Some(mut clipboard) = cushy.clipboard_guard() {
|
|
let builder = color_scheme_builder.get();
|
|
let mut source = String::default();
|
|
builder.format_rust_into(&mut source);
|
|
|
|
if let Err(err) = clipboard.set_text(&source) {
|
|
tracing::error!("Error setting clipboard text: {err}");
|
|
println!("{source}");
|
|
}
|
|
}
|
|
}
|
|
}))
|
|
.into_rows()
|
|
.pad()
|
|
.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_in(app)
|
|
}
|
|
|
|
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 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| {
|
|
if let Ok(mut source) = color.try_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| {
|
|
if let Ok(mut source) = color.try_get() {
|
|
source.saturation = saturation;
|
|
color.set(source);
|
|
}
|
|
}
|
|
})
|
|
.persist();
|
|
let saturation_text = saturation.linked_string();
|
|
|
|
ColorSourcePicker::new(color.clone())
|
|
.height(Lp::points(100))
|
|
.fit_horizontally()
|
|
.and(hue.slider_between(0., 360.))
|
|
.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()
|
|
.expand()
|
|
.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()
|
|
}
|
|
|
|
trait FormatRust {
|
|
fn format_rust_into(&self, out: &mut String);
|
|
}
|
|
|
|
impl FormatRust for ColorSource {
|
|
fn format_rust_into(&self, out: &mut String) {
|
|
write!(
|
|
out,
|
|
"ColorSource::new({:.1}, {:.1})",
|
|
self.hue.into_degrees(),
|
|
self.saturation
|
|
)
|
|
.expect("writing to string")
|
|
}
|
|
}
|
|
|
|
impl FormatRust for ColorSchemeBuilder {
|
|
fn format_rust_into(&self, source: &mut String) {
|
|
if self.secondary.is_none()
|
|
&& self.tertiary.is_none()
|
|
&& self.error.is_none()
|
|
&& self.neutral.is_none()
|
|
&& self.neutral_variant.is_none()
|
|
{
|
|
source.push_str("ColorScheme::from_primary(");
|
|
self.primary.format_rust_into(source);
|
|
source.push(')');
|
|
} else {
|
|
source.push_str("ColorSchemeBuilder::new(");
|
|
self.primary.format_rust_into(source);
|
|
source.push_str(").");
|
|
for (label, color) in [
|
|
self.secondary.map(|secondary| ("secondary", secondary)),
|
|
self.tertiary.map(|color| ("tertiary", color)),
|
|
self.error.map(|color| ("error", color)),
|
|
self.neutral.map(|color| ("neutral", color)),
|
|
self.neutral_variant.map(|color| ("neutral_variant", color)),
|
|
]
|
|
.into_iter()
|
|
.flatten()
|
|
{
|
|
source.push_str(label);
|
|
source.push('(');
|
|
color.format_rust_into(source);
|
|
source.push_str(").");
|
|
}
|
|
source.push_str("build()");
|
|
}
|
|
}
|
|
}
|