From 29dcca11eda5198f94faaa3a85e8f5c2757f42a9 Mon Sep 17 00:00:00 2001 From: Paris DOUADY Date: Sat, 23 Dec 2023 15:11:00 +0100 Subject: [PATCH] goryak: personal layer over yakui make my own UI framework over yakui with theme support and more --- Cargo.lock | 18 ++ assets_gui/src/yakui_gui.rs | 424 ++++++++++++++----------- goryak/Cargo.toml | 5 +- goryak/src/combo_box.rs | 11 +- goryak/src/count_grid.rs | 1 + goryak/src/dragvalue.rs | 131 +++++++- goryak/src/hovered.rs | 5 +- goryak/src/lib.rs | 20 +- goryak/src/material-theme.json | 418 ++++++++++++++++++++++++ goryak/src/roundrect.rs | 113 +++++++ goryak/src/scroll.rs | 152 +++++++++ goryak/src/theme.rs | 562 +++++++++++++++++++++++++++++++++ goryak/src/util.rs | 135 ++++++++ 13 files changed, 1778 insertions(+), 217 deletions(-) create mode 100644 goryak/src/material-theme.json create mode 100644 goryak/src/roundrect.rs create mode 100644 goryak/src/scroll.rs create mode 100644 goryak/src/theme.rs create mode 100644 goryak/src/util.rs diff --git a/Cargo.lock b/Cargo.lock index b9d203a8..ce7719d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1257,6 +1257,9 @@ dependencies = [ name = "goryak" version = "0.1.0" dependencies = [ + "lazy_static", + "nanoserde", + "serde", "yakui-core", "yakui-widgets", ] @@ -1847,6 +1850,21 @@ dependencies = [ "getrandom", ] +[[package]] +name = "nanoserde" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a983d0b19ed0fcd803c4f04f9b20d5e6dd17e06d44d98742a0985ac45dab1bc" +dependencies = [ + "nanoserde-derive", +] + +[[package]] +name = "nanoserde-derive" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4dc96541767a4279572fdcf9f95af9cc1c9b2a2254e7a079203c81e206a9059" + [[package]] name = "native_app" version = "0.4.3" diff --git a/assets_gui/src/yakui_gui.rs b/assets_gui/src/yakui_gui.rs index d962068e..eb0b971c 100644 --- a/assets_gui/src/yakui_gui.rs +++ b/assets_gui/src/yakui_gui.rs @@ -1,14 +1,19 @@ -use common::descriptions::{BuildingGen, CompanyKind}; -use yakui::widgets::*; -use yakui::*; +use yakui::widgets::{List, Pad, StateResponse, TextBox}; +use yakui::{ + align, center, colored_box_container, column, constrained, pad, row, use_state, Alignment, + Constraints, CrossAxisAlignment, MainAxisAlignment, MainAxisSize, Response, Vec2, +}; +use common::descriptions::{BuildingGen, CompanyKind}; use engine::meshload::MeshProperties; use engine::wgpu::RenderPass; use engine::{set_cursor_icon, CursorIcon, Drawable, GfxContext, Mesh, SpriteBatch}; use geom::Matrix4; use goryak::{ - center_width, checkbox_value, combo_box, drag_value, is_hovered, use_changed, CountGrid, - MainAxisAlignItems, + background, button_primary, button_secondary, center_width, checkbox_value, combo_box, + debug_constraints, debug_size, dragvalue, is_hovered, labelc, on_background, on_secondary, + outline_variant, scroll_vertical, secondary, secondary_container, set_theme, stretch_width, + use_changed, CountGrid, Draggable, MainAxisAlignItems, RoundRect, Theme, }; use crate::companies::Companies; @@ -58,24 +63,43 @@ impl State { constrained( Constraints::loose(Vec2::new(off.get(), f32::INFINITY)), || { - colored_box_container(Color::GRAY.with_alpha(0.8), || { - scroll_vertical(|| { - let mut l = List::column(); - l.cross_axis_alignment = CrossAxisAlignment::Stretch; - l.show(|| { - label("Companies"); - if self.gui.companies.changed && button("Save").clicked { - self.gui.companies.save(); - } - for (i, comp) in self.gui.companies.companies.iter().enumerate() { - let b = Button::styled(comp.name.to_string()); - - Pad::all(3.0).show(|| { - if b.show().clicked { - self.gui.inspected = Inspected::Company(i); - } + colored_box_container(background(), || { + let mut l = List::column(); + l.cross_axis_alignment = CrossAxisAlignment::Stretch; + l.show(|| { + let mut l = List::row(); + l.item_spacing = 5.0; + l.main_axis_alignment = MainAxisAlignment::Center; + Pad::all(5.0).show(|| { + l.show(|| { + if button_primary("Dark theme").clicked { + set_theme(Theme::Dark); + } + if button_primary("Light theme").clicked { + set_theme(Theme::Light); + } + }); + }); + scroll_vertical(|| { + let mut l = List::column(); + l.cross_axis_alignment = CrossAxisAlignment::Stretch; + l.show(|| { + let companies_open = use_state(|| false); + Self::explore_item(0, "Companies".to_string(), || { + companies_open.modify(|x| !x); }); - } + if self.gui.companies.changed && button_primary("Save").clicked { + self.gui.companies.save(); + } + if companies_open.get() { + for (i, comp) in self.gui.companies.companies.iter().enumerate() + { + Self::explore_item(4, comp.name.to_string(), || { + self.gui.inspected = Inspected::Company(i); + }); + } + } + }); }); }); }); @@ -84,41 +108,52 @@ impl State { resizebar_vert(&mut off, false); } + fn explore_item(indent: usize, name: String, on_click: impl FnOnce()) { + let mut p = Pad::ZERO; + p.left = indent as f32 * 4.0; + p.top = 4.0; + p.show(|| { + if button_secondary(name).clicked { + on_click(); + } + }); + } + fn model_properties(&mut self) { let mut l = List::column(); l.main_axis_alignment = MainAxisAlignment::End; l.cross_axis_alignment = CrossAxisAlignment::Stretch; l.show(|| { - colored_box_container(Color::GRAY, || { + colored_box_container(background(), || { column(|| { - label("Model properties"); + labelc(on_background(), "Model properties"); match &self.gui.shown { Shown::None => { - label("No model selected"); + labelc(on_background(), "No model selected"); } Shown::Error(e) => { - label(e.clone()); + labelc(on_background(), e.clone()); } Shown::Model((_, props)) => { row(|| { column(|| { - label("Vertices"); - label("Triangles"); - label("Materials"); - label("Textures"); - label("Draw calls"); + labelc(on_background(), "Vertices"); + labelc(on_background(), "Triangles"); + labelc(on_background(), "Materials"); + labelc(on_background(), "Textures"); + labelc(on_background(), "Draw calls"); }); column(|| { - label(format!("{}", props.n_vertices)); - label(format!("{}", props.n_triangles)); - label(format!("{}", props.n_materials)); - label(format!("{}", props.n_textures)); - label(format!("{}", props.n_draw_calls)); + labelc(on_background(), format!("{}", props.n_vertices)); + labelc(on_background(), format!("{}", props.n_triangles)); + labelc(on_background(), format!("{}", props.n_materials)); + labelc(on_background(), format!("{}", props.n_textures)); + labelc(on_background(), format!("{}", props.n_draw_calls)); }); }); } Shown::Sprite(_sprite) => { - label("Sprite"); + labelc(on_background(), "Sprite"); } } }); @@ -130,181 +165,206 @@ impl State { match self.gui.inspected { Inspected::None => {} Inspected::Company(i) => { - let mut off = use_state(|| 350.0); - resizebar_vert(&mut off, true); - constrained( - Constraints::loose(Vec2::new(off.get(), f32::INFINITY)), - || { - colored_box_container(Color::GRAY, || { - CountGrid::col(2) - .main_axis_align_items(MainAxisAlignItems::Center) - .show(|| { - let comp = &mut self.gui.companies.companies[i]; - - let label = |name: &str| { - pad(Pad::all(3.0), || { - label(name.to_string()); - }); - }; - - label("Name"); - text_inp(&mut comp.name); - - label("Kind"); - let mut selected = match comp.kind { - CompanyKind::Store => 0, - CompanyKind::Factory { .. } => 1, - CompanyKind::Network => 2, - }; - - if combo_box( - &mut selected, - &["Store", "Factory", "Network"], - 150.0, - ) { - match selected { - 0 => comp.kind = CompanyKind::Store, - 1 => comp.kind = CompanyKind::Factory { n_trucks: 1 }, - 2 => comp.kind = CompanyKind::Network, - _ => unreachable!(), - } - } + properties_container(|| { + let comp = &mut self.gui.companies.companies[i]; - label("Building generator"); - let mut selected = match comp.bgen { - BuildingGen::House => unreachable!(), - BuildingGen::Farm => 0, - BuildingGen::CenteredDoor { .. } => 1, - BuildingGen::NoWalkway { .. } => 2, - }; - - if combo_box( - &mut selected, - &["Farm", "Centered door", "No walkway"], - 150.0, - ) { - match selected { - 0 => comp.bgen = BuildingGen::Farm, - 1 => { - comp.bgen = BuildingGen::CenteredDoor { - vertical_factor: 1.0, - } - } - 2 => { - comp.bgen = BuildingGen::NoWalkway { - door_pos: geom::Vec2::ZERO, - } - } - _ => unreachable!(), - } - } + let label = |name: &str| { + pad(Pad::all(3.0), || { + labelc(on_background(), name.to_string()); + }); + }; - label("Recipe"); - label(" "); + fn dragv(v: &mut impl Draggable) { + Pad::all(5.0).show(|| { + stretch_width(|| { + dragvalue().show(v); + }); + }); + } - let recipe = &mut comp.recipe; - label("complexity"); - drag_value(&mut recipe.complexity); - label("storage_multiplier"); - drag_value(&mut recipe.storage_multiplier); - label("consumption"); - label(" "); + label("Name"); + text_inp(&mut comp.name); + + label("Kind"); + let mut selected = match comp.kind { + CompanyKind::Store => 0, + CompanyKind::Factory { .. } => 1, + CompanyKind::Network => 2, + }; + + if combo_box(&mut selected, &["Store", "Factory", "Network"], 150.0) { + match selected { + 0 => comp.kind = CompanyKind::Store, + 1 => comp.kind = CompanyKind::Factory { n_trucks: 1 }, + 2 => comp.kind = CompanyKind::Network, + _ => unreachable!(), + } + } - for (name, amount) in recipe.consumption.iter_mut() { - label(name); - drag_value(amount); - } + label("Building generator"); + let mut selected = match comp.bgen { + BuildingGen::House => unreachable!(), + BuildingGen::Farm => 0, + BuildingGen::CenteredDoor { .. } => 1, + BuildingGen::NoWalkway { .. } => 2, + }; + + if combo_box( + &mut selected, + &["Farm", "Centered door", "No walkway"], + 150.0, + ) { + match selected { + 0 => comp.bgen = BuildingGen::Farm, + 1 => { + comp.bgen = BuildingGen::CenteredDoor { + vertical_factor: 1.0, + } + } + 2 => { + comp.bgen = BuildingGen::NoWalkway { + door_pos: geom::Vec2::ZERO, + } + } + _ => unreachable!(), + } + } - label("production"); - label(" "); - for (name, amount) in recipe.production.iter_mut() { - label(name); - drag_value(amount); - } + label("Recipe"); + label(" "); - label("n_workers"); - drag_value(&mut comp.n_workers); + let recipe = &mut comp.recipe; - label("size"); - drag_value(&mut comp.size); + label("complexity"); + dragv(&mut recipe.complexity); - label("asset_location"); - text_inp(&mut comp.asset_location); + label("storage_multiplier"); + dragv(&mut recipe.storage_multiplier); - label("price"); - drag_value(&mut comp.price); + label("consumption"); + label(" "); - label("zone"); - let mut v = comp.zone.is_some(); - center_width(|| checkbox_value(&mut v)); + for (name, amount) in recipe.consumption.iter_mut() { + label(name); + dragv(amount); + } - if v != comp.zone.is_some() { - if v { - comp.zone = Some(Default::default()); - } else { - comp.zone = None; - } - } + label("production"); + label(" "); + for (name, amount) in recipe.production.iter_mut() { + label(name); + dragv(amount); + } - if let Some(ref mut z) = comp.zone { - label("floor"); - text_inp(&mut z.floor); + label("n_workers"); + dragv(&mut comp.n_workers); - label("filler"); - text_inp(&mut z.filler); + label("size"); + dragv(&mut comp.size); - label("price_per_area"); - drag_value(&mut z.price_per_area); - } - }); - }); - }, - ); + label("asset_location"); + text_inp(&mut comp.asset_location); + + label("price"); + dragv(&mut comp.price); + + label("zone"); + let mut v = comp.zone.is_some(); + center_width(|| checkbox_value(&mut v)); + + if v != comp.zone.is_some() { + if v { + comp.zone = Some(Default::default()); + } else { + comp.zone = None; + } + } + + if let Some(ref mut z) = comp.zone { + label("floor"); + text_inp(&mut z.floor); + + label("filler"); + text_inp(&mut z.filler); + + label("price_per_area"); + dragv(&mut z.price_per_area); + } + }); } } } } +fn properties_container(children: impl FnOnce()) { + let mut off = use_state(|| 350.0); + resizebar_vert(&mut off, true); + constrained( + Constraints::loose(Vec2::new(off.get(), f32::INFINITY)), + || { + colored_box_container(background(), || { + align(Alignment::TOP_CENTER, || { + Pad::balanced(5.0, 20.0).show(|| { + RoundRect::new(10.0) + .color(secondary_container()) + .show_children(|| { + Pad::all(8.0).show(|| { + CountGrid::col(2) + .main_axis_size(MainAxisSize::Min) + .main_axis_align_items(MainAxisAlignItems::Center) + .show(children); + }); + }); + }); + }); + }); + }, + ); +} + /// A horizontal resize bar. pub fn resizebar_vert(off: &mut Response>, scrollbar_on_left_side: bool) { - colored_box_container(Color::GRAY.adjust(0.5), || { - constrained(Constraints::tight(Vec2::new(5.0, f32::INFINITY)), || { - let last_val = use_state(|| None); - let mut hovered = false; - let d = draggable(|| { - hovered = is_hovered(); + colored_box_container(outline_variant(), || { + let last_val = use_state(|| None); + let mut hovered = false; + let d = yakui::draggable(|| { + constrained(Constraints::tight(Vec2::new(5.0, f32::INFINITY)), || { + hovered = *is_hovered(); + }); + }) + .dragging; + let delta = d + .map(|v| { + let delta = v.current.x - last_val.get().unwrap_or(v.current.x); + last_val.set(Some(v.current.x)); + delta }) - .dragging; - let delta = d - .map(|v| { - let delta = v.current.x - last_val.get().unwrap_or(v.current.x); - last_val.set(Some(v.current.x)); - delta - }) - .unwrap_or_else(|| { - last_val.set(None); - 0.0 - }); - off.modify(|v| { - if scrollbar_on_left_side { - v - delta - } else { - v + delta - } - .clamp(100.0, 600.0) + .unwrap_or_else(|| { + last_val.set(None); + 0.0 }); + off.modify(|v| { + if scrollbar_on_left_side { + v - delta + } else { + v + delta + } + .clamp(100.0, 600.0) + }); - let should_show_mouse_icon = d.is_some() || hovered; - use_changed(should_show_mouse_icon, || { - set_colresize_icon(should_show_mouse_icon); - }); + let should_show_mouse_icon = d.is_some() || hovered; + use_changed(should_show_mouse_icon, || { + set_colresize_icon(should_show_mouse_icon); }); }); } fn text_inp(v: &mut String) { center(|| { - if let Some(x) = textbox(v.clone()).into_inner().text { + let mut t = TextBox::new(v.clone()); + t.fill = Some(secondary()); + t.style.color = on_secondary(); + if let Some(x) = t.show().into_inner().text { *v = x; } }); diff --git a/goryak/Cargo.toml b/goryak/Cargo.toml index 333ff569..fec31d8a 100644 --- a/goryak/Cargo.toml +++ b/goryak/Cargo.toml @@ -6,4 +6,7 @@ description = "Egregoria's yakui component library" [dependencies] yakui-core = { git = "https://github.com/SecondHalfGames/yakui" } -yakui-widgets = { git = "https://github.com/SecondHalfGames/yakui" } \ No newline at end of file +yakui-widgets = { git = "https://github.com/SecondHalfGames/yakui" } +nanoserde = "0.1.35" +lazy_static = "1.4.0" +serde = { version = "1.0.193", features = ["derive"] } \ No newline at end of file diff --git a/goryak/src/combo_box.rs b/goryak/src/combo_box.rs index 5f5588d4..b8c3ebe4 100644 --- a/goryak/src/combo_box.rs +++ b/goryak/src/combo_box.rs @@ -1,7 +1,8 @@ -use yakui_core::geometry::{Color, Constraints, Dim2, Vec2}; +use crate::{background, button_secondary}; +use yakui_core::geometry::{Constraints, Dim2, Vec2}; use yakui_core::{Alignment, CrossAxisAlignment, MainAxisAlignment, MainAxisSize}; use yakui_widgets::widgets::{Layer, List, Pad}; -use yakui_widgets::{button, colored_box_container, constrained, pad, reflow, use_state}; +use yakui_widgets::{colored_box_container, constrained, pad, reflow, use_state}; pub fn combo_box(selected: &mut usize, items: &[&str], w: f32) -> bool { let mut changed = false; @@ -14,7 +15,7 @@ pub fn combo_box(selected: &mut usize, items: &[&str], w: f32) -> bool { l.show(|| { let open = use_state(|| false); - if button(items[*selected].to_string()).clicked { + if button_secondary(items[*selected].to_string()).clicked { open.modify(|x| !x); } @@ -22,7 +23,7 @@ pub fn combo_box(selected: &mut usize, items: &[&str], w: f32) -> bool { Layer::new().show(|| { reflow(Alignment::BOTTOM_LEFT, Dim2::ZERO, || { constrained(Constraints::loose(Vec2::new(w, f32::INFINITY)), || { - colored_box_container(Color::GRAY.adjust(0.8), || { + colored_box_container(background(), || { Pad::all(3.0).show(|| { let mut l = List::column(); l.cross_axis_alignment = CrossAxisAlignment::Stretch; @@ -33,7 +34,7 @@ pub fn combo_box(selected: &mut usize, items: &[&str], w: f32) -> bool { continue; } - if button(item.to_string()).clicked { + if button_secondary(item.to_string()).clicked { *selected = i; open.set(false); changed = true; diff --git a/goryak/src/count_grid.rs b/goryak/src/count_grid.rs index c35af96d..9606551b 100644 --- a/goryak/src/count_grid.rs +++ b/goryak/src/count_grid.rs @@ -287,6 +287,7 @@ impl Widget for CountGridWidget { MainAxisAlignItems::Start | MainAxisAlignItems::Stretch => cell_main_max, _ => max_sizes[n_cross + main_id], }; + #[allow(unreachable_patterns)] let offset_main = match self.props.main_axis_align_items { MainAxisAlignItems::Start | MainAxisAlignItems::Stretch => 0.0, MainAxisAlignItems::Center => ((cell_main_size - child_main_size) / 2.0).max(0.0), diff --git a/goryak/src/dragvalue.rs b/goryak/src/dragvalue.rs index 4e2ac3bf..d852b58a 100644 --- a/goryak/src/dragvalue.rs +++ b/goryak/src/dragvalue.rs @@ -1,16 +1,29 @@ -use crate::stretch_width; -use yakui_widgets::pad; -use yakui_widgets::widgets::{Pad, Slider}; +use yakui_core::geometry::Vec2; +use yakui_core::MainAxisAlignment; +use yakui_widgets::widgets::{List, Pad}; +use yakui_widgets::{draggable, pad, use_state}; + +use crate::roundrect::RoundRect; +use crate::{labelc, on_primary, outline, secondary}; pub trait Draggable: Copy { + const DEFAULT_STEP: f64; + const DEFAULT_MIN: f64; + const DEFAULT_MAX: f64; + fn to_f64(self) -> f64; fn from_f64(v: f64) -> Self; + fn default_step() -> f64; } macro_rules! impl_slidable { - ($($t:ty),*) => { + ($($t:ty; $step:expr),*) => { $( impl Draggable for $t { + const DEFAULT_STEP: f64 = $step; + const DEFAULT_MIN: f64 = <$t>::MIN as f64; + const DEFAULT_MAX: f64 = <$t>::MAX as f64; + fn to_f64(self) -> f64 { self as f64 } @@ -18,21 +31,109 @@ macro_rules! impl_slidable { fn from_f64(v: f64) -> Self { v as Self } + + fn default_step() -> f64 { + $step + } + } )* }; } -impl_slidable!(i32, u32, i64, u64, f32, f64); +impl_slidable!(i32; 1.0, + u32; 1.0, + i64; 1.0, + u64; 1.0, + f32; 0.01, + f64; 0.01); -pub fn drag_value(amount: &mut T) { - stretch_width(|| { - pad(Pad::horizontal(10.0), || { - let mut slider = Slider::new((*amount).to_f64(), 1.0, 10.0); - slider.step = Some(1.0); - if let Some(v) = slider.show().value { - *amount = Draggable::from_f64(v); - } - }); - }); +pub struct DragValue { + min: Option, + max: Option, + step: Option, +} + +impl DragValue { + pub fn min(mut self, v: f64) -> Self { + self.min = Some(v); + self + } + + pub fn max(mut self, v: f64) -> Self { + self.max = Some(v); + self + } + pub fn minmax(mut self, r: std::ops::Range) -> Self { + self.min = Some(r.start); + self.max = Some(r.end); + self + } + + pub fn step(mut self, v: f64) -> Self { + self.step = Some(v); + self + } + + /// Returns true if the value was changed. + pub fn show(self, value: &mut T) -> bool { + let mut changed = false; + + RoundRect::new(2.0) + .outline(outline(), 2.0) + .color(secondary()) + .show_children(|| { + let dragged = draggable_delta(|| { + pad(Pad::horizontal(10.0), || { + let mut l = List::row(); + l.main_axis_alignment = MainAxisAlignment::Center; + l.show(|| { + labelc(on_primary(), format!("{}", T::to_f64(*value))); + }); + }); + }); + + if let Some(dragged) = dragged { + let oldv = T::to_f64(*value); + let newv = oldv + dragged.x as f64 * self.step.unwrap_or(T::default_step()); + + *value = T::from_f64(newv.clamp( + self.min.unwrap_or(T::DEFAULT_MIN), + self.max.unwrap_or(T::DEFAULT_MAX), + )); + changed = true; + } + }); + + changed + } +} + +fn draggable_delta(children: impl FnOnce()) -> Option { + let last_val_state = use_state(|| None); + let Some(mut d) = draggable(children).dragging else { + last_val_state.set(None); + return None; + }; + + let last_val = last_val_state.get().unwrap_or(d.current); + let mut delta = d.current - last_val; + if delta.x.abs() < 1.0 { + d.current.x = last_val.x; + delta.x = 0.0; + } + if delta.y.abs() < 1.0 { + d.current.y = last_val.y; + delta.y = 0.0; + } + last_val_state.set(Some(d.current)); + Some(delta) +} + +pub fn dragvalue() -> DragValue { + DragValue { + min: None, + max: None, + step: None, + } } diff --git a/goryak/src/hovered.rs b/goryak/src/hovered.rs index 2bf79275..89604ec4 100644 --- a/goryak/src/hovered.rs +++ b/goryak/src/hovered.rs @@ -1,9 +1,10 @@ use yakui_core::event::{EventInterest, EventResponse, WidgetEvent}; use yakui_core::widget::{EventContext, Widget}; +use yakui_core::Response; use yakui_widgets::util::widget; -pub fn is_hovered() -> bool { - *widget::(()) +pub fn is_hovered() -> Response { + widget::(()) } #[derive(Debug)] diff --git a/goryak/src/lib.rs b/goryak/src/lib.rs index 98df1137..f174d03d 100644 --- a/goryak/src/lib.rs +++ b/goryak/src/lib.rs @@ -4,6 +4,10 @@ mod decoration; mod dragvalue; mod hovered; mod layout; +mod roundrect; +mod scroll; +mod theme; +mod util; pub use combo_box::*; pub use count_grid::*; @@ -11,15 +15,7 @@ pub use decoration::*; pub use dragvalue::*; pub use hovered::*; pub use layout::*; - -pub fn checkbox_value(v: &mut bool) { - *v = yakui_widgets::checkbox(*v).checked; -} - -pub fn use_changed(v: T, f: impl FnOnce()) { - let old_v = yakui_widgets::use_state(|| None); - if old_v.get() != Some(v) { - old_v.set(Some(v)); - f(); - } -} +pub use roundrect::*; +pub use scroll::*; +pub use theme::*; +pub use util::*; diff --git a/goryak/src/material-theme.json b/goryak/src/material-theme.json new file mode 100644 index 00000000..606b0da7 --- /dev/null +++ b/goryak/src/material-theme.json @@ -0,0 +1,418 @@ +{ + "description": "TYPE: CUSTOM\nMaterial Theme Builder export 2023-12-21 05:17:00", + "seed": "#0661A3", + "coreColors": { + "primary": "#0661A3" + }, + "extendedColors": [], + "schemes": { + "light": { + "primary": "#36618E", + "surfaceTint": "#36618E", + "onPrimary": "#FFFFFF", + "primaryContainer": "#D1E4FF", + "onPrimaryContainer": "#001D36", + "secondary": "#535F70", + "onSecondary": "#FFFFFF", + "secondaryContainer": "#D7E3F8", + "onSecondaryContainer": "#101C2B", + "tertiary": "#6B5778", + "onTertiary": "#FFFFFF", + "tertiaryContainer": "#F3DAFF", + "onTertiaryContainer": "#251431", + "error": "#BA1A1A", + "onError": "#FFFFFF", + "errorContainer": "#FFDAD6", + "onErrorContainer": "#410002", + "background": "#F8F9FF", + "onBackground": "#191C20", + "surface": "#F8F9FF", + "onSurface": "#191C20", + "surfaceVariant": "#DFE2EB", + "onSurfaceVariant": "#43474E", + "outline": "#73777F", + "outlineVariant": "#C3C6CF", + "shadow": "#000000", + "scrim": "#000000", + "inverseSurface": "#2E3135", + "inverseOnSurface": "#EFF0F7", + "inversePrimary": "#A0CAFD", + "primaryFixed": "#D1E4FF", + "onPrimaryFixed": "#001D36", + "primaryFixedDim": "#A0CAFD", + "onPrimaryFixedVariant": "#1A4975", + "secondaryFixed": "#D7E3F8", + "onSecondaryFixed": "#101C2B", + "secondaryFixedDim": "#BBC7DB", + "onSecondaryFixedVariant": "#3B4858", + "tertiaryFixed": "#F3DAFF", + "onTertiaryFixed": "#251431", + "tertiaryFixedDim": "#D7BEE4", + "onTertiaryFixedVariant": "#523F5F", + "surfaceDim": "#D8DAE0", + "surfaceBright": "#F8F9FF", + "surfaceContainerLowest": "#FFFFFF", + "surfaceContainerLow": "#F2F3FA", + "surfaceContainer": "#ECEEF4", + "surfaceContainerHigh": "#E6E8EE", + "surfaceContainerHighest": "#E1E2E8" + }, + "light-medium-contrast": { + "primary": "#144571", + "surfaceTint": "#36618E", + "onPrimary": "#FFFFFF", + "primaryContainer": "#4E77A6", + "onPrimaryContainer": "#FFFFFF", + "secondary": "#374454", + "onSecondary": "#FFFFFF", + "secondaryContainer": "#697687", + "onSecondaryContainer": "#FFFFFF", + "tertiary": "#4E3B5B", + "onTertiary": "#FFFFFF", + "tertiaryContainer": "#826D8F", + "onTertiaryContainer": "#FFFFFF", + "error": "#8C0009", + "onError": "#FFFFFF", + "errorContainer": "#DA342E", + "onErrorContainer": "#FFFFFF", + "background": "#F8F9FF", + "onBackground": "#191C20", + "surface": "#F8F9FF", + "onSurface": "#191C20", + "surfaceVariant": "#DFE2EB", + "onSurfaceVariant": "#3F434A", + "outline": "#5B5F67", + "outlineVariant": "#777B83", + "shadow": "#000000", + "scrim": "#000000", + "inverseSurface": "#2E3135", + "inverseOnSurface": "#EFF0F7", + "inversePrimary": "#A0CAFD", + "primaryFixed": "#4E77A6", + "onPrimaryFixed": "#FFFFFF", + "primaryFixedDim": "#335E8C", + "onPrimaryFixedVariant": "#FFFFFF", + "secondaryFixed": "#697687", + "onSecondaryFixed": "#FFFFFF", + "secondaryFixedDim": "#515D6E", + "onSecondaryFixedVariant": "#FFFFFF", + "tertiaryFixed": "#826D8F", + "onTertiaryFixed": "#FFFFFF", + "tertiaryFixedDim": "#685475", + "onTertiaryFixedVariant": "#FFFFFF", + "surfaceDim": "#D8DAE0", + "surfaceBright": "#F8F9FF", + "surfaceContainerLowest": "#FFFFFF", + "surfaceContainerLow": "#F2F3FA", + "surfaceContainer": "#ECEEF4", + "surfaceContainerHigh": "#E6E8EE", + "surfaceContainerHighest": "#E1E2E8" + }, + "light-high-contrast": { + "primary": "#002341", + "surfaceTint": "#36618E", + "onPrimary": "#FFFFFF", + "primaryContainer": "#144571", + "onPrimaryContainer": "#FFFFFF", + "secondary": "#172332", + "onSecondary": "#FFFFFF", + "secondaryContainer": "#374454", + "onSecondaryContainer": "#FFFFFF", + "tertiary": "#2C1B38", + "onTertiary": "#FFFFFF", + "tertiaryContainer": "#4E3B5B", + "onTertiaryContainer": "#FFFFFF", + "error": "#4E0002", + "onError": "#FFFFFF", + "errorContainer": "#8C0009", + "onErrorContainer": "#FFFFFF", + "background": "#F8F9FF", + "onBackground": "#191C20", + "surface": "#F8F9FF", + "onSurface": "#000000", + "surfaceVariant": "#DFE2EB", + "onSurfaceVariant": "#20242B", + "outline": "#3F434A", + "outlineVariant": "#3F434A", + "shadow": "#000000", + "scrim": "#000000", + "inverseSurface": "#2E3135", + "inverseOnSurface": "#FFFFFF", + "inversePrimary": "#E2EDFF", + "primaryFixed": "#144571", + "onPrimaryFixed": "#FFFFFF", + "primaryFixedDim": "#002E52", + "onPrimaryFixedVariant": "#FFFFFF", + "secondaryFixed": "#374454", + "onSecondaryFixed": "#FFFFFF", + "secondaryFixedDim": "#212E3D", + "onSecondaryFixedVariant": "#FFFFFF", + "tertiaryFixed": "#4E3B5B", + "onTertiaryFixed": "#FFFFFF", + "tertiaryFixedDim": "#372644", + "onTertiaryFixedVariant": "#FFFFFF", + "surfaceDim": "#D8DAE0", + "surfaceBright": "#F8F9FF", + "surfaceContainerLowest": "#FFFFFF", + "surfaceContainerLow": "#F2F3FA", + "surfaceContainer": "#ECEEF4", + "surfaceContainerHigh": "#E6E8EE", + "surfaceContainerHighest": "#E1E2E8" + }, + "dark": { + "primary": "#A0CAFD", + "surfaceTint": "#A0CAFD", + "onPrimary": "#003258", + "primaryContainer": "#1A4975", + "onPrimaryContainer": "#D1E4FF", + "secondary": "#BBC7DB", + "onSecondary": "#253140", + "secondaryContainer": "#3B4858", + "onSecondaryContainer": "#D7E3F8", + "tertiary": "#D7BEE4", + "onTertiary": "#3B2948", + "tertiaryContainer": "#523F5F", + "onTertiaryContainer": "#F3DAFF", + "error": "#FFB4AB", + "onError": "#690005", + "errorContainer": "#93000A", + "onErrorContainer": "#FFDAD6", + "background": "#111418", + "onBackground": "#E1E2E8", + "surface": "#111418", + "onSurface": "#E1E2E8", + "surfaceVariant": "#43474E", + "onSurfaceVariant": "#C3C6CF", + "outline": "#8D9199", + "outlineVariant": "#43474E", + "shadow": "#000000", + "scrim": "#000000", + "inverseSurface": "#E1E2E8", + "inverseOnSurface": "#2E3135", + "inversePrimary": "#36618E", + "primaryFixed": "#D1E4FF", + "onPrimaryFixed": "#001D36", + "primaryFixedDim": "#A0CAFD", + "onPrimaryFixedVariant": "#1A4975", + "secondaryFixed": "#D7E3F8", + "onSecondaryFixed": "#101C2B", + "secondaryFixedDim": "#BBC7DB", + "onSecondaryFixedVariant": "#3B4858", + "tertiaryFixed": "#F3DAFF", + "onTertiaryFixed": "#251431", + "tertiaryFixedDim": "#D7BEE4", + "onTertiaryFixedVariant": "#523F5F", + "surfaceDim": "#111418", + "surfaceBright": "#36393E", + "surfaceContainerLowest": "#0B0E13", + "surfaceContainerLow": "#191C20", + "surfaceContainer": "#1D2024", + "surfaceContainerHigh": "#272A2F", + "surfaceContainerHighest": "#32353A" + }, + "dark-medium-contrast": { + "primary": "#A7CEFF", + "surfaceTint": "#A0CAFD", + "onPrimary": "#00172E", + "primaryContainer": "#6B94C4", + "onPrimaryContainer": "#000000", + "secondary": "#BFCCDF", + "onSecondary": "#0A1725", + "secondaryContainer": "#8592A4", + "onSecondaryContainer": "#000000", + "tertiary": "#DBC2E9", + "onTertiary": "#1F0F2C", + "tertiaryContainer": "#9F88AD", + "onTertiaryContainer": "#000000", + "error": "#FFBAB1", + "onError": "#370001", + "errorContainer": "#FF5449", + "onErrorContainer": "#000000", + "background": "#111418", + "onBackground": "#E1E2E8", + "surface": "#111418", + "onSurface": "#FAFAFF", + "surfaceVariant": "#43474E", + "onSurfaceVariant": "#C7CBD3", + "outline": "#9FA3AB", + "outlineVariant": "#7F838B", + "shadow": "#000000", + "scrim": "#000000", + "inverseSurface": "#E1E2E8", + "inverseOnSurface": "#272A2F", + "inversePrimary": "#1B4A76", + "primaryFixed": "#D1E4FF", + "onPrimaryFixed": "#001225", + "primaryFixedDim": "#A0CAFD", + "onPrimaryFixedVariant": "#003862", + "secondaryFixed": "#D7E3F8", + "onSecondaryFixed": "#051220", + "secondaryFixedDim": "#BBC7DB", + "onSecondaryFixedVariant": "#2B3746", + "tertiaryFixed": "#F3DAFF", + "onTertiaryFixed": "#1A0926", + "tertiaryFixedDim": "#D7BEE4", + "onTertiaryFixedVariant": "#412F4E", + "surfaceDim": "#111418", + "surfaceBright": "#36393E", + "surfaceContainerLowest": "#0B0E13", + "surfaceContainerLow": "#191C20", + "surfaceContainer": "#1D2024", + "surfaceContainerHigh": "#272A2F", + "surfaceContainerHighest": "#32353A" + }, + "dark-high-contrast": { + "primary": "#FAFAFF", + "surfaceTint": "#A0CAFD", + "onPrimary": "#000000", + "primaryContainer": "#A7CEFF", + "onPrimaryContainer": "#000000", + "secondary": "#FAFAFF", + "onSecondary": "#000000", + "secondaryContainer": "#BFCCDF", + "onSecondaryContainer": "#000000", + "tertiary": "#FFF9FB", + "onTertiary": "#000000", + "tertiaryContainer": "#DBC2E9", + "onTertiaryContainer": "#000000", + "error": "#FFF9F9", + "onError": "#000000", + "errorContainer": "#FFBAB1", + "onErrorContainer": "#000000", + "background": "#111418", + "onBackground": "#E1E2E8", + "surface": "#111418", + "onSurface": "#FFFFFF", + "surfaceVariant": "#43474E", + "onSurfaceVariant": "#FAFAFF", + "outline": "#C7CBD3", + "outlineVariant": "#C7CBD3", + "shadow": "#000000", + "scrim": "#000000", + "inverseSurface": "#E1E2E8", + "inverseOnSurface": "#000000", + "inversePrimary": "#002B4E", + "primaryFixed": "#D9E8FF", + "onPrimaryFixed": "#000000", + "primaryFixedDim": "#A7CEFF", + "onPrimaryFixedVariant": "#00172E", + "secondaryFixed": "#DBE8FC", + "onSecondaryFixed": "#000000", + "secondaryFixedDim": "#BFCCDF", + "onSecondaryFixedVariant": "#0A1725", + "tertiaryFixed": "#F5DFFF", + "onTertiaryFixed": "#000000", + "tertiaryFixedDim": "#DBC2E9", + "onTertiaryFixedVariant": "#1F0F2C", + "surfaceDim": "#111418", + "surfaceBright": "#36393E", + "surfaceContainerLowest": "#0B0E13", + "surfaceContainerLow": "#191C20", + "surfaceContainer": "#1D2024", + "surfaceContainerHigh": "#272A2F", + "surfaceContainerHighest": "#32353A" + } + }, + "palettes": { + "primary": { + "0": "#000000", + "5": "#001225", + "10": "#001D36", + "15": "#002747", + "20": "#003258", + "25": "#003D6B", + "30": "#00497D", + "35": "#005591", + "40": "#0661A3", + "50": "#347ABE", + "60": "#5294DA", + "70": "#6FAFF6", + "80": "#9FCAFF", + "90": "#D1E4FF", + "95": "#EAF1FF", + "98": "#F8F9FF", + "99": "#FDFCFF", + "100": "#FFFFFF" + }, + "secondary": { + "0": "#000000", + "5": "#05121F", + "10": "#101C2B", + "15": "#1A2735", + "20": "#253140", + "25": "#303C4C", + "30": "#3B4858", + "35": "#475364", + "40": "#535F70", + "50": "#6C7889", + "60": "#8592A4", + "70": "#A0ACBF", + "80": "#BBC7DB", + "90": "#D7E3F7", + "95": "#EAF1FF", + "98": "#F8F9FF", + "99": "#FDFCFF", + "100": "#FFFFFF" + }, + "tertiary": { + "0": "#000000", + "5": "#1A0926", + "10": "#251431", + "15": "#301F3C", + "20": "#3B2947", + "25": "#463453", + "30": "#523F5F", + "35": "#5E4B6B", + "40": "#6B5778", + "50": "#856F92", + "60": "#9F89AC", + "70": "#BAA3C8", + "80": "#D7BEE4", + "90": "#F3DAFF", + "95": "#FBECFF", + "98": "#FFF7FD", + "99": "#FFFBFF", + "100": "#FFFFFF" + }, + "neutral": { + "0": "#000000", + "5": "#0F1114", + "10": "#1A1C1E", + "15": "#242629", + "20": "#2F3033", + "25": "#3A3B3E", + "30": "#45474A", + "35": "#515255", + "40": "#5D5E61", + "50": "#76777A", + "60": "#909094", + "70": "#ABABAE", + "80": "#C6C6CA", + "90": "#E2E2E6", + "95": "#F1F0F4", + "98": "#FAF9FC", + "99": "#FDFCFF", + "100": "#FFFFFF" + }, + "neutral-variant": { + "0": "#000000", + "5": "#0D1117", + "10": "#171C22", + "15": "#22262C", + "20": "#2C3137", + "25": "#373C42", + "30": "#43474E", + "35": "#4E535A", + "40": "#5A5F66", + "50": "#73777F", + "60": "#8D9199", + "70": "#A7ABB4", + "80": "#C3C6CF", + "90": "#DFE2EB", + "95": "#EDF1FA", + "98": "#F8F9FF", + "99": "#FDFCFF", + "100": "#FFFFFF" + } + } +} \ No newline at end of file diff --git a/goryak/src/roundrect.rs b/goryak/src/roundrect.rs new file mode 100644 index 00000000..3727eca5 --- /dev/null +++ b/goryak/src/roundrect.rs @@ -0,0 +1,113 @@ +use yakui_core::geometry::{Color, Constraints, Vec2}; +use yakui_core::widget::{LayoutContext, PaintContext, Widget}; +use yakui_core::Response; +use yakui_widgets::shapes; +use yakui_widgets::util::{widget, widget_children}; + +/** +A colored box with rounded corners that can contain children. + +Responds with [RoundRectResponse]. + */ +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct RoundRect { + pub radius: f32, + pub color: Color, + pub min_size: Vec2, + pub outline: Color, + pub outline_thickness: f32, +} + +impl RoundRect { + pub fn new(radius: f32) -> Self { + Self { + radius, + color: Color::WHITE, + min_size: Vec2::ZERO, + outline: Color::BLACK, + outline_thickness: 0.0, + } + } + + pub fn outline(mut self, color: Color, thickness: f32) -> Self { + self.outline = color; + self.outline_thickness = thickness; + self + } + + pub fn color(mut self, color: Color) -> Self { + self.color = color; + self + } + + pub fn min_size(mut self, size: Vec2) -> Self { + self.min_size = size; + self + } + + pub fn show(self) -> Response { + widget::(self) + } + + pub fn show_children(self, children: F) -> Response { + widget_children::(children, self) + } +} + +#[derive(Debug)] +pub struct RoundRectWidget { + props: RoundRect, +} + +pub type RoundRectResponse = (); + +impl Widget for RoundRectWidget { + type Props<'a> = RoundRect; + type Response = RoundRectResponse; + + fn new() -> Self { + Self { + props: RoundRect::new(0.0), + } + } + + fn update(&mut self, props: Self::Props<'_>) -> Self::Response { + self.props = props; + } + + fn layout(&self, mut ctx: LayoutContext<'_>, input: Constraints) -> Vec2 { + let node = ctx.dom.get_current(); + let mut size = self.props.min_size; + + for &child in &node.children { + let child_size = ctx.calculate_layout(child, input); + size = size.max(child_size); + } + + input.constrain_min(size) + } + + fn paint(&self, mut ctx: PaintContext<'_>) { + let node = ctx.dom.get_current(); + let layout_node = ctx.layout.get(ctx.dom.current()).unwrap(); + + let thickness = self.props.outline_thickness; + + let mut outer_rect = shapes::RoundedRectangle::new(layout_node.rect, self.props.radius); + outer_rect.color = self.props.outline; + outer_rect.add(ctx.paint); + + let mut inner_rect = layout_node.rect; + inner_rect.set_size(inner_rect.size() - Vec2::splat(thickness * 2.0)); + inner_rect.set_pos(inner_rect.pos() + Vec2::splat(thickness)); + + let mut inner_rect = shapes::RoundedRectangle::new(inner_rect, self.props.radius); + inner_rect.color = self.props.color; + inner_rect.add(ctx.paint); + + for &child in &node.children { + ctx.paint(child); + } + } +} diff --git a/goryak/src/scroll.rs b/goryak/src/scroll.rs new file mode 100644 index 00000000..a4f8525e --- /dev/null +++ b/goryak/src/scroll.rs @@ -0,0 +1,152 @@ +use std::cell::Cell; + +use yakui_core::event::{EventInterest, EventResponse, WidgetEvent}; +use yakui_core::geometry::{Constraints, FlexFit, Vec2}; +use yakui_core::widget::{EventContext, LayoutContext, PaintContext, Widget}; +use yakui_core::{MainAxisSize, Response}; + +#[derive(Debug)] +#[non_exhaustive] +pub struct Scrollable { + pub direction: Option, + pub main_axis_size: MainAxisSize, +} + +impl Scrollable { + pub fn none() -> Self { + Scrollable { + direction: None, + main_axis_size: MainAxisSize::Max, + } + } + + pub fn vertical() -> Self { + Scrollable { + direction: Some(ScrollDirection::Y), + main_axis_size: MainAxisSize::Max, + } + } + + pub fn main_axis_size(mut self, main_axis_size: MainAxisSize) -> Self { + self.main_axis_size = main_axis_size; + self + } + + pub fn show(self, children: F) -> Response { + yakui_widgets::util::widget_children::(children, self) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ScrollDirection { + Y, +} + +#[derive(Debug)] +#[non_exhaustive] +pub struct ScrollableWidget { + props: Scrollable, + scroll_position: Cell, + canvas_size: Cell, +} + +pub type ScrollableResponse = (); + +impl Widget for ScrollableWidget { + type Props<'a> = Scrollable; + type Response = ScrollableResponse; + + fn new() -> Self { + Self { + props: Scrollable::none(), + scroll_position: Cell::new(Vec2::ZERO), + canvas_size: Cell::new(Vec2::ZERO), + } + } + + fn update(&mut self, props: Self::Props<'_>) -> Self::Response { + self.props = props; + } + + fn flex(&self) -> (u32, FlexFit) { + match self.props.main_axis_size { + MainAxisSize::Max => (1, FlexFit::Tight), + MainAxisSize::Min => (0, FlexFit::Loose), + _ => unimplemented!(), + } + } + + fn layout(&self, mut ctx: LayoutContext<'_>, constraints: Constraints) -> Vec2 { + ctx.layout.enable_clipping(ctx.dom); + + let node = ctx.dom.get_current(); + let mut canvas_size = Vec2::ZERO; + + let main_axis_size = match self.props.main_axis_size { + MainAxisSize::Max => constraints.max.y, + MainAxisSize::Min => constraints.min.y, + _ => unimplemented!(), + }; + + canvas_size.y = canvas_size.y.max(main_axis_size); + + let child_constraints = match self.props.direction { + None => constraints, + Some(ScrollDirection::Y) => Constraints { + min: Vec2::new(constraints.min.x, 0.0), + max: Vec2::new(constraints.max.x, f32::INFINITY), + }, + }; + + for &child in &node.children { + let child_size = ctx.calculate_layout(child, child_constraints); + canvas_size = canvas_size.max(child_size); + } + self.canvas_size.set(canvas_size); + + let size = constraints.constrain(canvas_size); + + let max_scroll_position = (canvas_size - size).max(Vec2::ZERO); + let mut scroll_position = self + .scroll_position + .get() + .min(max_scroll_position) + .max(Vec2::ZERO); + + match self.props.direction { + None => scroll_position = Vec2::ZERO, + Some(ScrollDirection::Y) => scroll_position.x = 0.0, + } + + self.scroll_position.set(scroll_position); + + for &child in &node.children { + ctx.layout.set_pos(child, -scroll_position); + } + + size + } + + fn paint(&self, mut ctx: PaintContext<'_>) { + let node = ctx.dom.get_current(); + + for &child in &node.children { + ctx.paint(child); + } + } + + fn event_interest(&self) -> EventInterest { + EventInterest::MOUSE_INSIDE + } + + fn event(&mut self, _ctx: EventContext<'_>, event: &WidgetEvent) -> EventResponse { + match *event { + WidgetEvent::MouseScroll { delta } => { + let pos = self.scroll_position.get(); + self.scroll_position.set(pos + delta); + EventResponse::Sink + } + _ => EventResponse::Bubble, + } + } +} diff --git a/goryak/src/theme.rs b/goryak/src/theme.rs new file mode 100644 index 00000000..d4c8b0ca --- /dev/null +++ b/goryak/src/theme.rs @@ -0,0 +1,562 @@ +#![allow(dead_code)] + +use lazy_static::lazy_static; +use nanoserde::{DeJson, DeJsonErr}; +use serde::{Deserialize, Serialize}; +use std::ops::Deref; +use std::sync::{RwLock, RwLockReadGuard}; +use yakui_core::geometry::Color; + +///! See https://material-foundation.github.io/material-theme-builder/ +///! for the source of the default theme. +///! and https://m3.material.io/styles/color/roles for explanations of the role with more detail + +/// Use primary roles for the most prominent components across the UI, such as the FAB +/// high-emphasis buttons, and active states. +pub fn primary() -> Color { + THEMER.read().unwrap().cur_colors.primary +} + +/// Text and icons against primary +pub fn on_primary() -> Color { + THEMER.read().unwrap().cur_colors.on_primary +} + +/// Use secondary roles for less prominent components in the UI such as filter chips. +pub fn secondary() -> Color { + THEMER.read().unwrap().cur_colors.secondary +} + +/// Text and icons against secondary +pub fn on_secondary() -> Color { + THEMER.read().unwrap().cur_colors.on_secondary +} + +/// Use tertiary roles for contrasting accents that balance primary and secondary colors +/// or bring heightened attention to an element such as an input field. +pub fn tertiary() -> Color { + THEMER.read().unwrap().cur_colors.tertiary +} + +/// Text and icons against tertiary +pub fn on_tertiary() -> Color { + THEMER.read().unwrap().cur_colors.on_tertiary +} + +/// Use error roles for components that communicate that an error has occurred. +pub fn error() -> Color { + THEMER.read().unwrap().cur_colors.error +} + +/// Text and icons against error +pub fn on_error() -> Color { + THEMER.read().unwrap().cur_colors.on_error +} + +/// Use background roles for the background color of components such as cards, sheets, and menus. +pub fn background() -> Color { + THEMER.read().unwrap().cur_colors.background +} + +/// Text and icons against background +pub fn on_background() -> Color { + THEMER.read().unwrap().cur_colors.on_background +} + +/// Same color as background. Use surface roles for more neutral backgrounds, +/// and container colors for components like cards, sheets, and dialogs. +pub fn surface() -> Color { + THEMER.read().unwrap().cur_colors.surface +} + +/// Text and icons against surface +pub fn on_surface() -> Color { + THEMER.read().unwrap().cur_colors.on_surface +} + +pub fn surface_variant() -> Color { + THEMER.read().unwrap().cur_colors.surface_variant +} + +pub fn on_surface_variant() -> Color { + THEMER.read().unwrap().cur_colors.on_surface_variant +} + +/// Important boundaries, such as a text field outline +pub fn outline() -> Color { + THEMER.read().unwrap().cur_colors.outline +} + +/// Decorative elements, such as dividers +pub fn outline_variant() -> Color { + THEMER.read().unwrap().cur_colors.outline_variant +} + +/// Shadows for components such as cards, sheets, and menus. +pub fn shadow() -> Color { + THEMER.read().unwrap().cur_colors.shadow +} + +/// Use scrim roles for the color of a scrim, which is a translucent overlay that covers +/// the entire screen and indicates that the UI is temporarily unavailable. +pub fn scrim() -> Color { + THEMER.read().unwrap().cur_colors.scrim +} + +/// High-emphasis fills, texts, and icons against surface +pub fn primary_container() -> Color { + THEMER.read().unwrap().cur_colors.primary_container +} + +/// Text and icons against primary_container +pub fn on_primary_container() -> Color { + THEMER.read().unwrap().cur_colors.on_primary_container +} + +/// Less prominent fill color against surface, for recessive components like tonal buttons +pub fn secondary_container() -> Color { + THEMER.read().unwrap().cur_colors.secondary_container +} + +/// Text and icons against secondary_container +pub fn on_secondary_container() -> Color { + THEMER.read().unwrap().cur_colors.on_secondary_container +} + +/// Complementary container color against surface, for components like input fields +pub fn tertiary_container() -> Color { + THEMER.read().unwrap().cur_colors.tertiary_container +} + +/// Text and icons against tertiary_container +pub fn on_tertiary_container() -> Color { + THEMER.read().unwrap().cur_colors.on_tertiary_container +} + +pub fn colors() -> impl Deref + 'static { + // doesn't work with a closure for some reason + fn cur_color_get(g: &Themer) -> &ParsedSemanticColors { + &g.cur_colors + } + let g = THEMER.read().unwrap(); + MappedReadGuard::new(g, cur_color_get) +} + +pub struct MappedReadGuard { + inner: RwLockReadGuard<'static, T>, + map: F, +} + +impl MappedReadGuard { + pub fn new(inner: RwLockReadGuard<'static, T>, map: F) -> Self { + Self { inner, map } + } +} + +impl Deref for MappedReadGuard +where + F: for<'a> Fn(&'a T) -> &'a U, +{ + type Target = U; + + fn deref(&self) -> &Self::Target { + (self.map)(&self.inner) + } +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize)] +pub enum Theme { + Light, + LightMediumContrast, + LightHighContrast, + Dark, + DarkMediumContrast, + DarkHighContrast, +} + +pub fn set_theme(theme: Theme) { + let mut themer = THEMER.write().unwrap(); + themer.cur_theme = theme; + themer.cur_colors = match theme { + Theme::Light => themer.schemes.light.clone(), + Theme::LightMediumContrast => themer.schemes.light_medium_contrast.clone(), + Theme::LightHighContrast => themer.schemes.light_high_contrast.clone(), + Theme::Dark => themer.schemes.dark.clone(), + Theme::DarkMediumContrast => themer.schemes.dark_medium_contrast.clone(), + Theme::DarkHighContrast => themer.schemes.dark_high_contrast.clone(), + }; +} + +pub fn update_material_colors(json: &str) -> Result<(), DeJsonErr> { + let root: Root = DeJson::deserialize_json(json)?; + let cur_theme = THEMER.read().unwrap().cur_theme; + *THEMER.write().unwrap() = Themer::new(root); + set_theme(cur_theme); + Ok(()) +} + +const DEFAULT_THEME_JSON: &str = include_str!("material-theme.json"); + +struct Themer { + cur_colors: ParsedSemanticColors, + cur_theme: Theme, + palettes: ParsedPalettes, + schemes: ParsedSchemes, +} + +lazy_static! { + static ref THEMER: RwLock = + RwLock::new(Themer::new(parse_json(DEFAULT_THEME_JSON).unwrap())); +} + +impl Themer { + fn new(root: Root) -> Self { + let parsed_palettes: ParsedPalettes = root.palettes.into(); + let parsed_schemes: ParsedSchemes = root.schemes.into(); + + Self { + cur_colors: parsed_schemes.light.clone(), + cur_theme: Theme::Light, + palettes: parsed_palettes, + schemes: parsed_schemes, + } + } +} + +fn parse_json(json: &str) -> Result { + DeJson::deserialize_json(json) +} + +#[derive(Clone)] +/// See https://m3.material.io/styles/color/roles for explanation +pub struct ParsedSemanticColors { + pub primary: Color, + pub on_primary: Color, + pub primary_container: Color, + pub on_primary_container: Color, + + pub secondary: Color, + pub on_secondary: Color, + pub secondary_container: Color, + pub on_secondary_container: Color, + + pub tertiary: Color, + pub on_tertiary: Color, + pub tertiary_container: Color, + pub on_tertiary_container: Color, + + pub error: Color, + pub on_error: Color, + pub error_container: Color, + pub on_error_container: Color, + + pub background: Color, + pub on_background: Color, + + pub surface: Color, + pub surface_tint: Color, + pub on_surface: Color, + pub surface_variant: Color, + pub on_surface_variant: Color, + + pub outline: Color, + pub outline_variant: Color, + + pub shadow: Color, + pub scrim: Color, + + // When you need that extra bit of control + pub surface_dim: Color, + pub surface_bright: Color, + pub surface_container_lowest: Color, + pub surface_container_low: Color, + pub surface_container: Color, + pub surface_container_high: Color, + pub surface_container_highest: Color, + + // Mostly useless but here for completeness + pub inverse_surface: Color, + pub inverse_on_surface: Color, + pub inverse_primary: Color, + + pub primary_fixed: Color, + pub on_primary_fixed: Color, + pub primary_fixed_dim: Color, + pub on_primary_fixed_variant: Color, + + pub secondary_fixed: Color, + pub on_secondary_fixed: Color, + pub secondary_fixed_dim: Color, + pub on_secondary_fixed_variant: Color, + + pub tertiary_fixed: Color, + pub on_tertiary_fixed: Color, + pub tertiary_fixed_dim: Color, + pub on_tertiary_fixed_variant: Color, +} + +#[derive(DeJson)] +struct Palettes { + pub primary: Palette, + pub secondary: Palette, + pub tertiary: Palette, + pub neutral: Palette, + #[nserde(rename = "neutral-variant")] + pub neutral_variant: Palette, +} + +struct ParsedPalettes { + pub primary: [Color; 18], + pub secondary: [Color; 18], + pub tertiary: [Color; 18], + pub neutral: [Color; 18], + pub neutral_variant: [Color; 18], +} + +impl From for ParsedPalettes { + fn from(value: Palettes) -> Self { + fn parse_palette(palette: Palette) -> [Color; 18] { + [ + parse_hex(&palette._0), + parse_hex(&palette._5), + parse_hex(&palette._10), + parse_hex(&palette._15), + parse_hex(&palette._20), + parse_hex(&palette._25), + parse_hex(&palette._30), + parse_hex(&palette._35), + parse_hex(&palette._40), + parse_hex(&palette._50), + parse_hex(&palette._60), + parse_hex(&palette._70), + parse_hex(&palette._80), + parse_hex(&palette._90), + parse_hex(&palette._95), + parse_hex(&palette._98), + parse_hex(&palette._99), + parse_hex(&palette._100), + ] + } + + Self { + primary: parse_palette(value.primary), + secondary: parse_palette(value.secondary), + tertiary: parse_palette(value.tertiary), + neutral: parse_palette(value.neutral), + neutral_variant: parse_palette(value.neutral_variant), + } + } +} + +#[derive(DeJson)] +#[allow(non_snake_case)] +struct SemanticColors { + pub primary: String, + pub surfaceTint: String, + pub onPrimary: String, + pub primaryContainer: String, + pub onPrimaryContainer: String, + pub secondary: String, + pub onSecondary: String, + pub secondaryContainer: String, + pub onSecondaryContainer: String, + pub tertiary: String, + pub onTertiary: String, + pub tertiaryContainer: String, + pub onTertiaryContainer: String, + pub error: String, + pub onError: String, + pub errorContainer: String, + pub onErrorContainer: String, + pub background: String, + pub onBackground: String, + pub surface: String, + pub onSurface: String, + pub surfaceVariant: String, + pub onSurfaceVariant: String, + pub outline: String, + pub outlineVariant: String, + pub shadow: String, + pub scrim: String, + pub inverseSurface: String, + pub inverseOnSurface: String, + pub inversePrimary: String, + pub primaryFixed: String, + pub onPrimaryFixed: String, + pub primaryFixedDim: String, + pub onPrimaryFixedVariant: String, + pub secondaryFixed: String, + pub onSecondaryFixed: String, + pub secondaryFixedDim: String, + pub onSecondaryFixedVariant: String, + pub tertiaryFixed: String, + pub onTertiaryFixed: String, + pub tertiaryFixedDim: String, + pub onTertiaryFixedVariant: String, + pub surfaceDim: String, + pub surfaceBright: String, + pub surfaceContainerLowest: String, + pub surfaceContainerLow: String, + pub surfaceContainer: String, + pub surfaceContainerHigh: String, + pub surfaceContainerHighest: String, +} + +#[derive(DeJson)] +struct Schemes { + pub light: SemanticColors, + #[nserde(rename = "light-medium-contrast")] + pub light_medium_contrast: SemanticColors, + #[nserde(rename = "light-high-contrast")] + pub light_high_contrast: SemanticColors, + pub dark: SemanticColors, + #[nserde(rename = "dark-medium-contrast")] + pub dark_medium_contrast: SemanticColors, + #[nserde(rename = "dark-high-contrast")] + pub dark_high_contrast: SemanticColors, +} + +struct ParsedSchemes { + pub light: ParsedSemanticColors, + pub light_medium_contrast: ParsedSemanticColors, + pub light_high_contrast: ParsedSemanticColors, + pub dark: ParsedSemanticColors, + pub dark_medium_contrast: ParsedSemanticColors, + pub dark_high_contrast: ParsedSemanticColors, +} + +impl From for ParsedSchemes { + fn from(value: Schemes) -> Self { + Self { + light: value.light.into(), + light_medium_contrast: value.light_medium_contrast.into(), + light_high_contrast: value.light_high_contrast.into(), + dark: value.dark.into(), + dark_medium_contrast: value.dark_medium_contrast.into(), + dark_high_contrast: value.dark_high_contrast.into(), + } + } +} + +#[derive(DeJson)] +struct Root { + pub schemes: Schemes, + pub palettes: Palettes, +} + +fn parse_hex(v: &str) -> Color { + Color::hex(u32::from_str_radix(v.trim_start_matches('#'), 16).unwrap()) +} + +impl From for ParsedSemanticColors { + fn from(value: SemanticColors) -> Self { + Self { + primary: parse_hex(&value.primary), + surface_tint: parse_hex(&value.surfaceTint), + on_primary: parse_hex(&value.onPrimary), + primary_container: parse_hex(&value.primaryContainer), + on_primary_container: parse_hex(&value.onPrimaryContainer), + secondary: parse_hex(&value.secondary), + on_secondary: parse_hex(&value.onSecondary), + secondary_container: parse_hex(&value.secondaryContainer), + on_secondary_container: parse_hex(&value.onSecondaryContainer), + tertiary: parse_hex(&value.tertiary), + on_tertiary: parse_hex(&value.onTertiary), + tertiary_container: parse_hex(&value.tertiaryContainer), + on_tertiary_container: parse_hex(&value.onTertiaryContainer), + error: parse_hex(&value.error), + on_error: parse_hex(&value.onError), + error_container: parse_hex(&value.errorContainer), + on_error_container: parse_hex(&value.onErrorContainer), + background: parse_hex(&value.background), + on_background: parse_hex(&value.onBackground), + surface: parse_hex(&value.surface), + on_surface: parse_hex(&value.onSurface), + surface_variant: parse_hex(&value.surfaceVariant), + on_surface_variant: parse_hex(&value.onSurfaceVariant), + outline: parse_hex(&value.outline), + outline_variant: parse_hex(&value.outlineVariant), + shadow: parse_hex(&value.shadow), + scrim: parse_hex(&value.scrim), + inverse_surface: parse_hex(&value.inverseSurface), + inverse_on_surface: parse_hex(&value.inverseOnSurface), + inverse_primary: parse_hex(&value.inversePrimary), + primary_fixed: parse_hex(&value.primaryFixed), + on_primary_fixed: parse_hex(&value.onPrimaryFixed), + primary_fixed_dim: parse_hex(&value.primaryFixedDim), + on_primary_fixed_variant: parse_hex(&value.onPrimaryFixedVariant), + secondary_fixed: parse_hex(&value.secondaryFixed), + on_secondary_fixed: parse_hex(&value.onSecondaryFixed), + secondary_fixed_dim: parse_hex(&value.secondaryFixedDim), + on_secondary_fixed_variant: parse_hex(&value.onSecondaryFixedVariant), + tertiary_fixed: parse_hex(&value.tertiaryFixed), + on_tertiary_fixed: parse_hex(&value.onTertiaryFixed), + tertiary_fixed_dim: parse_hex(&value.tertiaryFixedDim), + on_tertiary_fixed_variant: parse_hex(&value.onTertiaryFixedVariant), + surface_dim: parse_hex(&value.surfaceDim), + surface_bright: parse_hex(&value.surfaceBright), + surface_container_lowest: parse_hex(&value.surfaceContainerLowest), + surface_container_low: parse_hex(&value.surfaceContainerLow), + surface_container: parse_hex(&value.surfaceContainer), + surface_container_high: parse_hex(&value.surfaceContainerHigh), + surface_container_highest: parse_hex(&value.surfaceContainerHighest), + } + } +} + +#[derive(DeJson)] +struct Palette { + #[nserde(rename = "0")] + pub _0: String, + + #[nserde(rename = "5")] + pub _5: String, + + #[nserde(rename = "10")] + pub _10: String, + + #[nserde(rename = "15")] + pub _15: String, + + #[nserde(rename = "20")] + pub _20: String, + + #[nserde(rename = "25")] + pub _25: String, + + #[nserde(rename = "30")] + pub _30: String, + + #[nserde(rename = "35")] + pub _35: String, + + #[nserde(rename = "40")] + pub _40: String, + + #[nserde(rename = "50")] + pub _50: String, + + #[nserde(rename = "60")] + pub _60: String, + + #[nserde(rename = "70")] + pub _70: String, + + #[nserde(rename = "80")] + pub _80: String, + + #[nserde(rename = "90")] + pub _90: String, + + #[nserde(rename = "95")] + pub _95: String, + + #[nserde(rename = "98")] + pub _98: String, + + #[nserde(rename = "99")] + pub _99: String, + + #[nserde(rename = "100")] + pub _100: String, +} diff --git a/goryak/src/util.rs b/goryak/src/util.rs new file mode 100644 index 00000000..1b755123 --- /dev/null +++ b/goryak/src/util.rs @@ -0,0 +1,135 @@ +use std::borrow::Cow; +use std::panic::Location; + +use yakui_core::geometry::{Color, Constraints, Vec2}; +use yakui_core::widget::{LayoutContext, PaintContext, Widget}; +use yakui_core::{Response, WidgetId}; +use yakui_widgets::util::widget; +use yakui_widgets::widgets::{Button, ButtonResponse, Text}; + +use crate::{on_primary, on_secondary, primary, secondary, Scrollable, ScrollableResponse}; + +pub fn scroll_vertical(children: impl FnOnce()) -> Response { + Scrollable::vertical().show(children) +} + +pub fn checkbox_value(v: &mut bool) { + *v = yakui_widgets::checkbox(*v).checked; +} + +pub fn use_changed(v: T, f: impl FnOnce()) { + let old_v = yakui_widgets::use_state(|| None); + if old_v.get() != Some(v) { + old_v.set(Some(v)); + f(); + } +} + +pub fn labelc(c: Color, text: impl Into) { + let mut t = Text::label(Cow::Owned(text.into())); + t.style.color = c; + t.show(); +} + +pub fn button_primary(text: impl Into) -> Response { + let mut b = Button::styled(text.into()); + b.style.fill = primary(); + b.style.text.color = on_primary(); + b.hover_style.fill = primary().adjust(1.2); + b.hover_style.text.color = on_primary(); + b.down_style.fill = primary().adjust(1.3); + b.down_style.text.color = on_primary(); + b.show() +} + +pub fn button_secondary(text: impl Into) -> Response { + let mut b = Button::styled(text.into()); + b.style.fill = secondary(); + b.style.text.color = on_secondary(); + b.hover_style.fill = secondary().adjust(1.2); + b.hover_style.text.color = on_secondary(); + b.down_style.fill = secondary().adjust(1.3); + b.down_style.text.color = on_secondary(); + b.show() +} + +#[track_caller] +pub fn debug_size(r: Response) -> Response { + widget::(DebugSize { + id: Some(r.id), + loc: Location::caller(), + }); + r +} + +#[track_caller] +pub fn debug_size_id(id: WidgetId) { + widget::(DebugSize { + id: Some(id), + loc: Location::caller(), + }); +} + +#[derive(Debug)] +struct DebugSize { + id: Option, + loc: &'static Location<'static>, +} + +impl Widget for DebugSize { + type Props<'a> = DebugSize; + type Response = (); + + fn new() -> Self { + Self { + id: None, + loc: Location::caller(), + } + } + + fn update(&mut self, props: Self::Props<'_>) -> Self::Response { + *self = props; + } + + fn paint(&self, ctx: PaintContext<'_>) { + let layout = ctx.layout; + let Some(id) = self.id else { + return; + }; + let Some(node) = layout.get(id) else { + eprintln!("{}: not found", self.loc); + return; + }; + eprintln!("{}: {:?}", self.loc, node.rect); + } +} + +#[track_caller] +pub fn debug_constraints() { + yakui_widgets::util::widget::(Location::caller()); +} + +#[derive(Debug)] +pub struct DebugConstraints { + props: &'static Location<'static>, +} + +impl Widget for DebugConstraints { + type Props<'a> = &'static Location<'static>; + type Response = (); + + fn new() -> Self { + Self { + props: Location::caller(), + } + } + + fn update(&mut self, props: Self::Props<'_>) -> Self::Response { + self.props = props; + } + + fn layout(&self, ctx: LayoutContext<'_>, constraints: Constraints) -> Vec2 { + println!("{}: {:?}", self.props, constraints); + Widget::default_layout(self, ctx, constraints) + } +}