diff --git a/Cargo.lock b/Cargo.lock index 2d76c04..8c3427a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1965,6 +1965,15 @@ dependencies = [ "libredox", ] +[[package]] +name = "ordered-float" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a76df7075c7d4d01fdcb46c912dd17fba5b60c78ea480b475f2b6ab6f666584e" +dependencies = [ + "num-traits", +] + [[package]] name = "ordered-stream" version = "0.2.0" @@ -3223,6 +3232,7 @@ dependencies = [ "heck", "indexmap", "itertools 0.12.1", + "ordered-float", "puffin", "rfd", "serde", diff --git a/Cargo.toml b/Cargo.toml index 93c1678..f08b9cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,6 +79,7 @@ wasm-bindgen = "0.2" web-sys = { version = "0.3", features = ["Clipboard"] } tracing-tree = "0.3" heck = "0.5" +ordered-float = "4.2" [dependencies] violet-wgpu = { path = "violet-wgpu" } diff --git a/colors.js b/colors.js index 6ed27dc..66453a3 100644 --- a/colors.js +++ b/colors.js @@ -1,4 +1,4 @@ -colors = require("colors.json") +colors = require("./colors.json") console.log(`use palette::Srgba; use crate::srgba; @@ -9,16 +9,22 @@ function kebabToSnakeCase(kebabCaseString) { } for (var color_name in colors) { + let uppercase_name = kebabToSnakeCase(color_name); const tints = colors[color_name]; for (var tint in tints) { const color = tints[tint]; - let color_name = kebabToSnakeCase(color_name); - // console.log(`tint: ${ tint }, name: ${ name }, value: ${ color } `) + + // console.log(`tint: ${ tint }, uppercase_name: ${ uppercase_name }, value: ${ color } `) console.log( - `pub const ${color_name}_${tint.toUpperCase()}: Srgba = srgba!("${color}"); `, + `pub const ${uppercase_name}_${tint.toUpperCase()}: Srgba = srgba!("${color}"); `, ); } - const tint_names = Object.keys(tints).map((tint) => ` ${color_name}_${tint.toUpperCase()},\n`).join(""); - console.log(`pub const ${color_name}_TINTS: [Srgba; ${Object.keys(tints).length}] = [\n${tint_names}];`) +} + +for (var color_name in colors) { + let uppercase_name = kebabToSnakeCase(color_name); + const tints = colors[color_name]; + const tint_names = Object.keys(tints).map((tint) => ` ${uppercase_name}_${tint.toUpperCase()},\n`).join(""); + console.log(`pub const ${uppercase_name}_TINTS: [Srgba; ${Object.keys(tints).length}] = [\n${tint_names}];`) } diff --git a/colors.json b/colors.json new file mode 100644 index 0000000..758308d --- /dev/null +++ b/colors.json @@ -0,0 +1,184 @@ +{ + "stone": { + "50": "#e7e7e7", + "100": "#dadada", + "200": "#bfbfbf", + "300": "#a5a5a5", + "400": "#8b8b8b", + "500": "#737373", + "600": "#5b5b5b", + "700": "#454545", + "800": "#2f2f2f", + "900": "#1b1b1b", + "950": "#121212" + }, + "platinum": { + "50": "#e8e7e6", + "100": "#dbd9d8", + "200": "#c1bebc", + "300": "#a8a4a1", + "400": "#908a86", + "500": "#77726d", + "600": "#5f5a56", + "700": "#474441", + "800": "#312f2d", + "900": "#1c1b1a", + "950": "#131211" + }, + "zinc": { + "50": "#e6e7ea", + "100": "#d8dadd", + "200": "#bdbfc4", + "300": "#a2a5ac", + "400": "#888b95", + "500": "#6f737e", + "600": "#585b64", + "700": "#42454b", + "800": "#2e2f33", + "900": "#1a1b1e", + "950": "#111214" + }, + "cherry": { + "50": "#f7e1e4", + "100": "#ecd2d6", + "200": "#d8b4ba", + "300": "#c8959e", + "400": "#b97582", + "500": "#a45969", + "600": "#844553", + "700": "#61363f", + "800": "#42262c", + "900": "#271519", + "950": "#1b0d10" + }, + "copper": { + "50": "#f6e3d9", + "100": "#ead5c8", + "200": "#d6b8a6", + "300": "#c59b81", + "400": "#b57d59", + "500": "#a06137", + "600": "#814c29", + "700": "#5f3b23", + "800": "#40291a", + "900": "#26170e", + "950": "#1b0f07" + }, + "redwood": { + "50": "#ffded9", + "100": "#f9cec9", + "200": "#e9aea7", + "300": "#de8b82", + "400": "#d5645a", + "500": "#c1413a", + "600": "#9c312b", + "700": "#722a25", + "800": "#4d1f1b", + "900": "#2e110f", + "950": "#210a08" + }, + "amber": { + "50": "#f4e6c5", + "100": "#e9d8b1", + "200": "#d4bd84", + "300": "#c3a046", + "400": "#b48300", + "500": "#9f6700", + "600": "#805100", + "700": "#5e3e00", + "800": "#402c00", + "900": "#261900", + "950": "#1a1000" + }, + "rose": { + "50": "#fcdeea", + "100": "#f2cedd", + "200": "#e0afc4", + "300": "#d28cac", + "400": "#c66895", + "500": "#b1477d", + "600": "#8f3664", + "700": "#692d4b", + "800": "#472133", + "900": "#2a121e", + "950": "#1e0a14" + }, + "forest": { + "50": "#dbefc9", + "100": "#cbe3b5", + "200": "#abcc8a", + "300": "#87b651", + "400": "#62a100", + "500": "#428900", + "600": "#326d00", + "700": "#2a5100", + "800": "#1e3800", + "900": "#102100", + "950": "#091600" + }, + "emerald": { + "50": "#baf7e3", + "100": "#a2ebd5", + "200": "#64d7b8", + "300": "#00c59b", + "400": "#00b47e", + "500": "#009d63", + "600": "#007f4e", + "700": "#005e3c", + "800": "#00402a", + "900": "#002618", + "950": "#001a0f" + }, + "teal": { + "50": "#b3f6f4", + "100": "#9aebe9", + "200": "#50d6d4", + "300": "#00c4c3", + "400": "#00b1b4", + "500": "#009aa0", + "600": "#007c81", + "700": "#005c5f", + "800": "#003f40", + "900": "#002526", + "950": "#001a1a" + }, + "ocean": { + "50": "#cfe8ff", + "100": "#bddbff", + "200": "#96bfff", + "300": "#68a2ff", + "400": "#327fff", + "500": "#005bff", + "600": "#0046dc", + "700": "#063a9d", + "800": "#0b2a68", + "900": "#05183f", + "950": "#020f2f" + }, + "violet": { + "50": "#ecdfff", + "100": "#dfd0ff", + "200": "#c7b0fb", + "300": "#b28df9", + "400": "#9f63fb", + "500": "#8a38ee", + "600": "#6f29c2", + "700": "#51278b", + "800": "#371e5d", + "900": "#201138", + "950": "#160929" + }, + "amethyst": { + "50": "#f4ddff", + "100": "#e9cefb", + "200": "#d4aded", + "300": "#c389e5", + "400": "#b45ee1", + "500": "#9f35d1", + "600": "#8026aa", + "700": "#5e257b", + "800": "#401d52", + "900": "#261031", + "950": "#1a0924" + } +} \ No newline at end of file diff --git a/colors.save.json b/colors.save.json new file mode 100644 index 0000000..219338d --- /dev/null +++ b/colors.save.json @@ -0,0 +1,128 @@ +[ + { + "color": { + "l": 0.49969554, + "chroma": 0.0, + "hue": 0.0 + }, + "falloff": 15.0, + "name": "Stone" + }, + { + "color": { + "l": 0.5, + "chroma": 0.01, + "hue": 62.0 + }, + "falloff": 15.0, + "name": "Platinum" + }, + { + "color": { + "l": 0.49287146, + "chroma": 0.017929163, + "hue": 270.86707 + }, + "falloff": 15.0, + "name": "Zinc" + }, + { + "color": { + "l": 0.49898326, + "chroma": 0.1, + "hue": 7.0 + }, + "falloff": 15.0, + "name": "Cherry" + }, + { + "color": { + "l": 0.50082797, + "chroma": 0.1, + "hue": 52.90864 + }, + "falloff": 15.0, + "name": "Copper" + }, + { + "color": { + "l": 0.5, + "chroma": 0.165, + "hue": 27.0 + }, + "falloff": 15.0, + "name": "Redwood" + }, + { + "color": { + "l": 0.50735986, + "chroma": 0.185, + "hue": 88.0 + }, + "falloff": 15.0, + "name": "Amber" + }, + { + "color": { + "l": 0.51262295, + "chroma": 0.15, + "hue": 351.19986 + }, + "falloff": 15.0, + "name": "Rose" + }, + { + "color": { + "l": 0.5, + "chroma": 0.225, + "hue": 130.0 + }, + "falloff": 15.0, + "name": "Forest" + }, + { + "color": { + "l": 0.5, + "chroma": 0.27, + "hue": 173.0 + }, + "falloff": 15.0, + "name": "Emerald" + }, + { + "color": { + "l": 0.57011175, + "chroma": 0.27, + "hue": 193.86102 + }, + "falloff": 15.0, + "name": "Teal" + }, + { + "color": { + "l": 0.5, + "chroma": 0.27, + "hue": 262.0 + }, + "falloff": 15.0, + "name": "Ocean" + }, + { + "color": { + "l": 0.50050735, + "chroma": 0.25, + "hue": 297.98505 + }, + "falloff": 15.0, + "name": "Violet" + }, + { + "color": { + "l": 0.51195455, + "chroma": 0.23, + "hue": 312.22745 + }, + "falloff": 15.0, + "name": "Amethyst" + } +] \ No newline at end of file diff --git a/examples/color.rs b/examples/color.rs index 095f9b0..090e31b 100644 --- a/examples/color.rs +++ b/examples/color.rs @@ -11,7 +11,7 @@ use violet_core::{ unit::Unit, utils::zip_latest, widget::{ - card, column, row, Rectangle, SignalWidget, SliderWithLabel, Stack, StreamWidget, Text, + card, col, row, Rectangle, SignalWidget, SliderWithLabel, Stack, StreamWidget, Text, }, Edges, Scope, Widget, }; @@ -57,7 +57,7 @@ impl Widget for MainApp { let falloff = Mutable::new(50.0); card( - column(( + col(( row(( Text::new("Lightness"), SliderWithLabel::new(lightness, 0.0, 1.0) @@ -129,7 +129,7 @@ impl Widget for Tints { ..self.base }; - Stack::new(column(( + Stack::new(col(( Rectangle::new(ValueOrRef::value(color.into_color())) .with_min_size(Unit::px2(60.0, 60.0)), Text::new(format!("{:.2}", f)), diff --git a/examples/flow.rs b/examples/flow.rs index dd2b419..cd5069a 100644 --- a/examples/flow.rs +++ b/examples/flow.rs @@ -21,7 +21,7 @@ use violet_core::{ spacing_small, Background, SizeExt, }, text::Wrap, - widget::{card, column, label, row, Button, ButtonStyle, SliderWithLabel, TextInput}, + widget::{card, col, label, row, Button, ButtonStyle, SliderWithLabel, TextInput}, }; use violet_wgpu::renderer::RendererConfig; @@ -59,10 +59,10 @@ impl Widget for MainApp { count: *count, }}); - column(( + col(( row((Text::new("Input: "), TextInput::new(content))), card( - column(( + col(( Button::label("Button"), Button::label("Button").with_style(ButtonStyle { normal_color: style::success_item().into(), @@ -80,8 +80,8 @@ impl Widget for MainApp { .with_stretch(true), ), Rectangle::new(EERIE_BLACK_600).with_size(Unit::rel2(1.0, 0.0) + Unit::px2(0.0, 1.0)), - card(column(( - column(( + card(col(( + col(( row(( Text::new("Size"), SliderWithLabel::new(value, 20.0, 200.0).editable(true), @@ -93,7 +93,7 @@ impl Widget for MainApp { )), SignalWidget::new(item_list), ))), - column( + col( [ // EERIE_BLACK_DEFAULT, // PLATINUM_DEFAULT, @@ -130,7 +130,7 @@ impl Widget for Tints { color_bytes.red, color_bytes.green, color_bytes.blue ); - card(column(( + card(col(( Rectangle::new(color).with_size(Unit::px2(100.0, 40.0)), label(format!("{tint}")), label(color_string), diff --git a/examples/row.rs b/examples/row.rs index 974050d..71f9042 100644 --- a/examples/row.rs +++ b/examples/row.rs @@ -23,7 +23,7 @@ use violet_core::{ }, spacing_medium, spacing_small, SizeExt, }, - widget::{card, centered, column, label, row, Image, Stack}, + widget::{card, centered, col, label, row, Image, Stack}, }; use violet_wgpu::renderer::RendererConfig; @@ -49,7 +49,7 @@ struct MainApp; impl Widget for MainApp { fn mount(self, scope: &mut Scope<'_>) { Stack::new( - column(( + col(( // row(( // label("This text can wrap to save horizontal space"), // card(( diff --git a/examples/sizing.rs b/examples/sizing.rs index 2a59923..bbf04cf 100644 --- a/examples/sizing.rs +++ b/examples/sizing.rs @@ -23,7 +23,7 @@ use violet_core::{ state::MapRef, style::{colors::DARK_CYAN_DEFAULT, SizeExt}, text::Wrap, - widget::{card, centered, column, label, row, Slider}, + widget::{card, centered, col, label, row, Slider}, }; use violet_wgpu::renderer::RendererConfig; @@ -69,7 +69,7 @@ impl Widget for Vec2Editor { let x = MapRef::new(value.clone(), |v| &v.x, |v| &mut v.x); let y = MapRef::new(value.clone(), |v| &v.y, |v| &mut v.y); - column(( + col(( row((label(self.x_label), Slider::new(x, 0.0, 200.0))), row((label(self.y_label), Slider::new(y, 0.0, 200.0))), )) @@ -82,8 +82,8 @@ impl Widget for MainApp { fn mount(self, scope: &mut Scope<'_>) { let size = Mutable::new(vec2(100.0, 100.0)); - column(( - card(column(( + col(( + card(col(( Vec2Editor::new(size.clone(), "width", "height"), SignalWidget::new(size.signal().map(|size| label(format!("Rectangle size: {size}")))), ))), @@ -116,25 +116,25 @@ impl Widget for FlowSizing { AnimatedSize, ); - column(( + col(( row(( - card(column(( + card(col(( label("Unconstrained list"), row(content.clone()).with_background(bg), ))), - card(column(( + card(col(( label("Constrained list with min size"), row(content.clone()) .with_background(bg) .with_min_size(Unit::px2(100.0, 100.0)), ))), - card(column(( + card(col(( label("Constrained list with max size"), row(content.clone()) .with_background(bg) .with_max_size(Unit::px2(100.0, 100.0)), ))), - card(column(( + card(col(( label("Constrained list with max size"), row(content.clone()) .with_background(bg) @@ -143,23 +143,23 @@ impl Widget for FlowSizing { ))), )), row(( - card(column(( + card(col(( label("Unconstrained stack"), centered(content.clone()).with_background(bg), ))), - card(column(( + card(col(( label("Constrained stack with min size"), centered(content.clone()) .with_background(bg) .with_min_size(Unit::px2(100.0, 100.0)), ))), - card(column(( + card(col(( label("Constrained stack with max size"), centered(content.clone()) .with_background(bg) .with_max_size(Unit::px2(100.0, 100.0)), ))), - card(column(( + card(col(( label("Constrained stack with max size"), centered(content.clone()) .with_background(bg) diff --git a/flax b/flax index f75c463..afe5f53 160000 --- a/flax +++ b/flax @@ -1 +1 @@ -Subproject commit f75c463de4fc8390c7f71b211b8cf0a26782bf12 +Subproject commit afe5f53a635e5c2394253a9962cb7b615f430816 diff --git a/violet-core/src/state/constant.rs b/violet-core/src/state/constant.rs index 402c0e4..08404b2 100644 --- a/violet-core/src/state/constant.rs +++ b/violet-core/src/state/constant.rs @@ -10,8 +10,6 @@ impl State for Constant { } impl StateRef for Constant { - type Item = T; - fn read_ref V, V>(&self, f: F) -> V { (f)(&self.0) } diff --git a/violet-core/src/state/memo.rs b/violet-core/src/state/memo.rs new file mode 100644 index 0000000..03e4d0a --- /dev/null +++ b/violet-core/src/state/memo.rs @@ -0,0 +1,86 @@ +use futures::{stream::BoxStream, FutureExt, StreamExt}; +use parking_lot::Mutex; + +use super::{State, StateMut, StateRef, StateSink, StateStream, StateStreamRef}; + +struct Inner { + value: T, + stream: BoxStream<'static, T>, +} + +/// Memo is a state that remembers the last value sent. +/// +/// This allows converting stream only state into ref states. +pub struct Memo { + value: Mutex>, + inner: C, +} + +impl Memo { + /// Create a new memo state. + pub fn new(inner: C, initial_value: T) -> Self + where + C: StateStream, + { + Self { + value: Mutex::new(Inner { + value: initial_value, + stream: inner.stream(), + }), + inner, + } + } +} + +impl State for Memo { + type Item = T; +} + +impl StateRef for Memo { + fn read_ref V, V>(&self, f: F) -> V { + let inner = &mut *self.value.lock(); + if let Some(new_value) = inner.stream.next().now_or_never().flatten() { + inner.value = new_value; + } + + f(&inner.value) + } +} + +impl + StateStream, T: Clone> StateMut for Memo { + fn write_mut V, V>(&self, f: F) -> V { + let inner = &mut *self.value.lock(); + + if let Some(new_value) = inner.stream.next().now_or_never().flatten() { + inner.value = new_value; + } + + let w = f(&mut inner.value); + self.send(inner.value.clone()); + w + } +} + +impl, T: 'static + Clone> StateStreamRef for Memo { + fn stream_ref V, V: 'static + Send>( + &self, + mut func: F, + ) -> impl futures::prelude::Stream + 'static + Send + where + Self: Sized, + { + self.inner.stream().map(move |v| func(&v)).boxed() + } +} + +impl, T: Clone> StateStream for Memo { + fn stream(&self) -> BoxStream<'static, Self::Item> { + self.inner.stream() + } +} + +impl, T: Clone> StateSink for Memo { + fn send(&self, value: Self::Item) { + self.inner.send(value); + } +} diff --git a/violet-core/src/state/mod.rs b/violet-core/src/state/mod.rs index 61d9f18..62f09da 100644 --- a/violet-core/src/state/mod.rs +++ b/violet-core/src/state/mod.rs @@ -12,11 +12,14 @@ mod dedup; mod feedback; mod filter; mod map; +mod memo; pub use dedup::*; pub use feedback::*; pub use filter::*; pub use map::*; +pub use memo::*; + use sync_wrapper::SyncWrapper; pub trait State { @@ -31,6 +34,7 @@ pub trait State { g: G, ) -> MapRef where + Self: StateRef, Self: Sized, { MapRef::new(self, f, g) @@ -75,11 +79,19 @@ pub trait State { { PreventFeedback::new(self) } + + fn memo(self, initial_value: Self::Item) -> Memo + where + Self: Sized, + Self: StateStream, + Self::Item: Clone, + { + Memo::new(self, initial_value) + } } /// A trait to read a reference from a generic state -pub trait StateRef { - type Item; +pub trait StateRef: State { fn read_ref V, V>(&self, f: F) -> V; } @@ -144,7 +156,6 @@ impl State for Mutable { } impl StateRef for Mutable { - type Item = T; fn read_ref V, V>(&self, f: F) -> V { f(&self.lock_ref()) } @@ -222,7 +233,6 @@ where C: StateRef, F: Fn(&C::Item) -> &U, { - type Item = U; fn read_ref V, V>(&self, f: H) -> V { self.inner.read_ref(|v| f((self.project)(v))) } @@ -349,7 +359,6 @@ macro_rules! impl_container { where T: StateRef, { - type Item = T::Item; fn read_ref V, V>(&self, f: F) -> V { (**self).read_ref(f) } diff --git a/violet-core/src/style/colors.rs b/violet-core/src/style/colors.rs index 5bdbe81..313e198 100644 --- a/violet-core/src/style/colors.rs +++ b/violet-core/src/style/colors.rs @@ -1,73 +1,339 @@ -use crate::srgba; use palette::Srgba; +use crate::srgba; -pub const EERIE_BLACK_100: Srgba = srgba!("#070707"); -pub const EERIE_BLACK_200: Srgba = srgba!("#0d0d0d"); -pub const EERIE_BLACK_300: Srgba = srgba!("#101010"); -pub const EERIE_BLACK_400: Srgba = srgba!("#1a1a1a"); -pub const EERIE_BLACK_500: Srgba = srgba!("#212121"); -pub const EERIE_BLACK_600: Srgba = srgba!("#2e2e2e"); -pub const EERIE_BLACK_700: Srgba = srgba!("#4f4f4f"); -pub const EERIE_BLACK_800: Srgba = srgba!("#a6a6a6"); -pub const EERIE_BLACK_900: Srgba = srgba!("#d3d3d3"); -pub const EERIE_BLACK_DEFAULT: Srgba = srgba!("#212121"); -pub const PLATINUM_100: Srgba = srgba!("#302e2b"); -pub const PLATINUM_200: Srgba = srgba!("#5f5c56"); -pub const PLATINUM_300: Srgba = srgba!("#8e8a82"); -pub const PLATINUM_400: Srgba = srgba!("#b9b7b2"); -pub const PLATINUM_500: Srgba = srgba!("#e5e4e2"); -pub const PLATINUM_600: Srgba = srgba!("#eae9e7"); -pub const PLATINUM_700: Srgba = srgba!("#efeeed"); -pub const PLATINUM_800: Srgba = srgba!("#f4f4f3"); -pub const PLATINUM_900: Srgba = srgba!("#faf9f9"); -pub const PLATINUM_DEFAULT: Srgba = srgba!("#e5e4e2"); -pub const JADE_100: Srgba = srgba!("#112116"); -pub const JADE_200: Srgba = srgba!("#21422c"); -pub const JADE_300: Srgba = srgba!("#326443"); -pub const JADE_400: Srgba = srgba!("#438559"); -pub const JADE_500: Srgba = srgba!("#53a66f"); -pub const JADE_600: Srgba = srgba!("#75b98c"); -pub const JADE_700: Srgba = srgba!("#97cba8"); -pub const JADE_800: Srgba = srgba!("#badcc5"); -pub const JADE_900: Srgba = srgba!("#dceee2"); -pub const JADE_DEFAULT: Srgba = srgba!("#53a66f"); -pub const DARK_CYAN_100: Srgba = srgba!("#0d1f1f"); -pub const DARK_CYAN_200: Srgba = srgba!("#1a3e3e"); -pub const DARK_CYAN_300: Srgba = srgba!("#275d5d"); -pub const DARK_CYAN_400: Srgba = srgba!("#347c7c"); -pub const DARK_CYAN_500: Srgba = srgba!("#409999"); -pub const DARK_CYAN_600: Srgba = srgba!("#5bbaba"); -pub const DARK_CYAN_700: Srgba = srgba!("#84cccc"); -pub const DARK_CYAN_800: Srgba = srgba!("#addddd"); -pub const DARK_CYAN_900: Srgba = srgba!("#d6eeee"); -pub const DARK_CYAN_DEFAULT: Srgba = srgba!("#409999"); -pub const ULTRA_VIOLET_100: Srgba = srgba!("#110d1b"); -pub const ULTRA_VIOLET_200: Srgba = srgba!("#211a35"); -pub const ULTRA_VIOLET_300: Srgba = srgba!("#322750"); -pub const ULTRA_VIOLET_400: Srgba = srgba!("#43356b"); -pub const ULTRA_VIOLET_500: Srgba = srgba!("#534185"); -pub const ULTRA_VIOLET_600: Srgba = srgba!("#6f58ad"); -pub const ULTRA_VIOLET_700: Srgba = srgba!("#9382c1"); -pub const ULTRA_VIOLET_800: Srgba = srgba!("#b7acd6"); -pub const ULTRA_VIOLET_900: Srgba = srgba!("#dbd5ea"); -pub const ULTRA_VIOLET_DEFAULT: Srgba = srgba!("#534185"); -pub const REDWOOD_100: Srgba = srgba!("#241210"); -pub const REDWOOD_200: Srgba = srgba!("#49241f"); -pub const REDWOOD_300: Srgba = srgba!("#6d362f"); -pub const REDWOOD_400: Srgba = srgba!("#92483e"); -pub const REDWOOD_500: Srgba = srgba!("#b35a4f"); -pub const REDWOOD_600: Srgba = srgba!("#c37c73"); -pub const REDWOOD_700: Srgba = srgba!("#d29d96"); -pub const REDWOOD_800: Srgba = srgba!("#e1beb9"); -pub const REDWOOD_900: Srgba = srgba!("#f0dedc"); -pub const REDWOOD_DEFAULT: Srgba = srgba!("#b35a4f"); -pub const LION_100: Srgba = srgba!("#231c0e"); -pub const LION_200: Srgba = srgba!("#47381d"); -pub const LION_300: Srgba = srgba!("#6a532b"); -pub const LION_400: Srgba = srgba!("#8e6f3a"); -pub const LION_500: Srgba = srgba!("#b38c49"); -pub const LION_600: Srgba = srgba!("#c3a36b"); -pub const LION_700: Srgba = srgba!("#d2ba90"); -pub const LION_800: Srgba = srgba!("#e1d1b5"); -pub const LION_900: Srgba = srgba!("#f0e8da"); -pub const LION_DEFAULT: Srgba = srgba!("#b38c49"); +pub const STONE_50: Srgba = srgba!("#e7e7e7"); +pub const STONE_100: Srgba = srgba!("#dadada"); +pub const STONE_200: Srgba = srgba!("#bfbfbf"); +pub const STONE_300: Srgba = srgba!("#a5a5a5"); +pub const STONE_400: Srgba = srgba!("#8b8b8b"); +pub const STONE_500: Srgba = srgba!("#737373"); +pub const STONE_600: Srgba = srgba!("#5b5b5b"); +pub const STONE_700: Srgba = srgba!("#454545"); +pub const STONE_800: Srgba = srgba!("#2f2f2f"); +pub const STONE_900: Srgba = srgba!("#1b1b1b"); +pub const STONE_950: Srgba = srgba!("#121212"); +pub const PLATINUM_50: Srgba = srgba!("#e8e7e6"); +pub const PLATINUM_100: Srgba = srgba!("#dbd9d8"); +pub const PLATINUM_200: Srgba = srgba!("#c1bebc"); +pub const PLATINUM_300: Srgba = srgba!("#a8a4a1"); +pub const PLATINUM_400: Srgba = srgba!("#908a86"); +pub const PLATINUM_500: Srgba = srgba!("#77726d"); +pub const PLATINUM_600: Srgba = srgba!("#5f5a56"); +pub const PLATINUM_700: Srgba = srgba!("#474441"); +pub const PLATINUM_800: Srgba = srgba!("#312f2d"); +pub const PLATINUM_900: Srgba = srgba!("#1c1b1a"); +pub const PLATINUM_950: Srgba = srgba!("#131211"); +pub const ZINC_50: Srgba = srgba!("#e6e7ea"); +pub const ZINC_100: Srgba = srgba!("#d8dadd"); +pub const ZINC_200: Srgba = srgba!("#bdbfc4"); +pub const ZINC_300: Srgba = srgba!("#a2a5ac"); +pub const ZINC_400: Srgba = srgba!("#888b95"); +pub const ZINC_500: Srgba = srgba!("#6f737e"); +pub const ZINC_600: Srgba = srgba!("#585b64"); +pub const ZINC_700: Srgba = srgba!("#42454b"); +pub const ZINC_800: Srgba = srgba!("#2e2f33"); +pub const ZINC_900: Srgba = srgba!("#1a1b1e"); +pub const ZINC_950: Srgba = srgba!("#111214"); +pub const CHERRY_50: Srgba = srgba!("#f7e1e4"); +pub const CHERRY_100: Srgba = srgba!("#ecd2d6"); +pub const CHERRY_200: Srgba = srgba!("#d8b4ba"); +pub const CHERRY_300: Srgba = srgba!("#c8959e"); +pub const CHERRY_400: Srgba = srgba!("#b97582"); +pub const CHERRY_500: Srgba = srgba!("#a45969"); +pub const CHERRY_600: Srgba = srgba!("#844553"); +pub const CHERRY_700: Srgba = srgba!("#61363f"); +pub const CHERRY_800: Srgba = srgba!("#42262c"); +pub const CHERRY_900: Srgba = srgba!("#271519"); +pub const CHERRY_950: Srgba = srgba!("#1b0d10"); +pub const COPPER_50: Srgba = srgba!("#f6e3d9"); +pub const COPPER_100: Srgba = srgba!("#ead5c8"); +pub const COPPER_200: Srgba = srgba!("#d6b8a6"); +pub const COPPER_300: Srgba = srgba!("#c59b81"); +pub const COPPER_400: Srgba = srgba!("#b57d59"); +pub const COPPER_500: Srgba = srgba!("#a06137"); +pub const COPPER_600: Srgba = srgba!("#814c29"); +pub const COPPER_700: Srgba = srgba!("#5f3b23"); +pub const COPPER_800: Srgba = srgba!("#40291a"); +pub const COPPER_900: Srgba = srgba!("#26170e"); +pub const COPPER_950: Srgba = srgba!("#1b0f07"); +pub const REDWOOD_50: Srgba = srgba!("#ffded9"); +pub const REDWOOD_100: Srgba = srgba!("#f9cec9"); +pub const REDWOOD_200: Srgba = srgba!("#e9aea7"); +pub const REDWOOD_300: Srgba = srgba!("#de8b82"); +pub const REDWOOD_400: Srgba = srgba!("#d5645a"); +pub const REDWOOD_500: Srgba = srgba!("#c1413a"); +pub const REDWOOD_600: Srgba = srgba!("#9c312b"); +pub const REDWOOD_700: Srgba = srgba!("#722a25"); +pub const REDWOOD_800: Srgba = srgba!("#4d1f1b"); +pub const REDWOOD_900: Srgba = srgba!("#2e110f"); +pub const REDWOOD_950: Srgba = srgba!("#210a08"); +pub const AMBER_50: Srgba = srgba!("#f4e6c5"); +pub const AMBER_100: Srgba = srgba!("#e9d8b1"); +pub const AMBER_200: Srgba = srgba!("#d4bd84"); +pub const AMBER_300: Srgba = srgba!("#c3a046"); +pub const AMBER_400: Srgba = srgba!("#b48300"); +pub const AMBER_500: Srgba = srgba!("#9f6700"); +pub const AMBER_600: Srgba = srgba!("#805100"); +pub const AMBER_700: Srgba = srgba!("#5e3e00"); +pub const AMBER_800: Srgba = srgba!("#402c00"); +pub const AMBER_900: Srgba = srgba!("#261900"); +pub const AMBER_950: Srgba = srgba!("#1a1000"); +pub const ROSE_50: Srgba = srgba!("#fcdeea"); +pub const ROSE_100: Srgba = srgba!("#f2cedd"); +pub const ROSE_200: Srgba = srgba!("#e0afc4"); +pub const ROSE_300: Srgba = srgba!("#d28cac"); +pub const ROSE_400: Srgba = srgba!("#c66895"); +pub const ROSE_500: Srgba = srgba!("#b1477d"); +pub const ROSE_600: Srgba = srgba!("#8f3664"); +pub const ROSE_700: Srgba = srgba!("#692d4b"); +pub const ROSE_800: Srgba = srgba!("#472133"); +pub const ROSE_900: Srgba = srgba!("#2a121e"); +pub const ROSE_950: Srgba = srgba!("#1e0a14"); +pub const FOREST_50: Srgba = srgba!("#dbefc9"); +pub const FOREST_100: Srgba = srgba!("#cbe3b5"); +pub const FOREST_200: Srgba = srgba!("#abcc8a"); +pub const FOREST_300: Srgba = srgba!("#87b651"); +pub const FOREST_400: Srgba = srgba!("#62a100"); +pub const FOREST_500: Srgba = srgba!("#428900"); +pub const FOREST_600: Srgba = srgba!("#326d00"); +pub const FOREST_700: Srgba = srgba!("#2a5100"); +pub const FOREST_800: Srgba = srgba!("#1e3800"); +pub const FOREST_900: Srgba = srgba!("#102100"); +pub const FOREST_950: Srgba = srgba!("#091600"); +pub const EMERALD_50: Srgba = srgba!("#baf7e3"); +pub const EMERALD_100: Srgba = srgba!("#a2ebd5"); +pub const EMERALD_200: Srgba = srgba!("#64d7b8"); +pub const EMERALD_300: Srgba = srgba!("#00c59b"); +pub const EMERALD_400: Srgba = srgba!("#00b47e"); +pub const EMERALD_500: Srgba = srgba!("#009d63"); +pub const EMERALD_600: Srgba = srgba!("#007f4e"); +pub const EMERALD_700: Srgba = srgba!("#005e3c"); +pub const EMERALD_800: Srgba = srgba!("#00402a"); +pub const EMERALD_900: Srgba = srgba!("#002618"); +pub const EMERALD_950: Srgba = srgba!("#001a0f"); +pub const TEAL_50: Srgba = srgba!("#b3f6f4"); +pub const TEAL_100: Srgba = srgba!("#9aebe9"); +pub const TEAL_200: Srgba = srgba!("#50d6d4"); +pub const TEAL_300: Srgba = srgba!("#00c4c3"); +pub const TEAL_400: Srgba = srgba!("#00b1b4"); +pub const TEAL_500: Srgba = srgba!("#009aa0"); +pub const TEAL_600: Srgba = srgba!("#007c81"); +pub const TEAL_700: Srgba = srgba!("#005c5f"); +pub const TEAL_800: Srgba = srgba!("#003f40"); +pub const TEAL_900: Srgba = srgba!("#002526"); +pub const TEAL_950: Srgba = srgba!("#001a1a"); +pub const OCEAN_50: Srgba = srgba!("#cfe8ff"); +pub const OCEAN_100: Srgba = srgba!("#bddbff"); +pub const OCEAN_200: Srgba = srgba!("#96bfff"); +pub const OCEAN_300: Srgba = srgba!("#68a2ff"); +pub const OCEAN_400: Srgba = srgba!("#327fff"); +pub const OCEAN_500: Srgba = srgba!("#005bff"); +pub const OCEAN_600: Srgba = srgba!("#0046dc"); +pub const OCEAN_700: Srgba = srgba!("#063a9d"); +pub const OCEAN_800: Srgba = srgba!("#0b2a68"); +pub const OCEAN_900: Srgba = srgba!("#05183f"); +pub const OCEAN_950: Srgba = srgba!("#020f2f"); +pub const VIOLET_50: Srgba = srgba!("#ecdfff"); +pub const VIOLET_100: Srgba = srgba!("#dfd0ff"); +pub const VIOLET_200: Srgba = srgba!("#c7b0fb"); +pub const VIOLET_300: Srgba = srgba!("#b28df9"); +pub const VIOLET_400: Srgba = srgba!("#9f63fb"); +pub const VIOLET_500: Srgba = srgba!("#8a38ee"); +pub const VIOLET_600: Srgba = srgba!("#6f29c2"); +pub const VIOLET_700: Srgba = srgba!("#51278b"); +pub const VIOLET_800: Srgba = srgba!("#371e5d"); +pub const VIOLET_900: Srgba = srgba!("#201138"); +pub const VIOLET_950: Srgba = srgba!("#160929"); +pub const AMETHYST_50: Srgba = srgba!("#f4ddff"); +pub const AMETHYST_100: Srgba = srgba!("#e9cefb"); +pub const AMETHYST_200: Srgba = srgba!("#d4aded"); +pub const AMETHYST_300: Srgba = srgba!("#c389e5"); +pub const AMETHYST_400: Srgba = srgba!("#b45ee1"); +pub const AMETHYST_500: Srgba = srgba!("#9f35d1"); +pub const AMETHYST_600: Srgba = srgba!("#8026aa"); +pub const AMETHYST_700: Srgba = srgba!("#5e257b"); +pub const AMETHYST_800: Srgba = srgba!("#401d52"); +pub const AMETHYST_900: Srgba = srgba!("#261031"); +pub const AMETHYST_950: Srgba = srgba!("#1a0924"); +pub const STONE_TINTS: [Srgba; 11] = [ + STONE_50, + STONE_100, + STONE_200, + STONE_300, + STONE_400, + STONE_500, + STONE_600, + STONE_700, + STONE_800, + STONE_900, + STONE_950, +]; +pub const PLATINUM_TINTS: [Srgba; 11] = [ + PLATINUM_50, + PLATINUM_100, + PLATINUM_200, + PLATINUM_300, + PLATINUM_400, + PLATINUM_500, + PLATINUM_600, + PLATINUM_700, + PLATINUM_800, + PLATINUM_900, + PLATINUM_950, +]; +pub const ZINC_TINTS: [Srgba; 11] = [ + ZINC_50, + ZINC_100, + ZINC_200, + ZINC_300, + ZINC_400, + ZINC_500, + ZINC_600, + ZINC_700, + ZINC_800, + ZINC_900, + ZINC_950, +]; +pub const CHERRY_TINTS: [Srgba; 11] = [ + CHERRY_50, + CHERRY_100, + CHERRY_200, + CHERRY_300, + CHERRY_400, + CHERRY_500, + CHERRY_600, + CHERRY_700, + CHERRY_800, + CHERRY_900, + CHERRY_950, +]; +pub const COPPER_TINTS: [Srgba; 11] = [ + COPPER_50, + COPPER_100, + COPPER_200, + COPPER_300, + COPPER_400, + COPPER_500, + COPPER_600, + COPPER_700, + COPPER_800, + COPPER_900, + COPPER_950, +]; +pub const REDWOOD_TINTS: [Srgba; 11] = [ + REDWOOD_50, + REDWOOD_100, + REDWOOD_200, + REDWOOD_300, + REDWOOD_400, + REDWOOD_500, + REDWOOD_600, + REDWOOD_700, + REDWOOD_800, + REDWOOD_900, + REDWOOD_950, +]; +pub const AMBER_TINTS: [Srgba; 11] = [ + AMBER_50, + AMBER_100, + AMBER_200, + AMBER_300, + AMBER_400, + AMBER_500, + AMBER_600, + AMBER_700, + AMBER_800, + AMBER_900, + AMBER_950, +]; +pub const ROSE_TINTS: [Srgba; 11] = [ + ROSE_50, + ROSE_100, + ROSE_200, + ROSE_300, + ROSE_400, + ROSE_500, + ROSE_600, + ROSE_700, + ROSE_800, + ROSE_900, + ROSE_950, +]; +pub const FOREST_TINTS: [Srgba; 11] = [ + FOREST_50, + FOREST_100, + FOREST_200, + FOREST_300, + FOREST_400, + FOREST_500, + FOREST_600, + FOREST_700, + FOREST_800, + FOREST_900, + FOREST_950, +]; +pub const EMERALD_TINTS: [Srgba; 11] = [ + EMERALD_50, + EMERALD_100, + EMERALD_200, + EMERALD_300, + EMERALD_400, + EMERALD_500, + EMERALD_600, + EMERALD_700, + EMERALD_800, + EMERALD_900, + EMERALD_950, +]; +pub const TEAL_TINTS: [Srgba; 11] = [ + TEAL_50, + TEAL_100, + TEAL_200, + TEAL_300, + TEAL_400, + TEAL_500, + TEAL_600, + TEAL_700, + TEAL_800, + TEAL_900, + TEAL_950, +]; +pub const OCEAN_TINTS: [Srgba; 11] = [ + OCEAN_50, + OCEAN_100, + OCEAN_200, + OCEAN_300, + OCEAN_400, + OCEAN_500, + OCEAN_600, + OCEAN_700, + OCEAN_800, + OCEAN_900, + OCEAN_950, +]; +pub const VIOLET_TINTS: [Srgba; 11] = [ + VIOLET_50, + VIOLET_100, + VIOLET_200, + VIOLET_300, + VIOLET_400, + VIOLET_500, + VIOLET_600, + VIOLET_700, + VIOLET_800, + VIOLET_900, + VIOLET_950, +]; +pub const AMETHYST_TINTS: [Srgba; 11] = [ + AMETHYST_50, + AMETHYST_100, + AMETHYST_200, + AMETHYST_300, + AMETHYST_400, + AMETHYST_500, + AMETHYST_600, + AMETHYST_700, + AMETHYST_800, + AMETHYST_900, + AMETHYST_950, +]; diff --git a/violet-core/src/style/mod.rs b/violet-core/src/style/mod.rs index 8ffd73f..ae6b4f4 100644 --- a/violet-core/src/style/mod.rs +++ b/violet-core/src/style/mod.rs @@ -14,10 +14,7 @@ use crate::{ Edges, Scope, }; -use self::colors::{ - EERIE_BLACK_300, EERIE_BLACK_600, EERIE_BLACK_700, EERIE_BLACK_DEFAULT, JADE_400, JADE_600, - JADE_DEFAULT, LION_DEFAULT, PLATINUM_DEFAULT, REDWOOD_DEFAULT, -}; +use self::colors::*; #[macro_export] /// Create a color from a hex string @@ -269,21 +266,21 @@ pub fn setup_stylesheet() -> EntityBuilder { builder // colors - .set(primary_background(), EERIE_BLACK_DEFAULT) - .set(primary_item(), PLATINUM_DEFAULT) - .set(secondary_background(), EERIE_BLACK_300) - .set(accent_background(), EERIE_BLACK_DEFAULT) - .set(accent_item(), JADE_DEFAULT) - .set(success_background(), EERIE_BLACK_DEFAULT) - .set(success_item(), JADE_DEFAULT) - .set(warning_background(), EERIE_BLACK_DEFAULT) - .set(warning_item(), LION_DEFAULT) - .set(danger_background(), EERIE_BLACK_DEFAULT) - .set(danger_item(), REDWOOD_DEFAULT) - .set(interactive_active(), JADE_DEFAULT) - .set(interactive_hover(), JADE_600) - .set(interactive_pressed(), JADE_400) - .set(interactive_inactive(), EERIE_BLACK_600) + .set(primary_background(), STONE_950) + .set(primary_item(), PLATINUM_100) + .set(secondary_background(), STONE_900) + .set(accent_background(), PLATINUM_800) + .set(accent_item(), EMERALD_500) + .set(success_background(), EMERALD_800) + .set(success_item(), EMERALD_500) + .set(warning_background(), AMBER_800) + .set(warning_item(), AMBER_500) + .set(danger_background(), REDWOOD_800) + .set(danger_item(), REDWOOD_400) + .set(interactive_active(), EMERALD_500) + .set(interactive_hover(), EMERALD_800) + .set(interactive_pressed(), EMERALD_500) + .set(interactive_inactive(), ZINC_800) // spacing .set(spacing_small(), 4.0.into()) .set(spacing_medium(), 8.0.into()) diff --git a/violet-core/src/widget/basic.rs b/violet-core/src/widget/basic.rs index 6a2d436..73be990 100644 --- a/violet-core/src/widget/basic.rs +++ b/violet-core/src/widget/basic.rs @@ -6,7 +6,7 @@ use crate::{ assets::AssetKey, components::{self, color, draw_shape, font_size, text, text_wrap}, shape, - style::{colors::REDWOOD_DEFAULT, spacing_small, SizeExt, StyleExt, ValueOrRef, WidgetSize}, + style::{colors::REDWOOD_500, spacing_small, SizeExt, StyleExt, ValueOrRef, WidgetSize}, text::{TextSegment, Wrap}, unit::Unit, Scope, Widget, @@ -82,7 +82,7 @@ where .set_opt(components::aspect_ratio(), self.aspect_ratio); } else { label("Image not found") - .with_color(REDWOOD_DEFAULT) + .with_color(REDWOOD_500) .mount(scope); } } diff --git a/violet-core/src/widget/container.rs b/violet-core/src/widget/container.rs index 866db0c..0d10477 100644 --- a/violet-core/src/widget/container.rs +++ b/violet-core/src/widget/container.rs @@ -254,7 +254,7 @@ pub fn row(widgets: W) -> List { List::new(widgets).with_direction(Direction::Horizontal) } -pub fn column(widgets: W) -> List { +pub fn col(widgets: W) -> List { List::new(widgets).with_direction(Direction::Vertical) } diff --git a/violet-core/src/widget/interactive/button.rs b/violet-core/src/widget/interactive/button.rs index 0aacf31..f5f83e7 100644 --- a/violet-core/src/widget/interactive/button.rs +++ b/violet-core/src/widget/interactive/button.rs @@ -13,7 +13,7 @@ use crate::{ }, unit::Unit, widget::{ContainerStyle, Stack, Text}, - Frame, Scope, Widget, + Frame, Scope, Widget, WidgetCollection, }; type ButtonCallback = Box, winit::event::MouseButton)>; @@ -190,14 +190,15 @@ impl Widget for Checkbox { } /// A button that can only be set -pub struct Radio { +pub struct Radio { state: Box>, style: ButtonStyle, size: WidgetSize, + label: W, } -impl Radio { - pub fn new(state: impl 'static + Send + Sync + StateDuplex) -> Self { +impl Radio { + pub fn new(label: W, state: impl 'static + Send + Sync + StateDuplex) -> Self { Self { state: Box::new(state), style: Default::default(), @@ -205,11 +206,12 @@ impl Radio { .with_padding(spacing_medium()) .with_margin(spacing_medium()) .with_min_size(Unit::px2(28.0, 28.0)), + label, } } } -impl Widget for Radio { +impl Widget for Radio { fn mount(self, scope: &mut Scope<'_>) { let stylesheet = scope.stylesheet(); @@ -232,7 +234,7 @@ impl Widget for Radio { } }); - Stack::new(()) + Stack::new(self.label) .with_style(ContainerStyle { background: Some(Background::new(normal_color)), }) diff --git a/violet-demo/Cargo.toml b/violet-demo/Cargo.toml index 34ccb1d..0213a45 100644 --- a/violet-demo/Cargo.toml +++ b/violet-demo/Cargo.toml @@ -20,12 +20,13 @@ console_error_panic_hook = "0.1.6" tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-web = "0.1" web-sys = { version = "0.3", features = ["Gpu", "CanvasRenderingContext2d", "GpuCanvasContext", "GpuRenderBundle"] } +ordered-float.workspace = true wgpu.workspace = true glam.workspace = true futures.workspace = true -wasm-bindgen-futures = "0.4" +wasm-bindgen-futures.workspace = true itertools.workspace = true tracing-tree.workspace = true puffin.workspace = true diff --git a/violet-demo/src/editor.rs b/violet-demo/src/editor.rs new file mode 100644 index 0000000..259c58f --- /dev/null +++ b/violet-demo/src/editor.rs @@ -0,0 +1,172 @@ +use std::sync::Arc; + +use futures::StreamExt; +use glam::Vec2; +use violet::{ + core::{ + state::{DynStateDuplex, State, StateStream}, + style::{SizeExt, ValueOrRef}, + unit::Unit, + widget::{ + card, col, label, row, Radio, Rectangle, SliderWithLabel, StreamWidget, Text, + TextInput, WidgetExt, + }, + Scope, Widget, + }, + futures_signals::signal::Mutable, + palette::{rgb::Rgb, FromColor, IntoColor, OklabHue, Oklch, Srgb}, +}; + +use crate::{color_hex, PaletteColor}; + +#[derive(Debug, Clone, Copy)] +enum EditorMode { + Oklch, + Rgb, +} + +impl EditorMode { + /// Returns `true` if the editor mode is [`Oklch`]. + /// + /// [`Oklch`]: EditorMode::Oklch + #[must_use] + fn is_oklch(&self) -> bool { + matches!(self, Self::Oklch) + } + + /// Returns `true` if the editor mode is [`Rgb`]. + /// + /// [`Rgb`]: EditorMode::Rgb + #[must_use] + fn is_rgb(&self) -> bool { + matches!(self, Self::Rgb) + } +} + +pub fn palette_editor(palette: Mutable) -> impl Widget { + let falloff = palette.clone().map_ref(|v| &v.falloff, |v| &mut v.falloff); + + let color = Arc::new(palette.clone().map_ref(|v| &v.color, |v| &mut v.color)); + let color_rect = color.stream().map(|v| { + Rectangle::new(ValueOrRef::value(v.into_color())) + .with_min_size(Unit::px2(100.0, 100.0)) + .with_maximize(Vec2::X) + // .with_min_size(Unit::new(vec2(0.0, 100.0), vec2(1.0, 0.0))) + .with_name("ColorPreview") + }); + + let current_mode = Mutable::new(EditorMode::Oklch); + + card(col(( + row(( + Radio::new( + label("Oklch"), + current_mode + .clone() + .map(|v| v.is_oklch(), |_| EditorMode::Oklch), + ), + Radio::new( + label("Rgb"), + current_mode + .clone() + .map(|v| v.is_rgb(), |_| EditorMode::Rgb), + ), + )), + StreamWidget(current_mode.stream().map(move |mode| match mode { + EditorMode::Oklch => Box::new(oklch_editor(palette.clone())) as Box, + EditorMode::Rgb => Box::new(rgb_editor(palette.clone())), + })), + ColorHexEditor { + color: Box::new(color.clone()), + }, + StreamWidget(color_rect), + row(( + Text::new("Chroma falloff"), + SliderWithLabel::new(falloff, 0.0, 100.0) + .editable(true) + .round(1.0), + )), + ))) + .with_name("PaletteEditor") +} + +pub struct ColorHexEditor { + color: DynStateDuplex, +} + +impl Widget for ColorHexEditor { + fn mount(self, scope: &mut Scope<'_>) { + let value = self.color.prevent_feedback().filter_map( + |v| Some(color_hex(v)), + |v| { + let v: Srgb = v.trim().parse().ok()?; + + let v = Oklch::from_color(v.into_format()); + Some(v) + }, + ); + + TextInput::new(value).mount(scope) + } +} + +fn oklch_editor(color: Mutable) -> impl Widget { + let color = Arc::new(color.map_ref(|v| &v.color, |v| &mut v.color)); + + let lightness = color.clone().map_ref(|v| &v.l, |v| &mut v.l); + let chroma = color.clone().map_ref(|v| &v.chroma, |v| &mut v.chroma); + let hue = color + .clone() + .map_ref(|v| &v.hue, |v| &mut v.hue) + .map(|v| v.into_positive_degrees(), OklabHue::new); + + col(( + row(( + Text::new("Lightness"), + SliderWithLabel::new(lightness, 0.0, 1.0) + .editable(true) + .round(0.01), + )), + row(( + Text::new("Chroma"), + SliderWithLabel::new(chroma, 0.0, 0.37) + .editable(true) + .round(0.005), + )), + row(( + Text::new("Hue"), + SliderWithLabel::new(hue, 0.0, 360.0) + .editable(true) + .round(1.0), + )), + )) +} + +pub fn rgb_editor(color: Mutable) -> impl Widget { + let rgb_color = Arc::new( + color + .map_ref(|v| &v.color, |v| &mut v.color) + .map(|v| Rgb::from_color(v), |v: Rgb| Oklch::from_color(v)) + .memo(Default::default()), + ); + + let r = rgb_color.clone().map_ref(|v| &v.red, |v| &mut v.red); + let g = rgb_color.clone().map_ref(|v| &v.green, |v| &mut v.green); + let b = rgb_color.clone().map_ref(|v| &v.blue, |v| &mut v.blue); + + card(col(( + row(( + Text::new("Red"), + SliderWithLabel::new(r, 0.0, 1.0).editable(true).round(0.01), + )), + row(( + Text::new("Green"), + SliderWithLabel::new(g, 0.0, 1.0).editable(true).round(0.01), + )), + row(( + Text::new("Blue"), + SliderWithLabel::new(b, 0.0, 1.0).editable(true).round(0.01), + )), + ))) + .with_name("PaletteEditor") +} diff --git a/violet-demo/src/lib.rs b/violet-demo/src/lib.rs index c8cc8d1..0482b6a 100644 --- a/violet-demo/src/lib.rs +++ b/violet-demo/src/lib.rs @@ -1,12 +1,18 @@ -use std::sync::Arc; +mod editor; +mod menu; + +use std::{path::PathBuf, sync::Arc}; use anyhow::Context; +use editor::palette_editor; use flume::Sender; -use futures::{Future, Stream, StreamExt}; +use futures::{future::ready, Future, Stream, StreamExt, TryStreamExt}; use glam::Vec2; use heck::ToKebabCase; use indexmap::IndexMap; use itertools::Itertools; +use menu::menu_bar; +use ordered_float::OrderedFloat; use rfd::AsyncFileDialog; use serde::{Deserialize, Serialize}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; @@ -14,18 +20,18 @@ use violet::{ core::{ declare_atom, layout::Alignment, - state::{DynStateDuplex, State, StateMut, StateStream, StateStreamRef}, + state::{DynStateDuplex, State, StateMut, StateSink, StateStream, StateStreamRef}, style::{ danger_item, primary_background, success_item, warning_item, Background, SizeExt, ValueOrRef, }, - time::interval, + time::{interval, sleep}, to_owned, unit::Unit, - utils::zip_latest_ref, + utils::{zip_latest, zip_latest_ref}, widget::{ - card, centered, column, label, row, Button, Radio, Rectangle, SliderWithLabel, Stack, - StreamWidget, Text, TextInput, WidgetExt, + card, centered, col, label, row, Button, Checkbox, Radio, Rectangle, SliderWithLabel, + Stack, StreamWidget, Text, TextInput, WidgetExt, }, Edges, Scope, Widget, }, @@ -131,7 +137,7 @@ fn tints(color: impl StateStream) -> impl Widget { .with_size(Unit::px2(120.0, 80.0)) }); - Stack::new(column(StreamWidget(color))) + Stack::new(col(StreamWidget(color))) .with_margin(Edges::even(4.0)) .with_name("Tint") }) @@ -160,8 +166,8 @@ declare_atom! { impl Widget for Palettes { fn mount(self, scope: &mut Scope<'_>) { let notify_tx = scope.frame().get_atom(notify_tx()).unwrap().clone(); - let items = self.items.clone(); + let items = self.items.clone(); let discard = move |i| { let items = items.clone(); Button::new(Text::new("-")) @@ -173,15 +179,40 @@ impl Widget for Palettes { .danger() }; + let items = self.items.clone(); + let move_up = move |i| { + let items = items.clone(); + Button::new(Text::new("˰")).on_press({ + move |_, _| { + items.write_mut(|v| { + if i > 0 { + v.swap(i, i - 1); + } + }); + } + }) + }; + + let items = self.items.clone(); + let move_down = move |i| { + let items = items.clone(); + Button::new(Text::new("˯")).on_press({ + move |_, _| { + items.write_mut(|v| { + if i < v.len() - 1 { + v.swap(i, i + 1); + } + }); + } + }) + }; + let current_choice = Mutable::new(Some(0)); let editor = zip_latest_ref( self.items.stream(), current_choice.stream(), - |items, i: &Option| { - i.and_then(|i| items.get(i).cloned()) - .map(PaletteEditor::new) - }, + |items, i: &Option| i.and_then(|i| items.get(i).cloned()).map(palette_editor), ); let palettes = StreamWidget(self.items.stream_ref({ @@ -193,9 +224,12 @@ impl Widget for Palettes { .map({ to_owned![current_choice]; let discard = &discard; + let move_up = &move_up; + let move_down = &move_down; move |(i, item)| { puffin::profile_scope!("Update palette item", format!("{i}")); let checkbox = Radio::new( + (), current_choice .clone() .map(move |v| v == Some(i), move |state| state.then_some(i)), @@ -203,6 +237,8 @@ impl Widget for Palettes { card(row(( checkbox, + move_down(i), + move_up(i), discard(i), palette_color_view(item.clone()), ))) @@ -210,26 +246,44 @@ impl Widget for Palettes { }) .collect_vec(); - column(items) + col(items) } })); let items = self.items.clone(); - let new_color = Button::label("+").on_press(move |_, _| { - items.write_mut(|v| { - v.push(Mutable::new(PaletteColor { - color: Oklch::new(0.5, 0.27, (v.len() as f32 * 60.0) % 360.0), - falloff: DEFAULT_FALLOFF, - name: format!("Color {}", v.len() + 1), - })); - current_choice.set(Some(v.len() - 1)); - }) + let new_color = Button::label("+").on_press({ + to_owned![items]; + move |_, _| { + items.write_mut(|v| { + v.push(Mutable::new(PaletteColor { + color: Oklch::new(0.5, 0.27, (v.len() as f32 * 60.0) % 360.0), + falloff: DEFAULT_FALLOFF, + name: format!("Color {}", v.len() + 1), + })); + current_choice.set(Some(v.len() - 1)); + }) + } + }); + + let sort = Button::label("Sort").on_press({ + to_owned![items]; + move |_, _| { + items.write_mut(|v| { + v.sort_by_cached_key(|v| { + let v = v.lock_ref(); + ( + (v.color.chroma / 0.37 * 5.0) as u32, + OrderedFloat(v.color.hue.into_positive_degrees()), + ) + }); + }); + } }); - let editor_column = column((StreamWidget(editor), palettes, new_color)); + let editor_column = col((StreamWidget(editor), palettes, card(row((new_color, sort))))); - column(( + col(( menu_bar(self.items.clone(), notify_tx), row((editor_column, description())), )) @@ -242,6 +296,12 @@ struct Notification { kind: NotificationKind, } +impl Notification { + fn new(message: String, kind: NotificationKind) -> Self { + Self { message, kind } + } +} + pub enum NotificationKind { Info, Warning, @@ -278,7 +338,7 @@ where }) .collect_vec(); - column(items) + col(items) }); scope.spawn(async move { @@ -332,158 +392,6 @@ This text is also editable, give it a try :)"#.to_string(), card(TextInput::new(content)) } -fn menu_bar( - items: Mutable>>, - notify_tx: Sender, -) -> impl Widget { - async fn notify_result( - fut: impl Future>, - notify_tx: Sender, - on_success: &str, - ) { - match fut.await { - Ok(()) => { - notify_tx - .send(Notification { - message: on_success.into(), - kind: NotificationKind::Info, - }) - .unwrap(); - } - Err(e) => { - notify_tx - .send(Notification { - message: format!("{e:?}"), - kind: NotificationKind::Error, - }) - .unwrap(); - } - } - } - - let export = Button::label("Export Json").on_press({ - to_owned![items, notify_tx]; - move |frame, _| { - let data = items - .lock_ref() - .iter() - .map(|item| { - let item = item.lock_ref(); - let tints = TINTS - .iter() - .map(|&i| { - let color = item.tint(i as f32 / 1000.0); - ( - format!("{}", i), - HexColor(Srgb::from_color(color).into_format()), - ) - }) - .collect::>(); - - (item.name.to_kebab_case(), tints) - }) - .collect::>(); - - let json = serde_json::to_string_pretty(&data).unwrap(); - - let fut = async move { - let Some(file) = AsyncFileDialog::new() - .set_directory(local_dir()) - .set_file_name("colors.json") - .save_file() - .await - else { - return anyhow::Ok(()); - }; - - file.write(json.as_bytes()) - .await - .context("Failed to write to save file")?; - - Ok(()) - }; - - frame.spawn(notify_result(fut, notify_tx.clone(), "Saves")); - } - }); - - let save = Button::label("Save").on_press({ - to_owned![items, notify_tx]; - move |frame, _| { - to_owned![items, notify_tx]; - let fut = async move { - let Some(file) = AsyncFileDialog::new() - .set_directory(local_dir()) - .set_file_name("colors.save.json") - .save_file() - .await - else { - return anyhow::Ok(()); - }; - - let items = items.lock_ref(); - let data = - serde_json::to_string_pretty(&*items).context("Failed to serialize state")?; - - file.write(data.as_bytes()) - .await - .context("Failed to write to save file")?; - - Ok(()) - }; - - frame.spawn(notify_result(fut, notify_tx, "Saves")); - } - }); - - let load = Button::label("Load").on_press({ - to_owned![items, notify_tx]; - move |frame, _| { - to_owned![items, notify_tx]; - let fut = async move { - let Some(file) = AsyncFileDialog::new() - .set_directory(local_dir()) - .pick_file() - .await - else { - return anyhow::Ok(()); - }; - - let data = file.read().await; - - let data = serde_json::from_slice(&data).context("Failed to deserialize state")?; - - items.set(data); - - Ok(()) - }; - - frame.spawn(notify_result(fut, notify_tx, "Loaded")); - } - }); - - let test_notification = Button::label("Test Notification").on_press({ - to_owned![notify_tx]; - move |_, _| { - notify_tx - .send(Notification { - message: "Test notification".to_string(), - kind: NotificationKind::Info, - }) - .unwrap(); - } - }); - - row(( - centered(label("Palette editor")), - save, - load, - export, - test_notification, - )) - .with_stretch(true) -} - #[derive(Clone, Serialize, Deserialize)] pub struct PaletteColor { color: Oklch, @@ -497,7 +405,7 @@ impl PaletteColor { // let color = self.base.lighten(f); Oklch { chroma, - l: tint, + l: (TINT_MAX - TINT_MIN) * (1.0 - tint) + TINT_MIN, ..self.color } } @@ -514,91 +422,6 @@ fn palette_color_view(color: Mutable) -> impl Widget { .with_horizontal_alignment(Alignment::Center) } -pub struct PaletteEditor { - color: Mutable, -} - -impl PaletteEditor { - pub fn new(color: Mutable) -> Self { - Self { color } - } -} - -impl Widget for PaletteEditor { - fn mount(self, scope: &mut Scope<'_>) { - let color = Arc::new(self.color.clone().map_ref(|v| &v.color, |v| &mut v.color)); - let falloff = self.color.map_ref(|v| &v.falloff, |v| &mut v.falloff); - - let lightness = color.clone().map_ref(|v| &v.l, |v| &mut v.l); - let chroma = color.clone().map_ref(|v| &v.chroma, |v| &mut v.chroma); - let hue = color - .clone() - .map_ref(|v| &v.hue, |v| &mut v.hue) - .map(|v| v.into_positive_degrees(), OklabHue::new); - - let color_rect = color.stream().map(|v| { - Rectangle::new(ValueOrRef::value(v.into_color())) - .with_min_size(Unit::px2(100.0, 100.0)) - .with_maximize(Vec2::X) - // .with_min_size(Unit::new(vec2(0.0, 100.0), vec2(1.0, 0.0))) - .with_name("ColorPreview") - }); - - card(column(( - row(( - Text::new("Lightness"), - SliderWithLabel::new(lightness, 0.0, 1.0) - .editable(true) - .round(0.01), - )), - row(( - Text::new("Chroma"), - SliderWithLabel::new(chroma, 0.0, 0.37) - .editable(true) - .round(0.005), - )), - row(( - Text::new("Hue"), - SliderWithLabel::new(hue, 0.0, 360.0) - .editable(true) - .round(1.0), - )), - ColorHexEditor { - color: Box::new(color.clone()), - }, - StreamWidget(color_rect), - row(( - Text::new("Chroma falloff"), - SliderWithLabel::new(falloff, 0.0, 100.0) - .editable(true) - .round(1.0), - )), - ))) - .with_name("PaletteEditor") - .mount(scope) - } -} - -pub struct ColorHexEditor { - color: DynStateDuplex, -} - -impl Widget for ColorHexEditor { - fn mount(self, scope: &mut Scope<'_>) { - let value = self.color.prevent_feedback().filter_map( - |v| Some(color_hex(v)), - |v| { - let v: Srgb = v.trim().parse().ok()?; - - let v = Oklch::from_color(v.into_format()); - Some(v) - }, - ); - - TextInput::new(value).mount(scope) - } -} - pub struct HexColor(Srgb); impl Serialize for HexColor { @@ -627,3 +450,7 @@ impl<'de> Deserialize<'de> for HexColor { } static TINTS: &[i32] = &[50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]; + +/// Going from 0.0 to 1.0 is too dark to be perceptible in the higher ranges +static TINT_MIN: f32 = 0.14; +static TINT_MAX: f32 = 0.97; diff --git a/violet-demo/src/menu.rs b/violet-demo/src/menu.rs new file mode 100644 index 0000000..6181766 --- /dev/null +++ b/violet-demo/src/menu.rs @@ -0,0 +1,170 @@ +use std::collections::BTreeMap; + +use anyhow::Context; +use flume::Sender; +use futures::Future; +use heck::ToKebabCase; +use indexmap::IndexMap; +use itertools::Itertools; +use rfd::AsyncFileDialog; +use violet::{ + core::{ + to_owned, + widget::{centered, label, row, Button}, + Widget, + }, + futures_signals::signal::Mutable, + palette::{num::Sqrt, FromColor, IntoColor, Oklch, Srgb}, +}; + +use crate::{local_dir, HexColor, Notification, NotificationKind, PaletteColor, TINTS}; + +async fn notify_result( + fut: impl Future>, + notify_tx: Sender, + on_success: &str, +) { + match fut.await { + Ok(()) => { + notify_tx + .send(Notification { + message: on_success.into(), + kind: NotificationKind::Info, + }) + .unwrap(); + } + Err(e) => { + notify_tx + .send(Notification { + message: format!("{e:?}"), + kind: NotificationKind::Error, + }) + .unwrap(); + } + } +} + +pub fn menu_bar( + items: Mutable>>, + notify_tx: Sender, +) -> impl Widget { + row(( + centered(label("Palette editor")), + save_button(items.clone(), notify_tx.clone()), + load_button(items.clone(), notify_tx.clone()), + export_button(items.clone(), notify_tx.clone()), + )) + .with_stretch(true) +} + +fn save_items(items: &Vec>) -> anyhow::Result { + let data = serde_json::to_string_pretty(items).context("Failed to serialize state")?; + Ok(data) +} + +type PaletteItems = Vec>; + +fn save_button(items: Mutable, notify_tx: Sender) -> impl Widget { + Button::label("Save").on_press({ + to_owned![items, notify_tx]; + move |frame, _| { + to_owned![items, notify_tx]; + let fut = async move { + let Some(file) = AsyncFileDialog::new() + .set_directory(local_dir()) + .set_file_name("colors.save.json") + .save_file() + .await + else { + return anyhow::Ok(()); + }; + + let data = save_items(&items.lock_ref())?; + + file.write(data.as_bytes()) + .await + .context("Failed to write to save file")?; + + Ok(()) + }; + + frame.spawn(notify_result(fut, notify_tx, "Saved")); + } + }) +} + +fn load_button(items: Mutable, notify_tx: Sender) -> impl Widget { + Button::label("Load").on_press({ + to_owned![items, notify_tx]; + move |frame, _| { + to_owned![items, notify_tx]; + let fut = async move { + let Some(file) = AsyncFileDialog::new() + .set_directory(local_dir()) + .pick_file() + .await + else { + return anyhow::Ok(()); + }; + + let data = file.read().await; + + let data = serde_json::from_slice(&data).context("Failed to deserialize state")?; + + items.set(data); + + Ok(()) + }; + + frame.spawn(notify_result(fut, notify_tx, "Loaded")); + } + }) +} + +fn export_button(items: Mutable, notify_tx: Sender) -> impl Widget { + Button::label("Export Json").on_press({ + to_owned![items, notify_tx]; + move |frame, _| { + let data = items + .lock_ref() + .iter() + .map(|item| { + let item = item.lock_ref(); + let tints = TINTS + .iter() + .map(|&i| { + let color = item.tint(i as f32 / 1000.0); + ( + format!("{}", i), + HexColor(Srgb::from_color(color).into_format()), + ) + }) + .collect::>(); + + (item.name.to_kebab_case(), tints) + }) + .collect::>(); + + let json = serde_json::to_string_pretty(&data).unwrap(); + + let fut = async move { + let Some(file) = AsyncFileDialog::new() + .set_directory(local_dir()) + .set_file_name("colors.json") + .save_file() + .await + else { + return anyhow::Ok(()); + }; + + file.write(json.as_bytes()) + .await + .context("Failed to write to save file")?; + + Ok(()) + }; + + frame.spawn(notify_result(fut, notify_tx.clone(), "Exported")); + } + }) +} diff --git a/violet-wgpu/src/app.rs b/violet-wgpu/src/app.rs index 5ce6ad8..38d6d9d 100644 --- a/violet-wgpu/src/app.rs +++ b/violet-wgpu/src/app.rs @@ -184,8 +184,8 @@ impl AppBuilder { let start_time = Instant::now(); - // #[cfg(not(target_arch = "wasm32"))] - // let _puffin_server = setup_puffin(); + #[cfg(not(target_arch = "wasm32"))] + let _puffin_server = setup_puffin(); let mut instance = App { frame,