From 512feb11a4b1803255285505046adb041af6a372 Mon Sep 17 00:00:00 2001 From: Freja Roberts Date: Tue, 12 Mar 2024 23:20:35 +0100 Subject: [PATCH 01/16] feat: checkboxes --- Cargo.lock | 7 + Cargo.toml | 1 + examples/flow.rs | 8 +- violet-core/Cargo.toml | 1 + violet-core/src/executor.rs | 6 +- violet-core/src/state/dedup.rs | 2 +- violet-core/src/state/feedback.rs | 2 +- violet-core/src/state/filter.rs | 4 +- violet-core/src/state/mod.rs | 50 +++- violet-core/src/utils.rs | 7 +- violet-core/src/widget/interactive/button.rs | 73 +++++- violet-demo/src/lib.rs | 237 +++++++++++-------- violet-wgpu/src/renderer/mod.rs | 19 +- 13 files changed, 283 insertions(+), 134 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index faa5d47..cad2dae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2155,6 +2155,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + [[package]] name = "sys-locale" version = "0.3.1" @@ -2526,6 +2532,7 @@ dependencies = [ "serde", "slab", "slotmap", + "sync_wrapper", "tokio", "tracing", "tynm", diff --git a/Cargo.toml b/Cargo.toml index 305e3b9..422bd34 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ slab = "0.4" tynm ="0.1" tokio = { version = "1.0", default-features = false, features = ["macros", "rt"] } arrayvec = "0.7" +sync_wrapper = "0.1" bytemuck = { version = "1.13", features = ["derive"] } winit = "0.29" diff --git a/examples/flow.rs b/examples/flow.rs index d98b8e0..761b510 100644 --- a/examples/flow.rs +++ b/examples/flow.rs @@ -78,16 +78,16 @@ impl Widget for MainApp { row((Text::new("Input: "), TextInput::new(content))), card( column(( - Button::with_label("Button"), - Button::with_label("Button").with_style(ButtonStyle { + Button::label("Button"), + Button::label("Button").with_style(ButtonStyle { normal_color: style::success_item().into(), ..Default::default() }), - Button::with_label("Warning").with_style(ButtonStyle { + Button::label("Warning").with_style(ButtonStyle { normal_color: style::warning_item().into(), ..Default::default() }), - Button::with_label("Error").with_style(ButtonStyle { + Button::label("Error").with_style(ButtonStyle { normal_color: style::danger_item().into(), ..Default::default() }), diff --git a/violet-core/Cargo.toml b/violet-core/Cargo.toml index 04f14bc..da41b50 100644 --- a/violet-core/Cargo.toml +++ b/violet-core/Cargo.toml @@ -27,6 +27,7 @@ once_cell.workspace = true slab.workspace = true tynm.workspace = true arrayvec.workspace = true +sync_wrapper.workspace = true bytemuck.workspace = true palette.workspace = true diff --git a/violet-core/src/executor.rs b/violet-core/src/executor.rs index 2d241ac..3db4c46 100644 --- a/violet-core/src/executor.rs +++ b/violet-core/src/executor.rs @@ -196,7 +196,11 @@ impl Executor { } for id in self.processing.drain(..) { - let (task, waker) = self.tasks.get_mut(id).unwrap(); + let Some((task, waker)) = self.tasks.get_mut(id) else { + // Task was canceled and thus returned Poll::Ready before being woken by an + // external waker + continue; + }; let mut context = Context::from_waker(&*waker); tracing::trace!(?id, "Polling task"); diff --git a/violet-core/src/state/dedup.rs b/violet-core/src/state/dedup.rs index 302c482..2e43da1 100644 --- a/violet-core/src/state/dedup.rs +++ b/violet-core/src/state/dedup.rs @@ -30,7 +30,7 @@ where T: StateStreamRef, T::Item: 'static + Send + Sync + Clone + PartialEq, { - fn stream_ref V, V: 'static + Send + Sync>( + fn stream_ref V, V: 'static + Send>( &self, mut func: F, ) -> impl futures::prelude::Stream + 'static + Send diff --git a/violet-core/src/state/feedback.rs b/violet-core/src/state/feedback.rs index b57d95b..efeed9d 100644 --- a/violet-core/src/state/feedback.rs +++ b/violet-core/src/state/feedback.rs @@ -29,7 +29,7 @@ where T: StateStreamRef, T::Item: 'static + Send + Sync + Clone + PartialEq, { - fn stream_ref V, V: 'static + Send + Sync>( + fn stream_ref V, V: 'static + Send>( &self, mut func: F, ) -> impl futures::prelude::Stream + 'static + Send diff --git a/violet-core/src/state/filter.rs b/violet-core/src/state/filter.rs index 2bd49ab..f561e67 100644 --- a/violet-core/src/state/filter.rs +++ b/violet-core/src/state/filter.rs @@ -16,9 +16,7 @@ impl State for FilterMap { type Item = U; } -impl Option, G: Fn(U) -> Option> - FilterMap -{ +impl Option, G: Fn(U) -> Option> FilterMap { pub fn new(inner: C, conv_to: F, conv_from: G) -> Self { Self { inner, diff --git a/violet-core/src/state/mod.rs b/violet-core/src/state/mod.rs index 0dab0a0..1f5999c 100644 --- a/violet-core/src/state/mod.rs +++ b/violet-core/src/state/mod.rs @@ -4,7 +4,7 @@ //! smaller parts of a larger state. use std::{marker::PhantomData, rc::Rc, sync::Arc}; -use futures::{stream::BoxStream, Stream, StreamExt}; +use futures::{stream::BoxStream, FutureExt, Stream, StreamExt}; use futures_signals::signal::{Mutable, SignalExt}; mod dedup; @@ -16,6 +16,7 @@ pub use dedup::*; pub use feedback::*; pub use filter::*; pub use map::*; +use sync_wrapper::SyncWrapper; pub trait State { type Item; @@ -101,7 +102,7 @@ pub trait StateStreamRef: State { /// /// The passed function is used to transform the value in the stream, to allow for handling /// non-static or non-cloneable types. - fn stream_ref V, V: 'static + Send + Sync>( + fn stream_ref V, V: 'static + Send>( &self, func: F, ) -> impl Stream + 'static + Send @@ -152,7 +153,7 @@ impl StateStreamRef for Mutable where T: 'static + Send + Sync, { - fn stream_ref V, V: 'static + Send + Sync>( + fn stream_ref V, V: 'static + Send>( &self, func: F, ) -> impl Stream + 'static + Send { @@ -241,7 +242,7 @@ where C: StateStreamRef, F: 'static + Fn(&C::Item) -> &U + Sync + Send, { - fn stream_ref V, V: 'static + Send + Sync>( + fn stream_ref V, V: 'static + Send>( &self, mut func: I, ) -> impl Stream + 'static + Send { @@ -273,6 +274,38 @@ where } } +pub struct WatchState { + stream: SyncWrapper, + last_item: Option, +} + +impl WatchState { + pub fn new(stream: S) -> Self { + Self { + stream: SyncWrapper::new(stream), + last_item: None, + } + } + + pub fn last_item(&self) -> Option<&S::Item> { + self.last_item.as_ref() + } + + pub fn get(&mut self) -> Option<&S::Item> + where + S: Unpin, + { + let new = + std::iter::from_fn(|| self.stream.get_mut().next().now_or_never().flatten()).last(); + + if let Some(new) = new { + self.last_item = Some(new); + } + + self.last_item.as_ref() + } +} + // type MappedMutableStream = // SignalStream V>>>; // pub struct MappedStream { @@ -311,7 +344,7 @@ macro_rules! impl_container { impl StateOwned for $ty where - T: StateOwned, + T: ?Sized + StateOwned, { fn read(&self) -> Self::Item { (**self).read() @@ -331,10 +364,7 @@ macro_rules! impl_container { where T: StateStreamRef, { - fn stream_ref< - F: 'static + Send + Sync + FnMut(&Self::Item) -> V, - V: 'static + Send + Sync, - >( + fn stream_ref V, V: 'static + Send>( &self, func: F, ) -> impl Stream + 'static + Send { @@ -378,7 +408,7 @@ impl StateStreamRef for flume::Receiver where T: 'static + Send + Sync, { - fn stream_ref V, V: 'static + Send + Sync>( + fn stream_ref V, V: 'static + Send>( &self, mut func: F, ) -> impl 'static + Send + Stream { diff --git a/violet-core/src/utils.rs b/violet-core/src/utils.rs index 04b487e..e6ff011 100644 --- a/violet-core/src/utils.rs +++ b/violet-core/src/utils.rs @@ -10,7 +10,12 @@ macro_rules! to_owned { } /// Combines two streams yielding the latest value from each stream -pub fn zip_latest_ref(a: A, b: B, func: F) -> ZipLatest { +pub fn zip_latest_ref(a: A, b: B, func: F) -> ZipLatest +where + A: Stream, + B: Stream, + F: Fn(&A::Item, &B::Item) -> V, +{ ZipLatest::new(a, b, func) } diff --git a/violet-core/src/widget/interactive/button.rs b/violet-core/src/widget/interactive/button.rs index 0f1c8c4..97aee10 100644 --- a/violet-core/src/widget/interactive/button.rs +++ b/violet-core/src/widget/interactive/button.rs @@ -1,15 +1,20 @@ +use futures::Stream; +use futures_signals::signal::Mutable; use palette::Srgba; use winit::event::{ElementState, MouseButton}; use crate::{ - components::color, + components::{self, color}, input::{focusable, on_mouse_input}, layout::Alignment, + state::{StateDuplex, StateMut, StateStream, WatchState}, style::{ danger_item, interactive_inactive, interactive_pressed, spacing_medium, success_item, warning_item, Background, SizeExt, StyleExt, ValueOrRef, WidgetSize, }, - widget::{ContainerStyle, Stack, Text}, + to_owned, + unit::Unit, + widget::{ContainerStyle, Rectangle, Stack, Text}, Frame, Scope, Widget, }; @@ -47,7 +52,10 @@ impl Button { on_press: Box::new(|_, _| {}), label, style: Default::default(), - size: WidgetSize::default().with_padding(spacing_medium()), + size: WidgetSize::default() + .with_padding(spacing_medium()) + .with_margin(spacing_medium()) + .with_min_size(Unit::px2(28.0, 28.0)), } } @@ -77,7 +85,7 @@ impl Button { } impl Button { - pub fn with_label(label: impl Into) -> Self { + pub fn label(label: impl Into) -> Self { Self::new(Text::new(label.into())) } } @@ -125,3 +133,60 @@ impl Widget for Button { .mount(scope); } } + +pub struct Checkbox { + state: Box>, + style: ButtonStyle, + size: WidgetSize, +} + +impl Checkbox { + pub fn new(state: impl 'static + Send + Sync + StateDuplex) -> Self { + Self { + state: Box::new(state), + style: Default::default(), + size: WidgetSize::default() + .with_padding(spacing_medium()) + .with_margin(spacing_medium()) + .with_min_size(Unit::px2(28.0, 28.0)), + } + } +} + +impl Widget for Checkbox { + fn mount(self, scope: &mut Scope<'_>) { + let stylesheet = scope.stylesheet(); + + let pressed_color = self.style.pressed_color.resolve(stylesheet); + let normal_color = self.style.normal_color.resolve(stylesheet); + + scope.spawn_stream(self.state.stream(), { + move |scope, state| { + let color = if state { pressed_color } else { normal_color }; + + scope.set(components::color(), color); + } + }); + + let mut last_state = WatchState::new(self.state.stream()); + + scope + .set(focusable(), ()) + .on_event(on_mouse_input(), move |_, _, input| { + if input.state == ElementState::Pressed { + if let Some(state) = last_state.get() { + self.state.send(!state) + } + } + }); + + Stack::new(()) + .with_style(ContainerStyle { + background: Some(Background::new(normal_color)), + }) + .with_horizontal_alignment(Alignment::Center) + .with_vertical_alignment(Alignment::Center) + .with_size_props(self.size) + .mount(scope); + } +} diff --git a/violet-demo/src/lib.rs b/violet-demo/src/lib.rs index b39ffcd..e76c6c3 100644 --- a/violet-demo/src/lib.rs +++ b/violet-demo/src/lib.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use futures::StreamExt; use glam::{Vec2, Vec3}; use itertools::Itertools; @@ -9,7 +11,7 @@ use violet::{ core::{ components, layout::{Alignment, Direction}, - state::{Map, MapRef, State, StateStream, StateStreamRef}, + state::{Map, MapRef, State, StateMut, StateStream, StateStreamRef}, style::{ colors::{ EERIE_BLACK_400, EERIE_BLACK_DEFAULT, JADE_200, JADE_DEFAULT, LION_DEFAULT, @@ -20,9 +22,9 @@ use violet::{ text::Wrap, to_owned, unit::Unit, - utils::zip_latest, + utils::{zip_latest, zip_latest_ref}, widget::{ - card, column, label, row, Button, ButtonStyle, List, Rectangle, SignalWidget, + card, column, label, row, Button, ButtonStyle, Checkbox, List, Rectangle, SignalWidget, SliderWithLabel, Stack, StreamWidget, Text, WidgetExt, }, Edges, Scope, Widget, WidgetCollection, @@ -77,89 +79,12 @@ struct MainApp; impl Widget for MainApp { fn mount(self, scope: &mut Scope<'_>) { - let color = Mutable::new(Vec3::new(0.5, 0.27, 153.0)); - let color_oklch = Map::new( - color.clone(), - |v| Oklch::new(v.x, v.y, v.z), - |v| Vec3::new(v.l, v.chroma, v.hue.into_positive_degrees()), - ); - - let lightness = color.clone().map_ref(|v| &v.x, |v| &mut v.x); - let chroma = color.clone().map_ref(|v| &v.y, |v| &mut v.y); - let hue = color.clone().map_ref(|v| &v.z, |v| &mut v.z); - - let color_rect = color.signal().map(|v| { - let color = Oklch::new(v.x, v.y, v.z).into_color(); - Rectangle::new(ValueOrRef::value(color)) - .with_size(Unit::new(vec2(0.0, 100.0), vec2(1.0, 0.0))) - }); - - let falloff = Mutable::new(50.0); - - let history = Mutable::new(Vec::new()); + let palette_item = Mutable::new(Vec::new()); - let save_button = Button::new(Text::new("Save color")) - .with_style(ButtonStyle { - normal_color: success_item().into(), - ..Default::default() - }) - .on_press({ - to_owned![history, falloff, color]; - move |_, _| { - let color = color.get(); - history.lock_mut().push(HistoryItem { - color: Oklch::new(color.x, color.y, color.z), - falloff: falloff.get(), - }); - } - }); - - 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), - )), - StreamWidget(color.stream_ref(|v| { - let hex: Srgb = Srgb::from_color(Oklch::new(v.x, v.y, v.z)).into_format(); - Text::new(format!( - "#{:0>2x}{:0>2x}{:0>2x}", - hex.red, hex.green, hex.blue - )) - })), - SignalWidget(color.signal().map(|v| Text::new(format!("{}", v)))), - SignalWidget(color_rect), - row(( - Text::new("Chroma falloff"), - SliderWithLabel::new(falloff.clone(), 0.0, 100.0) - .editable(true) - .round(1.0), - )), - StreamWidget( - zip_latest(color_oklch.stream(), falloff.stream()) - .map(|(color, falloff)| Tints::new(color, falloff)), - ), - save_button, - HistoryView::new(history), - )) - .with_margin(Edges::even(4.0)), - ) - .with_size(Unit::rel2(1.0, 1.0)) - .mount(scope); + card((Palettes::new(palette_item),)) + .with_margin(Edges::even(4.0)) + .with_size(Unit::rel2(1.0, 1.0)) + .mount(scope); } } @@ -191,7 +116,7 @@ impl Widget for Tints { Stack::new(column(( Rectangle::new(ValueOrRef::value(color.into_color())) .with_min_size(Unit::px2(60.0, 60.0)), - Text::new(format!("{:.2}", f)), + // Text::new(format!("{:.2}", f)), ))) .with_margin(Edges::even(4.0)) }) @@ -205,22 +130,23 @@ pub fn color_hex(color: impl IntoColor) -> String { format!("#{:0>2x}{:0>2x}{:0>2x}", hex.red, hex.green, hex.blue) } -pub struct HistoryView { - items: Mutable>, +pub struct Palettes { + items: Mutable>>, } -impl HistoryView { - pub fn new(items: Mutable>) -> Self { +impl Palettes { + pub fn new(items: Mutable>>) -> Self { Self { items } } } -impl Widget for HistoryView { +impl Widget for Palettes { fn mount(self, scope: &mut Scope<'_>) { let items = self.items.clone(); + let discard = move |i| { let items = items.clone(); - Button::new(Text::new("X")) + Button::new(Text::new("-")) .on_press({ move |_, _| { items.lock_mut().remove(i); @@ -229,33 +155,140 @@ impl Widget for HistoryView { .danger() }; - StreamWidget(self.items.stream_ref(move |items| { + let current_choice = Mutable::new(None as Option); + + let editor = zip_latest_ref( + self.items.stream(), + current_choice.stream(), + |items, i: &Option| i.and_then(|i| items.get(i).cloned()).map(OklchEditor::new), + ); + + let palettes = StreamWidget(self.items.stream_ref(move |items| { let items = items .iter() .enumerate() - .map(|(i, item)| card(row((discard(i), *item)))) + .map({ + to_owned![current_choice]; + let discard = &discard; + move |(i, item)| { + let checkbox = Checkbox::new( + current_choice + .clone() + .map(move |v| v == Some(i), move |state| state.then_some(i)), + ); + + card(row((checkbox, discard(i), StreamWidget(item.stream())))) + } + }) .collect_vec(); column(items) - })) + })); + + let items = self.items.clone(); + + column(( + StreamWidget(editor), + palettes, + Button::label("+").on_press(move |_, _| { + items.write_mut(|v| v.push(Mutable::new(PaletteColor::default()))) + }), + )) .mount(scope) } } #[derive(Debug, Clone, Copy)] -pub struct HistoryItem { - color: Oklch, +pub struct PaletteColor { + color: Vec3, falloff: f32, } -impl Widget for HistoryItem { +impl Default for PaletteColor { + fn default() -> Self { + Self { + color: Vec3::new(0.5, 0.27, 153.0), + falloff: 10.0, + } + } +} + +impl Widget for PaletteColor { fn mount(self, scope: &mut Scope<'_>) { + let oklch_color = Oklch::new(self.color.x, self.color.y, self.color.z); column(( - label(color_hex(self.color)), + row((Tints::new(oklch_color, self.falloff),)), + label(color_hex(oklch_color)), + )) + .with_cross_align(Alignment::Center) + .mount(scope) + } +} + +pub struct OklchEditor { + color: Mutable, +} + +impl OklchEditor { + pub fn new(color: Mutable) -> Self { + Self { color } + } +} + +impl Widget for OklchEditor { + 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 color_oklch = Map::new( + color.clone(), + |v| Oklch::new(v.x, v.y, v.z), + |v| Vec3::new(v.l, v.chroma, v.hue.into_positive_degrees()), + ); + + let lightness = color.clone().map_ref(|v| &v.x, |v| &mut v.x); + let chroma = color.clone().map_ref(|v| &v.y, |v| &mut v.y); + let hue = color.clone().map_ref(|v| &v.z, |v| &mut v.z); + + let color_rect = color.stream().map(|v| { + let color = Oklch::new(v.x, v.y, v.z).into_color(); + Rectangle::new(ValueOrRef::value(color)) + .with_size(Unit::new(vec2(0.0, 100.0), vec2(1.0, 0.0))) + }); + + 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), + )), + StreamWidget(color.stream_ref(|v| { + let hex: Srgb = Srgb::from_color(Oklch::new(v.x, v.y, v.z)).into_format(); + Text::new(format!( + "#{:0>2x}{:0>2x}{:0>2x}", + hex.red, hex.green, hex.blue + )) + })), + StreamWidget(color.stream().map(|v| Text::new(format!("{}", v)))), + StreamWidget(color_rect), row(( - Rectangle::new(ValueOrRef::value(self.color.into_color())) - .with_size(Unit::px2(100.0, 50.0)), - Tints::new(self.color, self.falloff), + Text::new("Chroma falloff"), + SliderWithLabel::new(falloff, 0.0, 100.0) + .editable(true) + .round(1.0), )), )) .mount(scope) diff --git a/violet-wgpu/src/renderer/mod.rs b/violet-wgpu/src/renderer/mod.rs index c6c65af..a62adb6 100644 --- a/violet-wgpu/src/renderer/mod.rs +++ b/violet-wgpu/src/renderer/mod.rs @@ -1,4 +1,4 @@ -use std::{collections::VecDeque, sync::Arc}; +use std::sync::Arc; use bytemuck::Zeroable; use flax::{ @@ -281,7 +281,7 @@ impl MainRenderer { let commands = RendererIter { world: &frame.world, - queue: roots, + stack: roots, } .filter_map(|entity| { let mut query = entity.query(&query); @@ -348,7 +348,7 @@ impl MainRenderer { } } -fn collect_draw_commands<'a>( +fn collect_draw_commands( entities: impl Iterator, objects: &mut Vec, draw_cmds: &mut Vec<(usize, InstancedDrawCommand)>, @@ -399,17 +399,22 @@ pub(crate) struct ObjectData { struct RendererIter<'a> { world: &'a World, - queue: VecDeque>, + // queue: VecDeque>, + stack: Vec>, } impl<'a> Iterator for RendererIter<'a> { type Item = EntityRef<'a>; fn next(&mut self) -> Option { - let entity = self.queue.pop_front()?; + let entity = self.stack.pop()?; if let Ok(children) = entity.get(children()) { - self.queue - .extend(children.iter().map(|&id| self.world.entity(id).unwrap())); + self.stack.extend( + children + .iter() + .rev() + .map(|&id| self.world.entity(id).unwrap()), + ); } Some(entity) From a050a7504e8055b3afc863713c57f3b6c0828bb1 Mon Sep 17 00:00:00 2001 From: Freja Roberts Date: Wed, 13 Mar 2024 19:18:42 +0100 Subject: [PATCH 02/16] tmp --- examples/row.rs | 63 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 examples/row.rs diff --git a/examples/row.rs b/examples/row.rs new file mode 100644 index 0000000..46a92bc --- /dev/null +++ b/examples/row.rs @@ -0,0 +1,63 @@ +use tracing_subscriber::{layer::SubscriberExt, registry, util::SubscriberInitExt, EnvFilter}; +use tracing_tree::HierarchicalLayer; + +use violet::core::{ + style::{ + colors::{EERIE_BLACK_DEFAULT, REDWOOD_DEFAULT}, + Background, + }, + unit::Unit, + widget::Rectangle, + Scope, Widget, +}; +use violet_core::{ + style::{ + colors::{JADE_400, JADE_DEFAULT, LION_DEFAULT}, + spacing_medium, SizeExt, + }, + widget::{column, row, Stack}, +}; +use violet_wgpu::renderer::RendererConfig; + +pub fn main() -> anyhow::Result<()> { + registry() + .with( + HierarchicalLayer::default() + .with_deferred_spans(true) + .with_span_retrace(true) + .with_indent_lines(true) + .with_indent_amount(4), + ) + .with(EnvFilter::from_default_env()) + .init(); + + violet_wgpu::App::new() + .with_renderer_config(RendererConfig { debug_mode: true }) + .run(MainApp) +} + +struct MainApp; + +impl Widget for MainApp { + fn mount(self, scope: &mut Scope<'_>) { + Stack::new( + column(( + row(( + column(( + Rectangle::new(JADE_DEFAULT).with_size(Unit::px2(900.0, 40.0)), + Rectangle::new(JADE_400).with_size(Unit::px2(900.0, 40.0)), + )), + Rectangle::new(LION_DEFAULT).with_size(Unit::px2(900.0, 40.0)), + )), + Rectangle::new(REDWOOD_DEFAULT) + .with_min_size(Unit::px2(100.0, 100.0)) + .with_size(Unit::px2(0.0, 100.0) + Unit::rel2(1.0, 0.0)), + // .with_margin(spacing_medium()), + )) + // .with_padding(spacing_medium()) + .contain_margins(true), + ) + .with_background(Background::new(EERIE_BLACK_DEFAULT)) + .mount(scope) + } +} From 836596df8cc680cae198175ee892923581eddfda Mon Sep 17 00:00:00 2001 From: Freja Roberts Date: Thu, 14 Mar 2024 15:45:46 +0100 Subject: [PATCH 03/16] chore: make grow 2D --- .env | 2 +- Cargo.lock | 4 +- Cargo.toml | 6 + examples/row.rs | 4 +- recipes.json | 3 + violet-core/src/constraints.rs | 10 +- violet-core/src/layout/cache.rs | 55 ++++-- violet-core/src/layout/flow.rs | 11 +- violet-core/src/layout/mod.rs | 196 +++++++++---------- violet-core/src/layout/stack.rs | 13 +- violet-core/src/state/map.rs | 16 ++ violet-core/src/state/mod.rs | 12 ++ violet-core/src/style/mod.rs | 6 +- violet-core/src/systems.rs | 3 +- violet-core/src/types.rs | 6 + violet-core/src/unit.rs | 13 +- violet-core/src/widget/container.rs | 17 +- violet-core/src/widget/interactive/input.rs | 6 +- violet-demo/src/lib.rs | 201 +++++++++++--------- violet-wgpu/src/renderer/debug_renderer.rs | 9 +- violet-wgpu/src/renderer/window_renderer.rs | 8 +- violet-wgpu/src/text.rs | 24 +-- 22 files changed, 340 insertions(+), 285 deletions(-) diff --git a/.env b/.env index 9d0069a..72ec0ad 100644 --- a/.env +++ b/.env @@ -1,2 +1,2 @@ -RUST_LOG="info" +# RUST_LOG="info" # RUST_BACKTRACE=1 diff --git a/Cargo.lock b/Cargo.lock index cad2dae..7f8132e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -630,7 +630,7 @@ dependencies = [ [[package]] name = "flax" version = "0.6.2" -source = "git+https://github.com/ten3roberts/flax#39c01757b9ea5f142faeaf9d8b1473e1745ffcca" +source = "git+https://github.com/ten3roberts/flax#bb23e6eaf91195371b96d8725cfef42efdc4b19c" dependencies = [ "anyhow", "atomic_refcell", @@ -648,7 +648,7 @@ dependencies = [ [[package]] name = "flax-derive" version = "0.6.0" -source = "git+https://github.com/ten3roberts/flax#39c01757b9ea5f142faeaf9d8b1473e1745ffcca" +source = "git+https://github.com/ten3roberts/flax#bb23e6eaf91195371b96d8725cfef42efdc4b19c" dependencies = [ "itertools 0.11.0", "proc-macro-crate", diff --git a/Cargo.toml b/Cargo.toml index 422bd34..a0ac819 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -106,6 +106,12 @@ opt-level = 2 [profile.dev.package.parking_lot] opt-level = 2 +[profile.dev.package.puffin] +opt-level = 2 + +[profile.dev.package.puffin_server] +opt-level = 2 + [patch.crates-io] palette = { git = "https://github.com/Ogeon/palette" } diff --git a/examples/row.rs b/examples/row.rs index 46a92bc..400b479 100644 --- a/examples/row.rs +++ b/examples/row.rs @@ -45,7 +45,9 @@ impl Widget for MainApp { row(( column(( Rectangle::new(JADE_DEFAULT).with_size(Unit::px2(900.0, 40.0)), - Rectangle::new(JADE_400).with_size(Unit::px2(900.0, 40.0)), + Rectangle::new(JADE_400) + .with_size(Unit::px2(900.0, 40.0)) + .with_min_size(Unit::px2(400.0, 40.0)), )), Rectangle::new(LION_DEFAULT).with_size(Unit::px2(900.0, 40.0)), )), diff --git a/recipes.json b/recipes.json index e4397fb..71f4881 100644 --- a/recipes.json +++ b/recipes.json @@ -5,6 +5,9 @@ "run demo": { "cmd": "cargo run --package violet-demo" }, + "run demo release": { + "cmd": "cargo run --package violet-demo --release" + }, "run basic": { "cmd": "cargo run --package violet --example basic" }, diff --git a/violet-core/src/constraints.rs b/violet-core/src/constraints.rs index 367a08d..d48c845 100644 --- a/violet-core/src/constraints.rs +++ b/violet-core/src/constraints.rs @@ -1,4 +1,4 @@ -use glam::{vec2, Vec2}; +use glam::{vec2, BVec2, Vec2}; use crate::layout::{Direction, LayoutLimits, SizeResolver, SizingHints}; @@ -42,8 +42,8 @@ impl SizeResolver for FixedAreaConstraint { min * self.unit_size, vec2(size.x, (self.area / size.x).ceil()) * self.unit_size, SizingHints { - can_grow: true, - fixed_size: false, + can_grow: BVec2::TRUE, + relative_size: BVec2::TRUE, }, ) } @@ -53,11 +53,11 @@ impl SizeResolver for FixedAreaConstraint { _: &flax::EntityRef, _: Vec2, limits: crate::layout::LayoutLimits, - ) -> (Vec2, bool) { + ) -> (Vec2, BVec2) { let width = (limits.max_size.x / self.unit_size).floor().max(1.0); let height = (self.area / width).ceil(); - (vec2(width, height) * self.unit_size, true) + (vec2(width, height) * self.unit_size, BVec2::TRUE) } } diff --git a/violet-core/src/layout/cache.rs b/violet-core/src/layout/cache.rs index ca61e2b..9d45c0a 100644 --- a/violet-core/src/layout/cache.rs +++ b/violet-core/src/layout/cache.rs @@ -1,7 +1,7 @@ use flax::{component, components::child_of, Entity, FetchExt, RelationExt, World}; -use glam::Vec2; +use glam::{BVec2, Vec2}; -use super::{flow::Row, Block, Direction, LayoutLimits, Sizing, SizingHints}; +use super::{flow::Row, Block, Direction, LayoutLimits, Sizing}; #[derive(Debug)] pub struct CachedValue { @@ -34,7 +34,7 @@ pub struct LayoutCache { pub(crate) query_row: Option>, pub(crate) layout: Option>, on_invalidated: Option>, - pub(crate) fixed_size: bool, + pub(crate) relative_size: BVec2, } impl LayoutCache { @@ -44,7 +44,7 @@ impl LayoutCache { query_row: None, layout: None, on_invalidated, - fixed_size: false, + relative_size: BVec2::TRUE, } } @@ -86,15 +86,12 @@ impl LayoutCache { pub fn query(&self) -> &[Option>; 2] { &self.query } - - pub fn fixed_size(&self) -> bool { - self.fixed_size - } } /// Invalidates a widgets layout cache along with its ancestors pub(crate) fn invalidate_widget(world: &World, id: Entity) { let entity = world.entity(id).unwrap(); + tracing::info!(%entity, "invalidating widget"); let query = (layout_cache().as_mut(), child_of.first_relation().opt()); let mut query = entity.query(&query); @@ -117,7 +114,14 @@ pub(crate) fn validate_cached_query( let min_size = value.min.size(); let preferred_size = value.preferred.size(); - tracing::debug!( ?preferred_size, %cache.limits.max_size, %limits.max_size, "validate_cached_query"); + // tracing::debug!( ?preferred_size, %cache.limits.max_size, %limits.max_size, "validate_cached_query"); + + let hints = &value.hints; + if hints.can_grow.x && cache.limits.max_size.x < limits.max_size.x + || (hints.can_grow.x && cache.limits.max_size.y < limits.max_size.y) + { + tracing::info!(%hints.can_grow, ?cache.limits.max_size, %limits.max_size, "invalidated by can_grow"); + } min_size.x >= limits.min_size.x - LAYOUT_TOLERANCE && min_size.y >= limits.min_size.y - LAYOUT_TOLERANCE @@ -126,8 +130,10 @@ pub(crate) fn validate_cached_query( && min_size.y <= limits.max_size.y + LAYOUT_TOLERANCE && preferred_size.x <= limits.max_size.x + LAYOUT_TOLERANCE && preferred_size.y <= limits.max_size.y + LAYOUT_TOLERANCE - && (!value.hints.can_grow || cache.limits.max_size.abs_diff_eq(limits.max_size, LAYOUT_TOLERANCE)) - && (value.hints.fixed_size || cache.content_area.abs_diff_eq(content_area, LAYOUT_TOLERANCE)) + && (!hints.can_grow.x || cache.limits.max_size.x >= limits.max_size.x - LAYOUT_TOLERANCE) + && (!hints.can_grow.y || cache.limits.max_size.y >= limits.max_size.y - LAYOUT_TOLERANCE) + && (!hints.relative_size.x || (cache.content_area.x - content_area.x).abs() < LAYOUT_TOLERANCE) + && (!hints.relative_size.y || (cache.content_area.y - content_area.y).abs() < LAYOUT_TOLERANCE) } pub(crate) fn validate_cached_layout( @@ -135,21 +141,29 @@ pub(crate) fn validate_cached_layout( limits: LayoutLimits, content_area: Vec2, // Calculated from the query stage - fixed_size: bool, + relative_size: BVec2, ) -> bool { let value = &cache.value; - let size = value.rect.size(); + let size = value.rect.size().min(cache.limits.max_size); + + // tracing::debug!( ?size, %cache.limits.max_size, %limits.max_size, "validate_cached_layout"); - tracing::debug!( ?size, %cache.limits.max_size, %limits.max_size, "validate_cached_layout"); + if value.can_grow.x && cache.limits.max_size.x < limits.max_size.x + || (value.can_grow.x && cache.limits.max_size.y < limits.max_size.y) + { + tracing::info!(%value.can_grow, ?cache.limits.max_size, %limits.max_size, "invalidated layout by can_grow"); + } size.x >= limits.min_size.x - LAYOUT_TOLERANCE && size.y >= limits.min_size.y - LAYOUT_TOLERANCE // Min may be larger than preferred for the orthogonal optimization direction && size.x <= limits.max_size.x + LAYOUT_TOLERANCE && size.y <= limits.max_size.y + LAYOUT_TOLERANCE - && (!value.can_grow || cache.limits.max_size.abs_diff_eq(limits.max_size, LAYOUT_TOLERANCE)) - && (fixed_size || cache.content_area.abs_diff_eq(content_area, LAYOUT_TOLERANCE)) + && (!value.can_grow.x || cache.limits.max_size.x >= limits.max_size.x - LAYOUT_TOLERANCE) + && (!value.can_grow.y || cache.limits.max_size.y >= limits.max_size.y - LAYOUT_TOLERANCE) + && (!relative_size.x || (cache.content_area.x - content_area.x).abs() < LAYOUT_TOLERANCE) + && (!relative_size.y || (cache.content_area.y - content_area.y).abs() < LAYOUT_TOLERANCE) } pub(crate) fn validate_cached_row( @@ -161,8 +175,9 @@ pub(crate) fn validate_cached_row( let min_size = value.min.size(); let preferred_size = value.preferred.size(); + let hints = value.hints; - tracing::debug!( ?preferred_size, %cache.limits.max_size, %limits.max_size, "validate_cached_row"); + // tracing::debug!( ?preferred_size, %cache.limits.max_size, %limits.max_size, "validate_cached_row"); min_size.x >= limits.min_size.x - LAYOUT_TOLERANCE && min_size.y >= limits.min_size.y - LAYOUT_TOLERANCE @@ -171,8 +186,10 @@ pub(crate) fn validate_cached_row( && min_size.y <= limits.max_size.y + LAYOUT_TOLERANCE && preferred_size.x <= limits.max_size.x + LAYOUT_TOLERANCE && preferred_size.y <= limits.max_size.y + LAYOUT_TOLERANCE - && (!value.hints.can_grow || cache.limits.max_size.abs_diff_eq(limits.max_size, LAYOUT_TOLERANCE)) - && (value.hints.fixed_size || cache.content_area.abs_diff_eq(content_area, LAYOUT_TOLERANCE)) + && (!hints.can_grow.x || cache.limits.max_size.x >= limits.max_size.x - LAYOUT_TOLERANCE) + && (!hints.can_grow.y || cache.limits.max_size.y >= limits.max_size.y - LAYOUT_TOLERANCE) + && (!hints.relative_size.x || (cache.content_area.x - content_area.x).abs() < LAYOUT_TOLERANCE) + && (!hints.relative_size.y || (cache.content_area.y - content_area.y).abs() < LAYOUT_TOLERANCE) } component! { diff --git a/violet-core/src/layout/flow.rs b/violet-core/src/layout/flow.rs index ea8b2e2..e14e515 100644 --- a/violet-core/src/layout/flow.rs +++ b/violet-core/src/layout/flow.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use flax::{Entity, EntityRef, World}; -use glam::{vec2, Vec2}; +use glam::{vec2, BVec2, Vec2}; use itertools::Itertools; use crate::{ @@ -260,7 +260,7 @@ impl FlowLayout { let cross_size = row.preferred.size().max(preferred_size).dot(cross_axis); - let mut can_grow = false; + let mut can_grow = BVec2::FALSE; // Distribute the size to the widgets and apply their layout let blocks = row .blocks @@ -328,7 +328,7 @@ impl FlowLayout { // let local_rect = widget_outer_bounds(world, &child, size); let block = update_subtree(world, &entity, content_area.size(), child_limits); - can_grow = can_grow || block.can_grow; + can_grow |= block.can_grow; tracing::debug!(?block, "updated subtree"); // block.rect = block @@ -462,10 +462,7 @@ impl FlowLayout { let mut sum = 0.0; let cross_size = row.preferred.size().max(preferred_size).dot(cross_axis); - let mut hints = SizingHints { - fixed_size: true, - can_grow: false, - }; + let mut hints = SizingHints::default(); // Distribute the size to the widgets and apply their layout row diff --git a/violet-core/src/layout/mod.rs b/violet-core/src/layout/mod.rs index d16adf9..d1d567d 100644 --- a/violet-core/src/layout/mod.rs +++ b/violet-core/src/layout/mod.rs @@ -5,7 +5,7 @@ mod stack; use std::fmt::{Display, Formatter}; use flax::{Entity, EntityRef, FetchExt, World}; -use glam::{vec2, Vec2}; +use glam::{vec2, BVec2, Vec2}; use crate::{ components::{ @@ -120,6 +120,14 @@ impl Layout { } } +#[derive(Debug, Clone)] +pub(crate) struct QueryArgs { + limits: LayoutLimits, + content_area: Vec2, + distribute: bool, + direction: Direction, +} + #[derive(Debug, Clone, Copy)] pub struct Sizing { min: Rect, @@ -149,43 +157,49 @@ pub struct LayoutLimits { pub max_size: Vec2, } +impl Display for LayoutLimits { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "min: {}, max: {}", self.min_size, self.max_size) + } +} + /// A block is a rectangle and surrounding support such as margin #[derive(Debug, Clone, Copy, Default)] pub struct Block { pub(crate) rect: Rect, pub(crate) margin: Edges, - /// See: [Sizing::clamped] - pub can_grow: bool, + /// See: [SizingHints::can_grow] + pub can_grow: BVec2, } impl Block { - pub(crate) fn new(rect: Rect, margin: Edges, clamped: bool) -> Self { + pub(crate) fn new(rect: Rect, margin: Edges, can_grow: BVec2) -> Self { Self { rect, margin, - can_grow: clamped, + can_grow, } } } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct SizingHints { /// Size does not depend on the size of the parent - pub can_grow: bool, + pub can_grow: BVec2, /// The widget size is clamped given the provided size limits, and could be larger. /// /// If this is true, giving *more* space to a widget may cause it to grow. /// /// This is used for an optimization to avoid invalidating the layout when the available size /// increases - pub fixed_size: bool, + pub relative_size: BVec2, } impl Default for SizingHints { fn default() -> Self { Self { - can_grow: false, - fixed_size: true, + can_grow: BVec2::FALSE, + relative_size: BVec2::FALSE, } } } @@ -193,8 +207,8 @@ impl Default for SizingHints { impl SizingHints { pub fn combine(self, other: Self) -> Self { Self { - can_grow: self.can_grow || other.can_grow, - fixed_size: self.fixed_size && other.fixed_size, + can_grow: self.can_grow | other.can_grow, + relative_size: self.relative_size | other.relative_size, } } } @@ -274,6 +288,7 @@ pub(crate) fn query_size( tracing::debug_span!("query_size", name=entity.name().as_deref(), ?limits, %content_area) .entered(); + // tracing::info!(name=entity.name().as_deref(), ?limits, %content_area, ?direction, "query_size"); let query = ( layout_cache().as_mut(), components::margin().opt_or_default(), @@ -290,12 +305,15 @@ pub(crate) fn query_size( let (cache, &margin, &padding, min_size, max_size, size, size_resolver, children, layout) = query.get().unwrap(); - let fixed_boundary_size = min_size.is_fixed() && max_size.map(|v| v.is_fixed()).unwrap_or(true); + let fixed_boundary_size = + min_size.is_relative() | max_size.map(|v| v.is_relative()).unwrap_or(BVec2::FALSE); let min_size = min_size.resolve(content_area); let max_size = max_size.map(|v| v.resolve(content_area)); limits.min_size = limits.min_size.max(min_size); + let external_max_size = limits.max_size; + // Minimum size is *always* respected, even if that entails overflowing limits.max_size = limits.max_size.max(limits.min_size); @@ -306,12 +324,7 @@ pub(crate) fn query_size( // Check if cache is valid if let Some(cache) = &cache.query[direction as usize] { if validate_cached_query(cache, limits, content_area) { - // if cache.is_valid(limits, content_area) { - let _span = tracing::trace_span!("cached").entered(); - // validate_sizing(entity, &cache.value, limits); - tracing::debug!(%entity, "found valid cached query"); return cache.value; - // } } } @@ -324,10 +337,17 @@ pub(crate) fn query_size( let resolved_size = size.resolve(content_area); let hints = SizingHints { - fixed_size: fixed_boundary_size && size.is_fixed(), - can_grow: resolved_size.x > limits.max_size.x || resolved_size.y > limits.max_size.y, + relative_size: fixed_boundary_size | size.is_relative(), + can_grow: BVec2::new( + resolved_size.x > external_max_size.x, + resolved_size.y > external_max_size.y, + ), }; + // if hints != Default::default() { + // tracing::info!(%entity, ?resolved_size, ?external_max_size, "can grow"); + // } + // Clamp max size here since we ensure it is > min_size let resolved_size = resolved_size.clamp(limits.min_size, limits.max_size); @@ -388,22 +408,21 @@ pub(crate) fn query_size( sizing.min = sizing.min.translate(min_offset); sizing.preferred = sizing.preferred.translate(offset); - // Widget size is limited by itself and is not affected by the size of the parent - if let Some(max_size) = max_size { - if sizing - .preferred - .size() - .abs_diff_eq(max_size, LAYOUT_TOLERANCE) - { - sizing.hints.can_grow = false; - } - } + // // Widget size is limited by itself and is not affected by the size of the parent + // if let Some(max_size) = max_size { + // if sizing + // .preferred + // .size() + // .abs_diff_eq(max_size, LAYOUT_TOLERANCE) + // { + // sizing.hints.can_grow = false; + // } + // } // validate_sizing(entity, &sizing, limits); - tracing::debug!(%sizing); cache.insert_query(direction, CachedValue::new(limits, content_area, sizing)); - cache.fixed_size = sizing.hints.fixed_size; + cache.relative_size = sizing.hints.relative_size; sizing } @@ -442,7 +461,9 @@ pub(crate) fn update_subtree( query.get().unwrap(); let min_size = min_size.resolve(content_area); let max_size = max_size.map(|v| v.resolve(content_area)); + let external_max_size = limits.max_size; + tracing::debug!(%min_size, ?max_size, %limits); limits.min_size = limits.min_size.max(min_size); limits.max_size = limits.max_size.max(limits.min_size); @@ -453,10 +474,12 @@ pub(crate) fn update_subtree( // Check if cache is still valid if let Some(value) = &cache.layout { - if validate_cached_layout(value, limits, content_area, cache.fixed_size) { - tracing::debug!(%entity, ?value, "found valid cached layout"); - // validate_block(entity, &value.value, limits); + if validate_cached_layout(value, limits, content_area, cache.relative_size) { + tracing::debug!(%entity, %value.value.rect, %value.value.can_grow, "found valid cached layout"); + return value.value; + } else { + tracing::debug!(%entity, ?value, "invalid cached layout"); } } @@ -471,7 +494,14 @@ pub(crate) fn update_subtree( let resolved_size = size.resolve(content_area); - let can_grow = resolved_size.x > limits.max_size.x || resolved_size.y > limits.max_size.y; + let can_grow = BVec2::new( + resolved_size.x > external_max_size.x, + resolved_size.y > external_max_size.y, + ); + + if can_grow.any() { + tracing::info!(%entity, ?resolved_size, ?external_max_size, "can grow"); + } let resolved_size = resolved_size.clamp(limits.min_size, limits.max_size); @@ -505,7 +535,7 @@ pub(crate) fn update_subtree( let (intrinsic_size, instrinsic_can_grow) = size_resolver .map(|v| v.apply(entity, content_area, limits)) - .unwrap_or((Vec2::ZERO, false)); + .unwrap_or((Vec2::ZERO, BVec2::TRUE)); let size = intrinsic_size.max(resolved_size); @@ -514,10 +544,20 @@ pub(crate) fn update_subtree( Block { rect, margin, - can_grow: instrinsic_can_grow || can_grow, + can_grow: instrinsic_can_grow | can_grow, } }; + // if block.rect.size().x > limits.max_size.x || block.rect.size().y > limits.max_size.y { + // tracing::error!( + // %entity, + // rect_size = %block.rect.size(), + // %limits.max_size, + // "Widget size exceeds constraints", + // ); + // panic!(""); + // } + let constraints = Constraints::from_entity(entity); block.rect = block.rect.with_size(constraints.apply(block.rect.size())); @@ -527,11 +567,11 @@ pub(crate) fn update_subtree( entity.update_dedup(components::layout_bounds(), block.rect.size()); // Widget size is limited by itself and is not affected by the size of the parent - if let Some(max_size) = max_size { - if block.rect.size().abs_diff_eq(max_size, LAYOUT_TOLERANCE) { - block.can_grow = false; - } - } + // if let Some(max_size) = max_size { + // if block.rect.size().abs_diff_eq(max_size, LAYOUT_TOLERANCE) { + // block.can_grow = BVec2::FALSE; + // } + // } // if block.rect.size().x > limits.max_size.x || block.rect.size().y > limits.max_size.y { // tracing::error!( @@ -542,6 +582,7 @@ pub(crate) fn update_subtree( // validate_block(entity, &block, limits); + tracing::debug!(%limits, %content_area, %block.can_grow, %block.rect, "caching layout"); cache.insert_layout(CachedValue::new(limits, content_area, block)); block @@ -568,7 +609,7 @@ pub trait SizeResolver: Send + Sync { entity: &EntityRef, content_area: Vec2, limits: LayoutLimits, - ) -> (Vec2, bool); + ) -> (Vec2, BVec2); } #[derive(Debug)] @@ -599,73 +640,6 @@ impl Constraints { } } -// fn query_constraints( -// entity: &EntityRef, -// content_area: Vec2, -// limits: LayoutLimits, -// squeeze: Direction, -// ) -> (Vec2, Vec2, SizingHints) { -// let (mut size, constraints, fixed_size) = resolve_base_size(entity, content_area); - -// let clamped = size.x > limits.max_size.x || size.y > limits.max_size.y; -// let mut min_size = limits.min_size; - -// if let Ok(mut resolver) = entity.get_mut(components::size_resolver()) { -// let (resolved_min, resolved_size, hints) = -// resolver.query(entity, content_area, limits, squeeze); - -// let optimize_axis = squeeze.to_axis(); -// if resolved_min.dot(optimize_axis) > resolved_size.dot(optimize_axis) { -// panic!("Size resolver returned a minimum size that is larger than the preferred size for the given optimization\n\nmin: {}, size: {}, widget: {}", resolved_min.dot(optimize_axis), resolved_size.dot(optimize_axis), entity); -// } - -// min_size = resolved_min; -// size = resolved_size.max(size); - -// ( -// constraints.resolve(min_size.clamp(limits.min_size, limits.max_size)), -// constraints.resolve(size.clamp(limits.min_size, limits.max_size)), -// SizingHints { -// fixed_size: fixed_size && hints.fixed_size, -// can_grow: clamped || hints.can_grow, -// }, -// ) -// } else { -// // tracing::info!(?min_size, ?size, ?limits, "query_constraints"); - -// ( -// constraints.resolve(min_size.clamp(limits.min_size, limits.max_size)), -// constraints.resolve(size.clamp(limits.min_size, limits.max_size)), -// SizingHints { -// can_grow: clamped, -// fixed_size, -// }, -// ) -// } -// } - -// fn apply_constraints(entity: &EntityRef, content_area: Vec2, limits: LayoutLimits) -> (Vec2, bool) { -// let (size, constraints, _) = resolve_base_size(entity, content_area); - -// let clamped = size.x > limits.max_size.x || size.y > limits.max_size.y; - -// if let Ok(mut resolver) = entity.get_mut(components::size_resolver()) { -// let (resolved_size, resolved_clamped) = resolver.apply(entity, content_area, limits); - -// let size = resolved_size.max(size); - -// ( -// constraints.resolve(size.clamp(limits.min_size, limits.max_size)), -// clamped || resolved_clamped, -// ) -// } else { -// ( -// constraints.resolve(size.clamp(limits.min_size, limits.max_size)), -// clamped, -// ) -// } -// } - /// Resolves a widgets position relative to its own bounds fn resolve_pos(entity: &EntityRef, parent_size: Vec2, self_size: Vec2) -> Vec2 { let query = (offset().opt_or_default(), anchor().opt_or_default()); diff --git a/violet-core/src/layout/stack.rs b/violet-core/src/layout/stack.rs index 783a6a5..6a240c5 100644 --- a/violet-core/src/layout/stack.rs +++ b/violet-core/src/layout/stack.rs @@ -1,5 +1,5 @@ use flax::{Entity, EntityRef, World}; -use glam::{vec2, Vec2}; +use glam::{vec2, BVec2, Vec2}; use itertools::Itertools; use crate::{ @@ -98,7 +98,7 @@ impl StackLayout { preferred_size: Vec2, ) -> Block { puffin::profile_function!(); - let _span = tracing::info_span!("StackLayout::apply").entered(); + let _span = tracing::debug_span!("StackLayout::apply").entered(); let mut bounds = Rect { min: Vec2::MAX, @@ -132,7 +132,7 @@ impl StackLayout { let mut aligned_bounds = StackableBounds::from_rect(Rect::from_size_pos(preferred_size, content_area.min)); - let mut can_grow = false; + let mut can_grow = BVec2::FALSE; let offset = resolve_pos(entity, content_area.size(), size); for (entity, block) in blocks { @@ -151,7 +151,7 @@ impl StackLayout { block.margin, )); - can_grow = can_grow || block.can_grow; + can_grow |= block.can_grow; // entity.update_dedup(components::rect(), block.rect.translate(offset)); entity.update_dedup(components::rect(), block.rect); @@ -183,10 +183,7 @@ impl StackLayout { let mut min_bounds = StackableBounds::from_rect(min_rect); let mut preferred_bounds = StackableBounds::from_rect(min_rect); - let mut hints = SizingHints { - fixed_size: true, - can_grow: false, - }; + let mut hints = SizingHints::default(); for &child in children.iter() { let entity = world.entity(child).expect("invalid child"); diff --git a/violet-core/src/state/map.rs b/violet-core/src/state/map.rs index 9dd5e0c..5dd1b7d 100644 --- a/violet-core/src/state/map.rs +++ b/violet-core/src/state/map.rs @@ -11,6 +11,7 @@ use super::{State, StateOwned, StateSink, StateStream}; /// /// However, as this does not assume the derived state is contained withing the original state is /// does not allow in-place mutation. + pub struct Map { inner: C, conv_to: Arc, @@ -18,6 +19,21 @@ pub struct Map { _marker: PhantomData, } +impl Clone for Map +where + C: Clone, + G: Clone, +{ + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + conv_to: self.conv_to.clone(), + conv_from: self.conv_from.clone(), + _marker: PhantomData, + } + } +} + impl State for Map { type Item = U; } diff --git a/violet-core/src/state/mod.rs b/violet-core/src/state/mod.rs index 1f5999c..accce60 100644 --- a/violet-core/src/state/mod.rs +++ b/violet-core/src/state/mod.rs @@ -22,6 +22,8 @@ pub trait State { type Item; /// Map a state from one type to another through reference projection + /// + /// This an be used to target a specific field of a struct or item in an array to transform. fn map_ref &U, G: Fn(&mut Self::Item) -> &mut U, U>( self, f: F, @@ -122,8 +124,18 @@ pub trait StateSink: State { } /// Allows sending and receiving a value to a state +/// +/// +/// This is the most common form of state and is used for both reading state updates, and sending +/// new state. +/// +/// Notably, this does not allow to directly read the state, as it may not always be available due +/// to filtered states. Instead, you can subscribe to changes and use [`WatchState`] to hold on to +/// the latest known state. pub trait StateDuplex: StateStream + StateSink {} +pub type DynStateDuplex = Box>; + impl StateDuplex for T where T: StateStream + StateSink {} impl State for Mutable { diff --git a/violet-core/src/style/mod.rs b/violet-core/src/style/mod.rs index 3db7190..c2d2925 100644 --- a/violet-core/src/style/mod.rs +++ b/violet-core/src/style/mod.rs @@ -15,8 +15,8 @@ use crate::{ }; use self::colors::{ - EERIE_BLACK_600, EERIE_BLACK_700, EERIE_BLACK_DEFAULT, JADE_400, JADE_600, JADE_DEFAULT, - LION_DEFAULT, PLATINUM_DEFAULT, REDWOOD_DEFAULT, + EERIE_BLACK_300, EERIE_BLACK_600, EERIE_BLACK_700, EERIE_BLACK_DEFAULT, JADE_400, JADE_600, + JADE_DEFAULT, LION_DEFAULT, PLATINUM_DEFAULT, REDWOOD_DEFAULT, }; #[macro_export] @@ -258,7 +258,7 @@ pub fn setup_stylesheet() -> EntityBuilder { // colors .set(primary_background(), EERIE_BLACK_DEFAULT) .set(primary_item(), PLATINUM_DEFAULT) - .set(secondary_background(), EERIE_BLACK_600) + .set(secondary_background(), EERIE_BLACK_300) .set(accent_background(), EERIE_BLACK_DEFAULT) .set(accent_item(), JADE_DEFAULT) .set(success_background(), EERIE_BLACK_DEFAULT) diff --git a/violet-core/src/systems.rs b/violet-core/src/systems.rs index 4526a90..b7d20b4 100644 --- a/violet-core/src/systems.rs +++ b/violet-core/src/systems.rs @@ -16,6 +16,7 @@ use flax::{ QueryBorrow, System, World, }; use glam::Vec2; +use tracing::info; use crate::{ components::{ @@ -180,7 +181,6 @@ pub fn layout_system() -> BoxedSystem { /// Updates the apparent screen position of entities based on the hierarchy pub fn transform_system() -> BoxedSystem { - puffin::profile_function!(); System::builder() .with_query( Query::new(( @@ -192,7 +192,6 @@ pub fn transform_system() -> BoxedSystem { .with_strategy(Dfs::new(child_of)), ) .build(|mut query: DfsBorrow<_>| { - puffin::profile_scope!("transform_system"); query.traverse( &Vec2::ZERO, |(pos, screen_rect, rect, local_pos): (&mut Vec2, &mut Rect, &Rect, &Vec2), diff --git a/violet-core/src/types.rs b/violet-core/src/types.rs index cf38e93..ef8e201 100644 --- a/violet-core/src/types.rs +++ b/violet-core/src/types.rs @@ -131,6 +131,12 @@ pub struct Rect { pub max: Vec2, } +impl Display for Rect { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("({:?},{:?})", self.min, self.max)) + } +} + impl Rect { pub const ZERO: Self = Self { min: Vec2::ZERO, diff --git a/violet-core/src/unit.rs b/violet-core/src/unit.rs index 3b643f4..9fdc0e5 100644 --- a/violet-core/src/unit.rs +++ b/violet-core/src/unit.rs @@ -3,7 +3,7 @@ use std::{ ops::{Add, AddAssign, Mul, MulAssign, Sub, SubAssign}, }; -use glam::{IVec2, Vec2}; +use glam::{BVec2, IVec2, Vec2}; /// Represents a unit of measurement #[derive(Debug, Clone, Copy, PartialEq)] @@ -37,13 +37,6 @@ impl Unit { pub fn rel(rel: T) -> Self { Self { px: T::ZERO, rel } } - - pub(crate) fn is_fixed(&self) -> bool - where - T: PartialEq, - { - self.rel == T::ZERO - } } impl Unit { @@ -60,6 +53,10 @@ impl Unit { rel: Vec2::new(x, y), } } + + pub(crate) fn is_relative(&self) -> BVec2 { + self.rel.cmpgt(Vec2::ZERO) + } } impl Unit { diff --git a/violet-core/src/widget/container.rs b/violet-core/src/widget/container.rs index e31e859..7ef8743 100644 --- a/violet-core/src/widget/container.rs +++ b/violet-core/src/widget/container.rs @@ -8,7 +8,8 @@ use crate::{ layout::{Alignment, Direction, FlowLayout, Layout, StackLayout}, style::{ colors::{EERIE_BLACK_300, EERIE_BLACK_400}, - Background, SizeExt, StyleExt, WidgetSize, + primary_background, secondary_background, spacing_medium, spacing_small, Background, + SizeExt, StyleExt, WidgetSize, }, unit::Unit, Edges, Frame, Scope, Widget, WidgetCollection, @@ -246,15 +247,15 @@ pub fn centered(widget: W) -> Stack { pub fn card(widget: W) -> Stack { Stack::new(widget) // TODO: semantic color and sizing increment - .with_background(Background::new(EERIE_BLACK_400)) - .with_padding(Edges::even(4.0)) - .with_margin(Edges::even(4.0)) + .with_background(Background::new(secondary_background())) + .with_padding(spacing_medium()) + .with_margin(spacing_medium()) } -pub fn card2(widget: W) -> Stack { +pub fn pill(widget: W) -> Stack { Stack::new(widget) // TODO: semantic color and sizing increment - .with_background(Background::new(EERIE_BLACK_300)) - .with_padding(Edges::even(4.0)) - .with_margin(Edges::even(4.0)) + .with_background(Background::new(primary_background())) + .with_padding(spacing_small()) + .with_margin(spacing_small()) } diff --git a/violet-core/src/widget/interactive/input.rs b/violet-core/src/widget/interactive/input.rs index d071d37..66ad76c 100644 --- a/violet-core/src/widget/interactive/input.rs +++ b/violet-core/src/widget/interactive/input.rs @@ -15,9 +15,9 @@ use crate::{ components::{self, screen_rect}, editor::{CursorMove, EditAction, EditorAction, TextEditor}, input::{focus_sticky, focusable, on_focus, on_keyboard_input, on_mouse_input, KeyboardInput}, - state::{State, StateDuplex, StateSink, StateStream}, + state::{StateDuplex, StateSink, StateStream}, style::{ - colors::EERIE_BLACK_300, interactive_active, spacing_small, Background, SizeExt, StyleExt, + interactive_active, interactive_inactive, spacing_small, Background, SizeExt, StyleExt, ValueOrRef, WidgetSize, }, text::{LayoutGlyphs, TextSegment}, @@ -39,7 +39,7 @@ impl Default for TextInputStyle { fn default() -> Self { Self { cursor_color: interactive_active().into(), - background: Background::new(EERIE_BLACK_300), + background: Background::new(interactive_inactive()), font_size: 16.0, } } diff --git a/violet-demo/src/lib.rs b/violet-demo/src/lib.rs index e76c6c3..16c2a51 100644 --- a/violet-demo/src/lib.rs +++ b/violet-demo/src/lib.rs @@ -9,30 +9,22 @@ use tracing_subscriber::{ use tracing_web::{performance_layer, MakeWebConsoleWriter}; use violet::{ core::{ - components, - layout::{Alignment, Direction}, - state::{Map, MapRef, State, StateMut, StateStream, StateStreamRef}, - style::{ - colors::{ - EERIE_BLACK_400, EERIE_BLACK_DEFAULT, JADE_200, JADE_DEFAULT, LION_DEFAULT, - REDWOOD_DEFAULT, - }, - danger_item, success_item, Background, SizeExt, StyleExt, ValueOrRef, - }, - text::Wrap, + layout::Alignment, + state::{DynStateDuplex, State, StateMut, StateStream, StateStreamRef}, + style::{primary_background, Background, SizeExt, ValueOrRef}, to_owned, unit::Unit, - utils::{zip_latest, zip_latest_ref}, + utils::zip_latest_ref, widget::{ - card, column, label, row, Button, ButtonStyle, Checkbox, List, Rectangle, SignalWidget, - SliderWithLabel, Stack, StreamWidget, Text, WidgetExt, + card, column, label, pill, row, Button, Checkbox, Rectangle, SliderWithLabel, Stack, + StreamWidget, Text, TextInput, WidgetExt, }, - Edges, Scope, Widget, WidgetCollection, + Edges, Scope, Widget, }, - flax::components::name, - futures_signals::signal::{Mutable, SignalExt}, + futures_signals::signal::Mutable, glam::vec2, - palette::{FromColor, IntoColor, Oklch, Srgb, Srgba}, + palette::{FromColor, IntoColor, OklabHue, Oklch, Srgb}, + wgpu::renderer::RendererConfig, }; use wasm_bindgen::prelude::*; @@ -72,18 +64,33 @@ fn setup() { pub fn run() { setup(); - violet::wgpu::App::new().run(MainApp).unwrap(); + violet::wgpu::App::new() + .with_renderer_config(RendererConfig { debug_mode: true }) + .run(MainApp) + .unwrap(); } struct MainApp; +const DEFAULT_FALLOFF: f32 = 15.0; + impl Widget for MainApp { fn mount(self, scope: &mut Scope<'_>) { - let palette_item = Mutable::new(Vec::new()); + let palette_item = Mutable::new( + (0..8) + .map(|i| { + Mutable::new(PaletteColor { + color: Oklch::new(0.5, 0.27, (i as f32 * 60.0) % 360.0), + falloff: DEFAULT_FALLOFF, + }) + }) + .collect(), + ); - card((Palettes::new(palette_item),)) - .with_margin(Edges::even(4.0)) + column((Palettes::new(palette_item),)) .with_size(Unit::rel2(1.0, 1.0)) + .with_background(Background::new(primary_background())) + .contain_margins(true) .mount(scope); } } @@ -113,12 +120,12 @@ impl Widget for Tints { ..self.base }; - Stack::new(column(( - Rectangle::new(ValueOrRef::value(color.into_color())) - .with_min_size(Unit::px2(60.0, 60.0)), - // Text::new(format!("{:.2}", f)), - ))) + Stack::new(column((Rectangle::new(ValueOrRef::value( + color.into_color(), + )) + .with_size(Unit::px2(80.0, 60.0)),))) .with_margin(Edges::even(4.0)) + .with_name("Tint") }) .collect_vec()) .mount(scope) @@ -155,34 +162,40 @@ impl Widget for Palettes { .danger() }; - let current_choice = Mutable::new(None as Option); + 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(OklchEditor::new), + |items, i: &Option| { + i.and_then(|i| items.get(i).cloned()) + .map(PaletteEditor::new) + }, ); - let palettes = StreamWidget(self.items.stream_ref(move |items| { - let items = items - .iter() - .enumerate() - .map({ - to_owned![current_choice]; - let discard = &discard; - move |(i, item)| { - let checkbox = Checkbox::new( - current_choice - .clone() - .map(move |v| v == Some(i), move |state| state.then_some(i)), - ); - - card(row((checkbox, discard(i), StreamWidget(item.stream())))) - } - }) - .collect_vec(); - - column(items) + let palettes = StreamWidget(self.items.stream_ref({ + to_owned![current_choice]; + move |items| { + let items = items + .iter() + .enumerate() + .map({ + to_owned![current_choice]; + let discard = &discard; + move |(i, item)| { + let checkbox = Checkbox::new( + current_choice + .clone() + .map(move |v| v == Some(i), move |state| state.then_some(i)), + ); + + card(row((checkbox, discard(i), StreamWidget(item.stream())))) + } + }) + .collect_vec(); + + column(items) + } })); let items = self.items.clone(); @@ -191,7 +204,13 @@ impl Widget for Palettes { StreamWidget(editor), palettes, Button::label("+").on_press(move |_, _| { - items.write_mut(|v| v.push(Mutable::new(PaletteColor::default()))) + 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, + })); + current_choice.set(Some(v.len() - 1)); + }) }), )) .mount(scope) @@ -200,63 +219,51 @@ impl Widget for Palettes { #[derive(Debug, Clone, Copy)] pub struct PaletteColor { - color: Vec3, + color: Oklch, falloff: f32, } -impl Default for PaletteColor { - fn default() -> Self { - Self { - color: Vec3::new(0.5, 0.27, 153.0), - falloff: 10.0, - } - } -} - impl Widget for PaletteColor { fn mount(self, scope: &mut Scope<'_>) { - let oklch_color = Oklch::new(self.color.x, self.color.y, self.color.z); - column(( - row((Tints::new(oklch_color, self.falloff),)), - label(color_hex(oklch_color)), + Stack::new(( + row((Tints::new(self.color, self.falloff),)), + pill(label(color_hex(self.color))), )) - .with_cross_align(Alignment::Center) + .with_vertical_alignment(Alignment::End) + .with_horizontal_alignment(Alignment::Center) .mount(scope) } } -pub struct OklchEditor { +pub struct PaletteEditor { color: Mutable, } -impl OklchEditor { +impl PaletteEditor { pub fn new(color: Mutable) -> Self { Self { color } } } -impl Widget for OklchEditor { +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 color_oklch = Map::new( - color.clone(), - |v| Oklch::new(v.x, v.y, v.z), - |v| Vec3::new(v.l, v.chroma, v.hue.into_positive_degrees()), - ); - - let lightness = color.clone().map_ref(|v| &v.x, |v| &mut v.x); - let chroma = color.clone().map_ref(|v| &v.y, |v| &mut v.y); - let hue = color.clone().map_ref(|v| &v.z, |v| &mut v.z); + 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| { - let color = Oklch::new(v.x, v.y, v.z).into_color(); - Rectangle::new(ValueOrRef::value(color)) + Rectangle::new(ValueOrRef::value(v.into_color())) .with_size(Unit::new(vec2(0.0, 100.0), vec2(1.0, 0.0))) + .with_name("ColorPreview") }); - column(( + card(column(( row(( Text::new("Lightness"), SliderWithLabel::new(lightness, 0.0, 1.0) @@ -275,14 +282,9 @@ impl Widget for OklchEditor { .editable(true) .round(1.0), )), - StreamWidget(color.stream_ref(|v| { - let hex: Srgb = Srgb::from_color(Oklch::new(v.x, v.y, v.z)).into_format(); - Text::new(format!( - "#{:0>2x}{:0>2x}{:0>2x}", - hex.red, hex.green, hex.blue - )) - })), - StreamWidget(color.stream().map(|v| Text::new(format!("{}", v)))), + ColorHexEditor { + color: Box::new(color.clone()), + }, StreamWidget(color_rect), row(( Text::new("Chroma falloff"), @@ -290,7 +292,28 @@ impl Widget for OklchEditor { .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) + } +} diff --git a/violet-wgpu/src/renderer/debug_renderer.rs b/violet-wgpu/src/renderer/debug_renderer.rs index 0619e5f..81653f3 100644 --- a/violet-wgpu/src/renderer/debug_renderer.rs +++ b/violet-wgpu/src/renderer/debug_renderer.rs @@ -213,11 +213,12 @@ impl DebugRenderer { self.objects.clear(); self.objects.extend(objects); - self.layout_changes.retain(|_, lifetime| { - *lifetime -= 1; + self.layout_changes.clear(); + // self.layout_changes.retain(|_, lifetime| { + // *lifetime -= 1; - *lifetime > 0 - }); + // *lifetime > 0 + // }); } pub fn draw_commands(&self) -> &[(DrawCommand, ObjectData)] { diff --git a/violet-wgpu/src/renderer/window_renderer.rs b/violet-wgpu/src/renderer/window_renderer.rs index cff3a4d..3b2fc71 100644 --- a/violet-wgpu/src/renderer/window_renderer.rs +++ b/violet-wgpu/src/renderer/window_renderer.rs @@ -4,6 +4,7 @@ use anyhow::Context; use flax::Entity; use glam::Mat4; use parking_lot::Mutex; +use puffin::profile_scope; use wgpu::{Operations, RenderPassDescriptor, StoreOp, SurfaceError}; use winit::dpi::PhysicalSize; @@ -110,8 +111,11 @@ impl WindowRenderer { .context("Failed to draw shapes")?; } - self.ctx.gpu.queue.submit([encoder.finish()]); - target.present(); + { + profile_scope!("submit"); + self.ctx.gpu.queue.submit([encoder.finish()]); + target.present(); + } Ok(()) } diff --git a/violet-wgpu/src/text.rs b/violet-wgpu/src/text.rs index 1fabb36..03e2bf8 100644 --- a/violet-wgpu/src/text.rs +++ b/violet-wgpu/src/text.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use cosmic_text::{ fontdb::Source, Attrs, Buffer, FontSystem, LayoutGlyph, Metrics, Shaping, SwashCache, }; -use glam::{vec2, Vec2}; +use glam::{vec2, BVec2, Vec2}; use itertools::Itertools; use palette::Srgba; use parking_lot::Mutex; @@ -73,8 +73,8 @@ impl SizeResolver for TextSizeResolver { let line_height = state.buffer.metrics().line_height; - // If preferred is clamped, so is min - let (min, _clamped) = Self::resolve_text_size( + // If preferred is can_grow, so is min + let (min, _can_grow) = Self::resolve_text_size( state, text_system, font_size, @@ -84,7 +84,7 @@ impl SizeResolver for TextSizeResolver { }, ); - let (preferred, clamped) = Self::resolve_text_size( + let (preferred, can_grow) = Self::resolve_text_size( state, text_system, font_size, @@ -99,8 +99,8 @@ impl SizeResolver for TextSizeResolver { min, preferred, SizingHints { - can_grow: clamped, - fixed_size: true, + can_grow, + relative_size: BVec2::TRUE, }, ) } @@ -110,7 +110,7 @@ impl SizeResolver for TextSizeResolver { entity: &flax::EntityRef, content_area: Vec2, limits: LayoutLimits, - ) -> (Vec2, bool) { + ) -> (Vec2, BVec2) { puffin::profile_scope!("TextSizeResolver::apply"); let _span = tracing::debug_span!("TextSizeResolver::apply", ?content_area).entered(); @@ -122,7 +122,7 @@ impl SizeResolver for TextSizeResolver { let text_system = &mut *self.text_system.lock(); let line_height = state.buffer.metrics().line_height; - let (size, clamped) = Self::resolve_text_size( + let (size, can_grow) = Self::resolve_text_size( state, text_system, font_size, @@ -135,7 +135,7 @@ impl SizeResolver for TextSizeResolver { // tracing::error!(%entity, text=?state.text(), %size, %limits.max_size, "Text overflowed"); } - (size, clamped) + (size, can_grow) } } @@ -149,7 +149,7 @@ impl TextSizeResolver { text_system: &mut TextSystem, font_size: f32, size: Vec2, - ) -> (Vec2, bool) { + ) -> (Vec2, BVec2) { // let _span = tracing::debug_span!("resolve_text_size", font_size, ?text, ?limits).entered(); let mut buffer = state.buffer.borrow_with(&mut text_system.font_system); @@ -168,7 +168,7 @@ fn glyph_bounds(glyph: &LayoutGlyph) -> (f32, f32) { (glyph.x, glyph.x + glyph.w) } -fn measure(buffer: &Buffer) -> (Vec2, bool) { +fn measure(buffer: &Buffer) -> (Vec2, BVec2) { let (width, total_lines) = buffer .layout_runs() @@ -193,7 +193,7 @@ fn measure(buffer: &Buffer) -> (Vec2, bool) { ( vec2(width, total_lines as f32 * buffer.metrics().line_height), - total_lines > buffer.lines.len(), + BVec2::new(total_lines > buffer.lines.len(), false), ) } From 8865ddb7e20561628a3926e1c985b43a89d5f93e Mon Sep 17 00:00:00 2001 From: Freja Roberts Date: Fri, 15 Mar 2024 00:02:26 +0100 Subject: [PATCH 04/16] chore: move query args to struct --- violet-core/src/constraints.rs | 16 ++--- violet-core/src/layout/cache.rs | 6 +- violet-core/src/layout/flow.rs | 96 +++++++++++++++++++---------- violet-core/src/layout/mod.rs | 106 +++++++++++++------------------- violet-core/src/layout/stack.rs | 36 +++++------ violet-wgpu/src/text.rs | 27 ++++---- 6 files changed, 146 insertions(+), 141 deletions(-) diff --git a/violet-core/src/constraints.rs b/violet-core/src/constraints.rs index d48c845..2c7d96c 100644 --- a/violet-core/src/constraints.rs +++ b/violet-core/src/constraints.rs @@ -1,6 +1,6 @@ use glam::{vec2, BVec2, Vec2}; -use crate::layout::{Direction, LayoutLimits, SizeResolver, SizingHints}; +use crate::layout::{Direction, QueryArgs, SizeResolver, SizingHints}; pub struct FixedAreaConstraint { pub area: f32, @@ -24,16 +24,12 @@ impl SizeResolver for FixedAreaConstraint { // } // } - fn query( - &mut self, - _: &flax::EntityRef, - _content_area: Vec2, - limits: LayoutLimits, - squeeze: Direction, - ) -> (Vec2, Vec2, SizingHints) { - let size = (limits.max_size / self.unit_size).floor().max(Vec2::ONE); + fn query(&mut self, _: &flax::EntityRef, args: QueryArgs) -> (Vec2, Vec2, SizingHints) { + let size = (args.limits.max_size / self.unit_size) + .floor() + .max(Vec2::ONE); - let min = match squeeze { + let min = match args.direction { Direction::Horizontal => vec2((self.area / size.y).ceil(), size.y), Direction::Vertical => vec2(size.x, (self.area / size.x).ceil()), }; diff --git a/violet-core/src/layout/cache.rs b/violet-core/src/layout/cache.rs index 9d45c0a..5f6d10d 100644 --- a/violet-core/src/layout/cache.rs +++ b/violet-core/src/layout/cache.rs @@ -68,7 +68,7 @@ impl LayoutCache { pub(crate) fn insert_query_row(&mut self, value: CachedValue) { self.query_row = Some(value); if let Some(f) = self.on_invalidated.as_ref() { - f(LayoutUpdate::SizeQueryUpdate) + // f(LayoutUpdate::SizeQueryUpdate) } } @@ -83,8 +83,8 @@ impl LayoutCache { self.layout.as_ref() } - pub fn query(&self) -> &[Option>; 2] { - &self.query + pub(crate) fn get_query(&self, direction: Direction) -> Option<&CachedValue> { + self.query[direction as usize].as_ref() } } diff --git a/violet-core/src/layout/flow.rs b/violet-core/src/layout/flow.rs index e14e515..9b31204 100644 --- a/violet-core/src/layout/flow.rs +++ b/violet-core/src/layout/flow.rs @@ -14,7 +14,8 @@ use crate::{ }; use super::{ - cache::LayoutCache, resolve_pos, update_subtree, Block, Direction, LayoutLimits, Sizing, + cache::LayoutCache, resolve_pos, update_subtree, Block, Direction, LayoutLimits, QueryArgs, + Sizing, }; #[derive(Debug, Clone)] @@ -193,7 +194,16 @@ impl FlowLayout { // Query the minimum and preferred size of this flow layout, optimizing for minimum size in // the direction of this axis. - let row = self.query_row(world, cache, children, content_area, limits); + let row = self.query_row( + world, + cache, + children, + QueryArgs { + limits, + content_area: content_area.size(), + direction: self.direction, + }, + ); // tracing::info!(?row.margin, "row margins to be contained"); self.distribute_children(world, entity, &row, content_area, limits, preferred_size) @@ -405,9 +415,7 @@ impl FlowLayout { &self, world: &World, row: &Row, - content_area: Rect, - limits: LayoutLimits, - direction: Direction, + args: QueryArgs, preferred_size: Vec2, ) -> Sizing { puffin::profile_function!(); @@ -441,7 +449,7 @@ impl FlowLayout { // Clipped maximum that we remap to let target_inner_size = distribute_size - .min(limits.max_size.dot(axis) - minimum_inner_size) + .min(args.limits.max_size.dot(axis) - minimum_inner_size) .max(0.0); tracing::debug!( @@ -452,12 +460,10 @@ impl FlowLayout { "distribute" ); - let available_size = limits.max_size; + let available_size = args.limits.max_size; - let mut min_cursor = - MarginCursor::new(content_area.min, axis, cross_axis, self.contain_margins); - let mut cursor = - MarginCursor::new(content_area.min, axis, cross_axis, self.contain_margins); + let mut min_cursor = MarginCursor::new(Vec2::ZERO, axis, cross_axis, self.contain_margins); + let mut cursor = MarginCursor::new(Vec2::ZERO, axis, cross_axis, self.contain_margins); let mut sum = 0.0; @@ -537,7 +543,12 @@ impl FlowLayout { // NOTE: optimize for the minimum size in the query direction, not the // direction of the flow - let sizing = query_size(world, &entity, content_area.size(), child_limits, direction); + let sizing = query_size(world, &entity, QueryArgs { + limits: child_limits, + content_area: args.content_area, + // Use the query direction, not the flow direction + direction: args.direction, + }); hints = hints.combine(sizing.hints); @@ -557,8 +568,8 @@ impl FlowLayout { .to_edges(cursor.main_margin, cursor.cross_margin, self.reverse); Sizing { - min: min_rect.max_size(limits.min_size), - preferred: rect.max_size(limits.min_size), + min: min_rect.max_size(args.limits.min_size), + preferred: rect.max_size(args.limits.min_size), margin, hints, } @@ -569,12 +580,11 @@ impl FlowLayout { world: &World, cache: &mut LayoutCache, children: &[Entity], - content_area: Rect, - limits: LayoutLimits, + args: QueryArgs, ) -> Row { puffin::profile_function!(); if let Some(value) = cache.query_row.as_ref() { - if validate_cached_row(value, limits, content_area.size()) { + if validate_cached_row(value, args.limits, args.content_area) { return value.value.clone(); } } @@ -601,7 +611,19 @@ impl FlowLayout { let entity = world.entity(child).expect("Invalid child"); let child_margin = if self.contain_margins { - query_size(world, &entity, content_area.size(), limits, self.direction).margin + query_size( + world, + &entity, + QueryArgs { + limits: LayoutLimits { + min_size: Vec2::ZERO, + max_size: args.limits.max_size, + }, + content_area: args.content_area, + direction: self.direction, + }, + ) + .margin } else { Edges::ZERO }; @@ -609,13 +631,14 @@ impl FlowLayout { let sizing = query_size( world, &entity, - content_area.size(), - LayoutLimits { - min_size: Vec2::ZERO, - // max_size: limits.max_size, - max_size: limits.max_size - child_margin.size(), + QueryArgs { + limits: LayoutLimits { + min_size: Vec2::ZERO, + max_size: args.limits.max_size - child_margin.size(), + }, + content_area: args.content_area, + direction: self.direction, }, - self.direction, ); hints = hints.combine(sizing.hints); @@ -649,7 +672,11 @@ impl FlowLayout { hints, }; - cache.insert_query_row(CachedValue::new(limits, content_area.size(), row.clone())); + cache.insert_query_row(CachedValue::new( + args.limits, + args.content_area, + row.clone(), + )); row } @@ -658,12 +685,10 @@ impl FlowLayout { world: &World, cache: &mut LayoutCache, children: &[Entity], - content_area: Rect, - limits: LayoutLimits, - direction: Direction, + args: QueryArgs, preferred_size: Vec2, ) -> Sizing { - puffin::profile_function!(format!("{direction:?}")); + puffin::profile_function!(format!("{args:?}")); // We want to query the min/preferred size in the direction orthogonal to the flows // layout @@ -695,10 +720,17 @@ impl FlowLayout { // exist a better solution where some widgets may get slightly more space and still // fall within the max height. If anybody comes across a non-iterative solution for // this, be sure to let me know :) - let row = self.query_row(world, cache, children, content_area, limits); + let row = self.query_row( + world, + cache, + children, + QueryArgs { + direction: self.direction, + ..args + }, + ); - let sizing = - self.distribute_query(world, &row, content_area, limits, direction, preferred_size); + let sizing = self.distribute_query(world, &row, args, preferred_size); tracing::debug!(?self.direction, ?sizing, "query"); sizing } diff --git a/violet-core/src/layout/mod.rs b/violet-core/src/layout/mod.rs index d1d567d..8d1b724 100644 --- a/violet-core/src/layout/mod.rs +++ b/violet-core/src/layout/mod.rs @@ -12,7 +12,7 @@ use crate::{ self, anchor, aspect_ratio, children, layout, max_size, min_size, offset, padding, size, size_resolver, }, - layout::cache::{validate_cached_layout, validate_cached_query, CachedValue, LAYOUT_TOLERANCE}, + layout::cache::{validate_cached_layout, validate_cached_query, CachedValue}, Edges, Rect, }; @@ -24,8 +24,8 @@ use self::cache::{layout_cache, LayoutCache}; #[derive(Default, Debug, Clone, Copy, PartialEq, PartialOrd, Hash, Ord, Eq)] pub enum Direction { #[default] - Horizontal, - Vertical, + Horizontal = 0, + Vertical = 1, } impl Direction { @@ -98,34 +98,21 @@ impl Layout { world: &World, cache: &mut LayoutCache, children: &[Entity], - inner_rect: Rect, - limits: LayoutLimits, - squeeze: Direction, + args: QueryArgs, preferred_size: Vec2, ) -> Sizing { match self { - Layout::Stack(v) => { - v.query_size(world, children, inner_rect, limits, squeeze, preferred_size) - } - Layout::Flow(v) => v.query_size( - world, - cache, - children, - inner_rect, - limits, - squeeze, - preferred_size, - ), + Layout::Stack(v) => v.query_size(world, children, args, preferred_size), + Layout::Flow(v) => v.query_size(world, cache, children, args, preferred_size), } } } -#[derive(Debug, Clone)] -pub(crate) struct QueryArgs { - limits: LayoutLimits, - content_area: Vec2, - distribute: bool, - direction: Direction, +#[derive(Debug, Clone, Copy)] +pub struct QueryArgs { + pub limits: LayoutLimits, + pub content_area: Vec2, + pub direction: Direction, } #[derive(Debug, Clone, Copy)] @@ -274,18 +261,12 @@ fn validate_block(entity: &EntityRef, block: &Block, limits: LayoutLimits) { } } -pub(crate) fn query_size( - world: &World, - entity: &EntityRef, - content_area: Vec2, - mut limits: LayoutLimits, - direction: Direction, -) -> Sizing { - puffin::profile_function!(format!("{entity}")); +pub(crate) fn query_size(world: &World, entity: &EntityRef, mut args: QueryArgs) -> Sizing { + puffin::profile_function!(format!("{entity} {args:?}")); // assert!(limits.min_size.x <= limits.max_size.x); // assert!(limits.min_size.y <= limits.max_size.y); let _span = - tracing::debug_span!("query_size", name=entity.name().as_deref(), ?limits, %content_area) + tracing::debug_span!("query_size", name=entity.name().as_deref(), ?args.limits, %args.content_area) .entered(); // tracing::info!(name=entity.name().as_deref(), ?limits, %content_area, ?direction, "query_size"); @@ -308,22 +289,24 @@ pub(crate) fn query_size( let fixed_boundary_size = min_size.is_relative() | max_size.map(|v| v.is_relative()).unwrap_or(BVec2::FALSE); - let min_size = min_size.resolve(content_area); - let max_size = max_size.map(|v| v.resolve(content_area)); - limits.min_size = limits.min_size.max(min_size); + let min_size = min_size.resolve(args.content_area); + let max_size = max_size.map(|v| v.resolve(args.content_area)); + args.limits.min_size = args.limits.min_size.max(min_size); - let external_max_size = limits.max_size; + let external_max_size = args.limits.max_size; + + let external_max_size = args.limits.max_size; // Minimum size is *always* respected, even if that entails overflowing - limits.max_size = limits.max_size.max(limits.min_size); + args.limits.max_size = args.limits.max_size.max(args.limits.min_size); if let Some(max_size) = max_size { - limits.max_size = limits.max_size.min(max_size); + args.limits.max_size = args.limits.max_size.min(max_size); } // Check if cache is valid - if let Some(cache) = &cache.query[direction as usize] { - if validate_cached_query(cache, limits, content_area) { + if let Some(cache) = cache.get_query(args.direction) { + if validate_cached_query(cache, args.limits, args.content_area) { return cache.value; } } @@ -335,7 +318,7 @@ pub(crate) fn query_size( let children = children.map(Vec::as_slice).unwrap_or(&[]); - let resolved_size = size.resolve(content_area); + let resolved_size = size.resolve(args.content_area); let hints = SizingHints { relative_size: fixed_boundary_size | size.is_relative(), can_grow: BVec2::new( @@ -349,7 +332,7 @@ pub(crate) fn query_size( // } // Clamp max size here since we ensure it is > min_size - let resolved_size = resolved_size.clamp(limits.min_size, limits.max_size); + let resolved_size = resolved_size.clamp(args.limits.min_size, args.limits.max_size); // Flow let mut sizing = if let Some(layout) = layout { @@ -357,12 +340,14 @@ pub(crate) fn query_size( world, cache, children, - Rect::from_size(content_area).inset(&padding), - LayoutLimits { - min_size: (limits.min_size - padding.size()).max(Vec2::ZERO), - max_size: (limits.max_size - padding.size()).max(Vec2::ZERO), + QueryArgs { + limits: LayoutLimits { + min_size: (args.limits.min_size - padding.size()).max(Vec2::ZERO), + max_size: (args.limits.max_size - padding.size()).max(Vec2::ZERO), + }, + content_area: args.content_area - padding.size(), + ..args }, - direction, resolved_size - padding.size(), ); @@ -374,18 +359,18 @@ pub(crate) fn query_size( } } else if let [child] = children { let child = world.entity(*child).unwrap(); - query_size(world, &child, content_area, limits, direction) + query_size(world, &child, args) } else { let (instrisic_min_size, intrinsic_size, intrinsic_hints) = size_resolver - .map(|v| v.query(entity, content_area, limits, direction)) + .map(|v| v.query(entity, args)) .unwrap_or((Vec2::ZERO, Vec2::ZERO, SizingHints::default())); // If intrinsic_min_size > max_size we overflow, but respect the minimum size nonetheless - limits.min_size = limits.min_size.max(instrisic_min_size); + args.limits.min_size = args.limits.min_size.max(instrisic_min_size); let size = intrinsic_size.max(resolved_size); - let min_size = instrisic_min_size.max(limits.min_size); + let min_size = instrisic_min_size.max(args.limits.min_size); Sizing { min: Rect::from_size(min_size), @@ -402,8 +387,8 @@ pub(crate) fn query_size( .preferred .with_size(constraints.apply(sizing.preferred.size())); - let min_offset = resolve_pos(entity, content_area, sizing.min.size()); - let offset = resolve_pos(entity, content_area, sizing.preferred.size()); + let min_offset = resolve_pos(entity, args.content_area, sizing.min.size()); + let offset = resolve_pos(entity, args.content_area, sizing.preferred.size()); sizing.min = sizing.min.translate(min_offset); sizing.preferred = sizing.preferred.translate(offset); @@ -421,7 +406,10 @@ pub(crate) fn query_size( // validate_sizing(entity, &sizing, limits); - cache.insert_query(direction, CachedValue::new(limits, content_area, sizing)); + cache.insert_query( + args.direction, + CachedValue::new(args.limits, args.content_area, sizing), + ); cache.relative_size = sizing.hints.relative_size; sizing @@ -595,13 +583,7 @@ pub trait SizeResolver: Send + Sync { /// /// Returns a minimum possible size optimized for the given direction, and the preferred /// size - fn query( - &mut self, - entity: &EntityRef, - content_area: Vec2, - limits: LayoutLimits, - direction: Direction, - ) -> (Vec2, Vec2, SizingHints); + fn query(&mut self, entity: &EntityRef, args: QueryArgs) -> (Vec2, Vec2, SizingHints); /// Uses the current constraints to determine the size of the widget fn apply( diff --git a/violet-core/src/layout/stack.rs b/violet-core/src/layout/stack.rs index 6a240c5..3fc8c74 100644 --- a/violet-core/src/layout/stack.rs +++ b/violet-core/src/layout/stack.rs @@ -8,7 +8,9 @@ use crate::{ Edges, Rect, }; -use super::{resolve_pos, update_subtree, Alignment, Block, Direction, LayoutLimits, Sizing}; +use super::{ + resolve_pos, update_subtree, Alignment, Block, Direction, LayoutLimits, QueryArgs, Sizing, +}; #[derive(Debug)] pub struct StackableBounds { @@ -173,13 +175,12 @@ impl StackLayout { &self, world: &World, children: &[Entity], - content_area: Rect, - limits: LayoutLimits, - squeeze: Direction, + args: QueryArgs, preferred_size: Vec2, ) -> Sizing { puffin::profile_function!(); - let min_rect = Rect::from_size_pos(limits.min_size, content_area.min); + let min_rect = Rect::from_size(args.limits.min_size); + let mut min_bounds = StackableBounds::from_rect(min_rect); let mut preferred_bounds = StackableBounds::from_rect(min_rect); @@ -191,32 +192,29 @@ impl StackLayout { let sizing = query_size( world, &entity, - content_area.size(), - LayoutLimits { - min_size: Vec2::ZERO, - max_size: limits.max_size, + QueryArgs { + limits: LayoutLimits { + min_size: Vec2::ZERO, + max_size: args.limits.max_size, + }, + content_area: args.content_area, + direction: args.direction, }, - squeeze, ); hints = hints.combine(sizing.hints); - min_bounds = min_bounds.merge(&StackableBounds::new( - sizing.min.translate(content_area.min), - sizing.margin, - )); + min_bounds = min_bounds.merge(&StackableBounds::new(sizing.min, sizing.margin)); - preferred_bounds = preferred_bounds.merge(&StackableBounds::new( - sizing.preferred.translate(content_area.min), - sizing.margin, - )); + preferred_bounds = + preferred_bounds.merge(&StackableBounds::new(sizing.preferred, sizing.margin)); } let min_margin = min_bounds.margin(); let preferred_margin = preferred_bounds.margin(); Sizing { - min: min_bounds.inner.max_size(limits.min_size), + min: min_bounds.inner.max_size(args.limits.min_size), // .clamp_size(limits.min_size, limits.max_size), preferred: preferred_bounds.inner.max_size(preferred_size), // .clamp_size(limits.min_size, limits.max_size), diff --git a/violet-wgpu/src/text.rs b/violet-wgpu/src/text.rs index 03e2bf8..9cde39a 100644 --- a/violet-wgpu/src/text.rs +++ b/violet-wgpu/src/text.rs @@ -10,7 +10,7 @@ use parking_lot::Mutex; use violet_core::{ components::font_size, - layout::{Direction, LayoutLimits, SizeResolver, SizingHints}, + layout::{Direction, LayoutLimits, QueryArgs, SizeResolver, SizingHints}, text::{LayoutGlyphs, LayoutLineGlyphs, TextSegment}, Rect, }; @@ -54,15 +54,9 @@ pub struct TextSizeResolver { } impl SizeResolver for TextSizeResolver { - fn query( - &mut self, - entity: &flax::EntityRef, - _content_area: Vec2, - limits: LayoutLimits, - direction: Direction, - ) -> (Vec2, Vec2, SizingHints) { + fn query(&mut self, entity: &flax::EntityRef, args: QueryArgs) -> (Vec2, Vec2, SizingHints) { puffin::profile_scope!("TextSizeResolver::query"); - let _span = tracing::debug_span!("TextSizeResolver::query", ?direction).entered(); + let _span = tracing::debug_span!("TextSizeResolver::query", ?args.direction).entered(); let query = (text_buffer_state().as_mut(), font_size()); @@ -78,9 +72,12 @@ impl SizeResolver for TextSizeResolver { state, text_system, font_size, - match direction { - Direction::Horizontal => vec2(1.0, limits.max_size.y.max(line_height)), - Direction::Vertical => vec2(limits.max_size.x, limits.max_size.y.max(line_height)), + match args.direction { + Direction::Horizontal => vec2(1.0, args.limits.max_size.y.max(line_height)), + Direction::Vertical => vec2( + args.limits.max_size.x, + args.limits.max_size.y.max(line_height), + ), }, ); @@ -88,12 +85,12 @@ impl SizeResolver for TextSizeResolver { state, text_system, font_size, - limits.max_size.max(vec2(1.0, line_height)), + args.limits.max_size.max(vec2(1.0, line_height)), ); // + vec2(5.0, 5.0); - if min.dot(direction.to_axis()) > preferred.dot(direction.to_axis()) { - tracing::error!(%entity, text=?state.text(), %min, %preferred, ?direction, %limits.max_size, "Text wrapping failed"); + if min.dot(args.direction.to_axis()) > preferred.dot(args.direction.to_axis()) { + tracing::error!(%entity, text=?state.text(), %min, %preferred, ?args.direction, %args.limits.max_size, "Text wrapping failed"); } ( min, From 40626c84ef997e07a54a62cab42a2808c08b75ad Mon Sep 17 00:00:00 2001 From: Freja Roberts Date: Fri, 15 Mar 2024 16:51:51 +0100 Subject: [PATCH 05/16] fix: improve performance for non-coupled size widgets --- Cargo.lock | 5 +- Cargo.toml | 3 +- assets/shaders/border_shader.wgsl | 69 ++++++++ examples/row.rs | 54 +++++-- violet-core/src/constraints.rs | 1 + violet-core/src/layout/cache.rs | 23 ++- violet-core/src/layout/flow.rs | 56 ++++++- violet-core/src/layout/mod.rs | 40 +++-- violet-core/src/layout/stack.rs | 4 +- violet-core/src/systems.rs | 52 +++--- violet-core/src/widget/basic.rs | 2 +- violet-core/src/widget/interactive/button.rs | 54 +++++++ violet-demo/src/lib.rs | 5 +- violet-wgpu/Cargo.toml | 1 + violet-wgpu/src/app.rs | 3 +- violet-wgpu/src/renderer/debug_renderer.rs | 158 +++++++++++-------- violet-wgpu/src/renderer/mod.rs | 18 +-- violet-wgpu/src/renderer/window_renderer.rs | 2 + violet-wgpu/src/text.rs | 45 ++++-- 19 files changed, 430 insertions(+), 165 deletions(-) create mode 100644 assets/shaders/border_shader.wgsl diff --git a/Cargo.lock b/Cargo.lock index 7f8132e..6764b7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2157,9 +2157,9 @@ dependencies = [ [[package]] name = "sync_wrapper" -version = "0.1.2" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +checksum = "384595c11a4e2969895cad5a8c4029115f5ab956a9e5ef4de79d11a426e5f20c" [[package]] name = "sys-locale" @@ -2578,6 +2578,7 @@ dependencies = [ "parking_lot", "puffin", "puffin_http", + "smallvec", "tracing", "violet-core", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index a0ac819..f867acc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,8 @@ slab = "0.4" tynm ="0.1" tokio = { version = "1.0", default-features = false, features = ["macros", "rt"] } arrayvec = "0.7" -sync_wrapper = "0.1" +sync_wrapper = "1.0" +smallvec = "1.0" bytemuck = { version = "1.13", features = ["derive"] } winit = "0.29" diff --git a/assets/shaders/border_shader.wgsl b/assets/shaders/border_shader.wgsl new file mode 100644 index 0000000..a6db66b --- /dev/null +++ b/assets/shaders/border_shader.wgsl @@ -0,0 +1,69 @@ +struct VertexInput { + @location(0) pos: vec3, + @location(1) color: vec4, + @location(2) tex_coord: vec2, + @builtin(instance_index) instance: u32, +} + +struct VertexOutput { + @builtin(position) pos: vec4, + @location(0) color: vec4, + @location(1) tex_coord: vec2, + @location(2) vertex_pos: vec3, + @location(3) scale: vec2, +} + +struct Object { + world_matrix: mat4x4, + color: vec4, +} + +struct Globals { + viewproj: mat4x4, +} + +@group(0) @binding(0) +var globals: Globals; + +@group(1) @binding(0) +var objects: array; + +@group(2) @binding(0) +var default_sampler: sampler; + +@group(2) @binding(1) +var fill_image: texture_2d; + +@vertex +fn vs_main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + let object = objects[in.instance]; + let scale = (object.world_matrix * vec4(1.0, 1.0, 0.0, 0.0)).xy; + out.pos = globals.viewproj * object.world_matrix * vec4(in.pos, 1.0); + out.color = object.color; + out.tex_coord = in.tex_coord; + out.vertex_pos = in.pos; + out.scale = scale; + + return out; +} + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + + let nearest_corner = vec2(select(0.0, in.scale.x, in.vertex_pos.x > 0.5), select(0.0, in.scale.y, in.vertex_pos.y > 0.5)); + let to_nearest = abs(in.vertex_pos.xy * in.scale - nearest_corner); + + // return vec4(length(nearest_corner - in.pos.xy) / 100.0); + let dist = max(1.0 - length(to_nearest) * 0.1, 0.0); + // return vec4(0.0, 0.0, dist, 1.0); + let border_size = 2.0; + + var border = 0.0; + + if to_nearest.x < border_size || to_nearest.y < border_size || to_nearest.x > in.scale.x - border_size || to_nearest.y > in.scale.y - border_size { + border = 1.0; + } + + return in.color * textureSample(fill_image, default_sampler, in.tex_coord) * border; +} diff --git a/examples/row.rs b/examples/row.rs index 400b479..3de8791 100644 --- a/examples/row.rs +++ b/examples/row.rs @@ -1,3 +1,6 @@ +use std::iter::repeat; + +use itertools::Itertools; use tracing_subscriber::{layer::SubscriberExt, registry, util::SubscriberInitExt, EnvFilter}; use tracing_tree::HierarchicalLayer; @@ -12,10 +15,10 @@ use violet::core::{ }; use violet_core::{ style::{ - colors::{JADE_400, JADE_DEFAULT, LION_DEFAULT}, - spacing_medium, SizeExt, + colors::{JADE_400, JADE_DEFAULT, LION_DEFAULT, REDWOOD_100, ULTRA_VIOLET_DEFAULT}, + spacing_medium, spacing_small, SizeExt, }, - widget::{column, row, Stack}, + widget::{card, column, label, row, Stack}, }; use violet_wgpu::renderer::RendererConfig; @@ -42,19 +45,28 @@ impl Widget for MainApp { fn mount(self, scope: &mut Scope<'_>) { Stack::new( column(( - row(( - column(( - Rectangle::new(JADE_DEFAULT).with_size(Unit::px2(900.0, 40.0)), - Rectangle::new(JADE_400) - .with_size(Unit::px2(900.0, 40.0)) - .with_min_size(Unit::px2(400.0, 40.0)), - )), - Rectangle::new(LION_DEFAULT).with_size(Unit::px2(900.0, 40.0)), - )), - Rectangle::new(REDWOOD_DEFAULT) - .with_min_size(Unit::px2(100.0, 100.0)) - .with_size(Unit::px2(0.0, 100.0) + Unit::rel2(1.0, 0.0)), + // row(( + // label("This text can wrap to save horizontal space"), + // card(( + // Rectangle::new(JADE_DEFAULT).with_size(Unit::px2(100.0, 40.0)), + // label("Jade"), + // )), + // label("This text can wrap to save horizontal space"), + // )), + // row(( + // column(( + // Rectangle::new(JADE_DEFAULT).with_size(Unit::px2(900.0, 40.0)), + // Rectangle::new(JADE_400) + // .with_size(Unit::px2(900.0, 40.0)) + // .with_min_size(Unit::px2(400.0, 40.0)), + // )), + // Rectangle::new(LION_DEFAULT).with_size(Unit::px2(900.0, 40.0)), + // )), + // Rectangle::new(REDWOOD_DEFAULT) + // .with_min_size(Unit::px2(100.0, 100.0)) + // .with_size(Unit::px2(0.0, 100.0) + Unit::rel2(1.0, 0.0)), // .with_margin(spacing_medium()), + row((0..16).map(|_| Stack::new(Item)).collect_vec()), )) // .with_padding(spacing_medium()) .contain_margins(true), @@ -63,3 +75,15 @@ impl Widget for MainApp { .mount(scope) } } + +#[derive(Debug, Clone)] +struct Item; + +impl Widget for Item { + fn mount(self, scope: &mut Scope<'_>) { + Rectangle::new(ULTRA_VIOLET_DEFAULT) + .with_size(Unit::px2(100.0, 100.0)) + .with_margin(spacing_small()) + .mount(scope) + } +} diff --git a/violet-core/src/constraints.rs b/violet-core/src/constraints.rs index 2c7d96c..de3e582 100644 --- a/violet-core/src/constraints.rs +++ b/violet-core/src/constraints.rs @@ -40,6 +40,7 @@ impl SizeResolver for FixedAreaConstraint { SizingHints { can_grow: BVec2::TRUE, relative_size: BVec2::TRUE, + coupled_size: true, }, ) } diff --git a/violet-core/src/layout/cache.rs b/violet-core/src/layout/cache.rs index 5f6d10d..ab2fa1b 100644 --- a/violet-core/src/layout/cache.rs +++ b/violet-core/src/layout/cache.rs @@ -1,7 +1,7 @@ use flax::{component, components::child_of, Entity, FetchExt, RelationExt, World}; use glam::{BVec2, Vec2}; -use super::{flow::Row, Block, Direction, LayoutLimits, Sizing}; +use super::{flow::Row, Block, Direction, LayoutLimits, Sizing, SizingHints}; #[derive(Debug)] pub struct CachedValue { @@ -10,7 +10,7 @@ pub struct CachedValue { pub value: T, } -pub const LAYOUT_TOLERANCE: f32 = 0.1; +pub const LAYOUT_TOLERANCE: f32 = 0.01; impl CachedValue { pub(crate) fn new(limits: LayoutLimits, content_area: Vec2, value: T) -> Self { @@ -34,7 +34,7 @@ pub struct LayoutCache { pub(crate) query_row: Option>, pub(crate) layout: Option>, on_invalidated: Option>, - pub(crate) relative_size: BVec2, + pub(crate) hints: SizingHints, } impl LayoutCache { @@ -44,7 +44,7 @@ impl LayoutCache { query_row: None, layout: None, on_invalidated, - relative_size: BVec2::TRUE, + hints: Default::default(), } } @@ -59,6 +59,7 @@ impl LayoutCache { } pub(crate) fn insert_query(&mut self, direction: Direction, value: CachedValue) { + self.hints = value.value.hints; self.query[direction as usize] = Some(value); if let Some(f) = self.on_invalidated.as_ref() { f(LayoutUpdate::SizeQueryUpdate) @@ -83,15 +84,19 @@ impl LayoutCache { self.layout.as_ref() } - pub(crate) fn get_query(&self, direction: Direction) -> Option<&CachedValue> { + pub fn get_query(&self, direction: Direction) -> Option<&CachedValue> { self.query[direction as usize].as_ref() } + + pub fn hints(&self) -> SizingHints { + self.hints + } } /// Invalidates a widgets layout cache along with its ancestors pub(crate) fn invalidate_widget(world: &World, id: Entity) { let entity = world.entity(id).unwrap(); - tracing::info!(%entity, "invalidating widget"); + // tracing::info!(%entity, "invalidating widget"); let query = (layout_cache().as_mut(), child_of.first_relation().opt()); let mut query = entity.query(&query); @@ -117,10 +122,11 @@ pub(crate) fn validate_cached_query( // tracing::debug!( ?preferred_size, %cache.limits.max_size, %limits.max_size, "validate_cached_query"); let hints = &value.hints; + #[allow(clippy::nonminimal_bool)] if hints.can_grow.x && cache.limits.max_size.x < limits.max_size.x || (hints.can_grow.x && cache.limits.max_size.y < limits.max_size.y) { - tracing::info!(%hints.can_grow, ?cache.limits.max_size, %limits.max_size, "invalidated by can_grow"); + // tracing::info!(%hints.can_grow, ?cache.limits.max_size, %limits.max_size, "invalidated by can_grow"); } min_size.x >= limits.min_size.x - LAYOUT_TOLERANCE @@ -149,10 +155,11 @@ pub(crate) fn validate_cached_layout( // tracing::debug!( ?size, %cache.limits.max_size, %limits.max_size, "validate_cached_layout"); + #[allow(clippy::nonminimal_bool)] if value.can_grow.x && cache.limits.max_size.x < limits.max_size.x || (value.can_grow.x && cache.limits.max_size.y < limits.max_size.y) { - tracing::info!(%value.can_grow, ?cache.limits.max_size, %limits.max_size, "invalidated layout by can_grow"); + // tracing::info!(%value.can_grow, ?cache.limits.max_size, %limits.max_size, "invalidated layout by can_grow"); } size.x >= limits.min_size.x - LAYOUT_TOLERANCE diff --git a/violet-core/src/layout/flow.rs b/violet-core/src/layout/flow.rs index 9b31204..efeec59 100644 --- a/violet-core/src/layout/flow.rs +++ b/violet-core/src/layout/flow.rs @@ -14,7 +14,7 @@ use crate::{ }; use super::{ - cache::LayoutCache, resolve_pos, update_subtree, Block, Direction, LayoutLimits, QueryArgs, + apply_layout, cache::LayoutCache, resolve_pos, Block, Direction, LayoutLimits, QueryArgs, Sizing, }; @@ -163,6 +163,7 @@ pub(crate) struct Row { pub(crate) min: Rect, pub(crate) preferred: Rect, pub(crate) blocks: Arc>, + pub(crate) margin: Edges, pub(crate) hints: SizingHints, } @@ -336,7 +337,7 @@ impl FlowLayout { }; // let local_rect = widget_outer_bounds(world, &child, size); - let block = update_subtree(world, &entity, content_area.size(), child_limits); + let block = apply_layout(world, &entity, content_area.size(), child_limits); can_grow |= block.can_grow; tracing::debug!(?block, "updated subtree"); @@ -489,7 +490,7 @@ impl FlowLayout { "min is larger than preferred", ); - return; + // return; } assert!(block_min_size.is_finite()); @@ -665,11 +666,18 @@ impl FlowLayout { let preferred = preferred_cursor.finish(); let min = min_cursor.finish(); + let margin = self.direction.to_edges( + preferred_cursor.main_margin, + preferred_cursor.cross_margin, + self.reverse, + ); + let row = Row { min, preferred, blocks: Arc::new(blocks), hints, + margin, }; cache.insert_query_row(CachedValue::new( @@ -730,8 +738,44 @@ impl FlowLayout { }, ); - let sizing = self.distribute_query(world, &row, args, preferred_size); - tracing::debug!(?self.direction, ?sizing, "query"); - sizing + if row.hints.coupled_size { + let sizing = self.distribute_query(world, &row, args, preferred_size); + tracing::debug!(?self.direction, ?sizing, "query"); + sizing + } else { + let (axis, cross) = self.direction.as_main_and_cross(self.reverse); + let minimum_inner_size = row.min.size().dot(axis); + + let preferred_size = row.preferred.size().dot(axis); + let to_distribute = (preferred_size - minimum_inner_size).max(0.0); + + let can_grow = to_distribute > (args.limits.max_size.dot(axis) - minimum_inner_size); + + let can_grow = if self.direction.is_horizontal() { + BVec2::new(can_grow, false) + } else { + BVec2::new(false, can_grow) + }; + + let to_distribute = to_distribute + .min(args.limits.max_size.dot(axis) - minimum_inner_size) + .max(0.0); + + let preferred = + (minimum_inner_size + to_distribute) * axis + row.preferred.size() * cross; + + let min = row.min.max_size(args.limits.min_size); + let preferred = preferred.max(preferred).max(args.limits.min_size); + + Sizing { + min, + preferred: Rect::from_size(preferred), + margin: row.margin, + hints: SizingHints { + can_grow: can_grow | row.hints.can_grow, + ..row.hints + }, + } + } } } diff --git a/violet-core/src/layout/mod.rs b/violet-core/src/layout/mod.rs index 8d1b724..ba6e5dd 100644 --- a/violet-core/src/layout/mod.rs +++ b/violet-core/src/layout/mod.rs @@ -12,7 +12,7 @@ use crate::{ self, anchor, aspect_ratio, children, layout, max_size, min_size, offset, padding, size, size_resolver, }, - layout::cache::{validate_cached_layout, validate_cached_query, CachedValue}, + layout::cache::{validate_cached_layout, validate_cached_query, CachedValue, LAYOUT_TOLERANCE}, Edges, Rect, }; @@ -53,6 +53,22 @@ impl Direction { Direction::Vertical => Vec2::Y, } } + + /// Returns `true` if the direction is [`Horizontal`]. + /// + /// [`Horizontal`]: Direction::Horizontal + #[must_use] + pub fn is_horizontal(&self) -> bool { + matches!(self, Self::Horizontal) + } + + /// Returns `true` if the direction is [`Vertical`]. + /// + /// [`Vertical`]: Direction::Vertical + #[must_use] + pub fn is_vertical(&self) -> bool { + matches!(self, Self::Vertical) + } } #[derive(Debug, Clone)] @@ -180,6 +196,10 @@ pub struct SizingHints { /// This is used for an optimization to avoid invalidating the layout when the available size /// increases pub relative_size: BVec2, + /// Changes to width affect the height and vice versa. + /// + /// This is used to optimize the layout query as not full distribution queries are needed + pub coupled_size: bool, } impl Default for SizingHints { @@ -187,6 +207,7 @@ impl Default for SizingHints { Self { can_grow: BVec2::FALSE, relative_size: BVec2::FALSE, + coupled_size: false, } } } @@ -196,6 +217,7 @@ impl SizingHints { Self { can_grow: self.can_grow | other.can_grow, relative_size: self.relative_size | other.relative_size, + coupled_size: self.coupled_size | other.coupled_size, } } } @@ -295,8 +317,6 @@ pub(crate) fn query_size(world: &World, entity: &EntityRef, mut args: QueryArgs) let external_max_size = args.limits.max_size; - let external_max_size = args.limits.max_size; - // Minimum size is *always* respected, even if that entails overflowing args.limits.max_size = args.limits.max_size.max(args.limits.min_size); @@ -325,6 +345,7 @@ pub(crate) fn query_size(world: &World, entity: &EntityRef, mut args: QueryArgs) resolved_size.x > external_max_size.x, resolved_size.y > external_max_size.y, ), + coupled_size: false, }; // if hints != Default::default() { @@ -410,7 +431,6 @@ pub(crate) fn query_size(world: &World, entity: &EntityRef, mut args: QueryArgs) args.direction, CachedValue::new(args.limits, args.content_area, sizing), ); - cache.relative_size = sizing.hints.relative_size; sizing } @@ -419,7 +439,7 @@ pub(crate) fn query_size(world: &World, entity: &EntityRef, mut args: QueryArgs) /// /// Returns the outer bounds of the subtree. #[must_use = "This function does not mutate the entity"] -pub(crate) fn update_subtree( +pub(crate) fn apply_layout( world: &World, entity: &EntityRef, // The size of the potentially available space for the subtree @@ -462,7 +482,7 @@ pub(crate) fn update_subtree( // Check if cache is still valid if let Some(value) = &cache.layout { - if validate_cached_layout(value, limits, content_area, cache.relative_size) { + if validate_cached_layout(value, limits, content_area, cache.hints.relative_size) { tracing::debug!(%entity, %value.value.rect, %value.value.can_grow, "found valid cached layout"); return value.value; @@ -487,9 +507,7 @@ pub(crate) fn update_subtree( resolved_size.y > external_max_size.y, ); - if can_grow.any() { - tracing::info!(%entity, ?resolved_size, ?external_max_size, "can grow"); - } + // tracing::trace!(%entity, ?resolved_size, ?external_max_size, %can_grow); let resolved_size = resolved_size.clamp(limits.min_size, limits.max_size); @@ -514,7 +532,7 @@ pub(crate) fn update_subtree( block } else if let [child] = children { let child = world.entity(*child).unwrap(); - let block = update_subtree(world, &child, content_area, limits); + let block = apply_layout(world, &child, content_area, limits); child.update_dedup(components::rect(), block.rect); block @@ -523,7 +541,7 @@ pub(crate) fn update_subtree( let (intrinsic_size, instrinsic_can_grow) = size_resolver .map(|v| v.apply(entity, content_area, limits)) - .unwrap_or((Vec2::ZERO, BVec2::TRUE)); + .unwrap_or((Vec2::ZERO, BVec2::FALSE)); let size = intrinsic_size.max(resolved_size); diff --git a/violet-core/src/layout/stack.rs b/violet-core/src/layout/stack.rs index 3fc8c74..203e471 100644 --- a/violet-core/src/layout/stack.rs +++ b/violet-core/src/layout/stack.rs @@ -9,7 +9,7 @@ use crate::{ }; use super::{ - resolve_pos, update_subtree, Alignment, Block, Direction, LayoutLimits, QueryArgs, Sizing, + resolve_pos, apply_layout, Alignment, Block, Direction, LayoutLimits, QueryArgs, Sizing, }; #[derive(Debug)] @@ -119,7 +119,7 @@ impl StackLayout { max_size: limits.max_size, }; - let block = update_subtree(world, &entity, content_area.size(), limits); + let block = apply_layout(world, &entity, content_area.size(), limits); bounds = bounds.merge(block.rect.translate(content_area.min)); diff --git a/violet-core/src/systems.rs b/violet-core/src/systems.rs index b7d20b4..2e88154 100644 --- a/violet-core/src/systems.rs +++ b/violet-core/src/systems.rs @@ -23,8 +23,9 @@ use crate::{ self, children, layout_bounds, local_position, rect, screen_position, screen_rect, text, }, layout::{ + apply_layout, cache::{invalidate_widget, layout_cache, LayoutCache, LayoutUpdate}, - update_subtree, LayoutLimits, + LayoutLimits, }, Rect, }; @@ -149,32 +150,37 @@ impl EventSubscriber for QueryInvalidator { } } /// Updates the layout for entities using the given constraints -pub fn layout_system() -> BoxedSystem { +pub fn layout_system(root: Entity) -> BoxedSystem { puffin::profile_function!(); System::builder() .with_world() - .with_query(Query::new((rect(), children())).without_relation(child_of)) - .build(move |world: &World, mut roots: QueryBorrow<_, _>| { + // .with_query(Query::new((rect(), children())).without_relation(child_of)) + .build(move |world: &World| { + let Ok(entity) = world.entity(root) else { + return; + }; + let query = (rect().opt_or_default(), children().opt_or_default()); + let mut query = entity.query(&query); + + let (canvas_rect, children) = query.get().unwrap(); + puffin::profile_scope!("layout_system"); - (&mut roots) - .into_iter() - .for_each(|(canvas_rect, children): (&Rect, &Vec<_>)| { - for &child in children { - let entity = world.entity(child).unwrap(); - - let res = update_subtree( - world, - &entity, - canvas_rect.size(), - LayoutLimits { - min_size: Vec2::ZERO, - max_size: canvas_rect.size(), - }, - ); - - entity.update_dedup(components::rect(), res.rect); - } - }); + + for &child in children { + let entity = world.entity(child).unwrap(); + + let res = apply_layout( + world, + &entity, + canvas_rect.size(), + LayoutLimits { + min_size: Vec2::ZERO, + max_size: canvas_rect.size(), + }, + ); + + entity.update_dedup(components::rect(), res.rect); + } }) .boxed() } diff --git a/violet-core/src/widget/basic.rs b/violet-core/src/widget/basic.rs index af2335b..8634489 100644 --- a/violet-core/src/widget/basic.rs +++ b/violet-core/src/widget/basic.rs @@ -88,7 +88,7 @@ impl Default for TextStyle { fn default() -> Self { Self { font_size: 16.0, - wrap: Wrap::WordOrGlyph, + wrap: Wrap::Word, color: None, } } diff --git a/violet-core/src/widget/interactive/button.rs b/violet-core/src/widget/interactive/button.rs index 97aee10..77e6332 100644 --- a/violet-core/src/widget/interactive/button.rs +++ b/violet-core/src/widget/interactive/button.rs @@ -190,3 +190,57 @@ impl Widget for Checkbox { .mount(scope); } } + +/// A button that can only be set +pub struct Radio { + state: Box>, + style: ButtonStyle, + size: WidgetSize, +} + +impl Radio { + pub fn new(state: impl 'static + Send + Sync + StateDuplex) -> Self { + Self { + state: Box::new(state), + style: Default::default(), + size: WidgetSize::default() + .with_padding(spacing_medium()) + .with_margin(spacing_medium()) + .with_min_size(Unit::px2(28.0, 28.0)), + } + } +} + +impl Widget for Radio { + fn mount(self, scope: &mut Scope<'_>) { + let stylesheet = scope.stylesheet(); + + let pressed_color = self.style.pressed_color.resolve(stylesheet); + let normal_color = self.style.normal_color.resolve(stylesheet); + + scope.spawn_stream(self.state.stream(), { + move |scope, state| { + let color = if state { pressed_color } else { normal_color }; + + scope.set(components::color(), color); + } + }); + + scope + .set(focusable(), ()) + .on_event(on_mouse_input(), move |_, _, input| { + if input.state == ElementState::Pressed { + self.state.send(true) + } + }); + + Stack::new(()) + .with_style(ContainerStyle { + background: Some(Background::new(normal_color)), + }) + .with_horizontal_alignment(Alignment::Center) + .with_vertical_alignment(Alignment::Center) + .with_size_props(self.size) + .mount(scope); + } +} diff --git a/violet-demo/src/lib.rs b/violet-demo/src/lib.rs index 16c2a51..2f5fb76 100644 --- a/violet-demo/src/lib.rs +++ b/violet-demo/src/lib.rs @@ -16,7 +16,7 @@ use violet::{ unit::Unit, utils::zip_latest_ref, widget::{ - card, column, label, pill, row, Button, Checkbox, Rectangle, SliderWithLabel, Stack, + card, column, label, pill, row, Button, Radio, Rectangle, SliderWithLabel, Stack, StreamWidget, Text, TextInput, WidgetExt, }, Edges, Scope, Widget, @@ -183,7 +183,7 @@ impl Widget for Palettes { to_owned![current_choice]; let discard = &discard; move |(i, item)| { - let checkbox = Checkbox::new( + let checkbox = Radio::new( current_choice .clone() .map(move |v| v == Some(i), move |state| state.then_some(i)), @@ -260,6 +260,7 @@ impl Widget for PaletteEditor { let color_rect = color.stream().map(|v| { Rectangle::new(ValueOrRef::value(v.into_color())) .with_size(Unit::new(vec2(0.0, 100.0), vec2(1.0, 0.0))) + // .with_min_size(Unit::new(vec2(0.0, 100.0), vec2(1.0, 0.0))) .with_name("ColorPreview") }); diff --git a/violet-wgpu/Cargo.toml b/violet-wgpu/Cargo.toml index ce08150..8408c50 100644 --- a/violet-wgpu/Cargo.toml +++ b/violet-wgpu/Cargo.toml @@ -13,6 +13,7 @@ flax.workspace = true wgpu.workspace = true winit.workspace = true +smallvec.workspace = true tracing.workspace = true cosmic-text.workspace = true guillotiere.workspace = true diff --git a/violet-wgpu/src/app.rs b/violet-wgpu/src/app.rs index 6662c05..31f5054 100644 --- a/violet-wgpu/src/app.rs +++ b/violet-wgpu/src/app.rs @@ -152,6 +152,7 @@ impl App { .send(WindowRenderer::new( frame, gpu, + root, text_system.clone(), surface, layout_changes_rx.clone(), @@ -172,7 +173,7 @@ impl App { .flush() .with_system(update_text_buffers(text_system.clone())) .with_system(invalidate_cached_layout_system(&mut frame.world)) - .with_system(layout_system()) + .with_system(layout_system(root)) .with_system(transform_system()); let start_time = Instant::now(); diff --git a/violet-wgpu/src/renderer/debug_renderer.rs b/violet-wgpu/src/renderer/debug_renderer.rs index 81653f3..330133a 100644 --- a/violet-wgpu/src/renderer/debug_renderer.rs +++ b/violet-wgpu/src/renderer/debug_renderer.rs @@ -8,7 +8,10 @@ use palette::bool_mask::Select; use violet_core::{ assets::Asset, components::screen_rect, - layout::cache::{layout_cache, LayoutUpdate}, + layout::{ + cache::{layout_cache, LayoutUpdate}, + Direction, + }, stored::{self, Handle}, Frame, }; @@ -36,7 +39,8 @@ pub struct DebugRenderer { mesh: Arc, - shader: stored::Handle, + corner_shader: stored::Handle, + border_shader: stored::Handle, layout_changes_rx: flume::Receiver<(Entity, LayoutUpdate)>, layout_changes: BTreeMap<(Entity, LayoutUpdate), usize>, @@ -87,7 +91,7 @@ impl DebugRenderer { let mesh = Arc::new(ctx.mesh_buffer.insert(&ctx.gpu, &vertices, &indices)); - let shader = store.shaders.insert(Shader::new( + let corner_shader = store.shaders.insert(Shader::new( &ctx.gpu, &ShaderDesc { label: "ShapeRenderer::shader", @@ -97,13 +101,24 @@ impl DebugRenderer { layouts: &[&ctx.globals_layout, &object_bind_group_layout, &layout], }, )); + let border_shader = store.shaders.insert(Shader::new( + &ctx.gpu, + &ShaderDesc { + label: "ShapeRenderer::shader", + source: include_str!("../../../assets/shaders/border_shader.wgsl"), + format: color_format, + vertex_layouts: &[Vertex::layout()], + layouts: &[&ctx.globals_layout, &object_bind_group_layout, &layout], + }, + )); Self { white_image, layout, bind_group, sampler, mesh, - shader, + corner_shader, + border_shader, layout_changes_rx, layout_changes: BTreeMap::new(), objects: Vec::new(), @@ -111,6 +126,7 @@ impl DebugRenderer { } pub fn update(&mut self, frame: &mut Frame) { + puffin::profile_function!(); self.layout_changes.extend( self.layout_changes_rx .try_iter() @@ -122,41 +138,47 @@ impl DebugRenderer { let mut query = Query::new((entity_refs(), layout_cache())); let mut query = query.borrow(&frame.world); - // let clamped_indicators = query.iter().filter_map(|(entity, v)| { - // let clamped_query_vertical = - // if v.query()[0].as_ref().is_some_and(|v| v.value.hints.can_grow) { - // vec3(0.5, 0.0, 0.0) - // } else { - // Vec3::ZERO - // }; - - // let clamped_query_horizontal = - // if v.query()[1].as_ref().is_some_and(|v| v.value.hints.can_grow) { - // vec3(0.0, 0.5, 0.0) - // } else { - // Vec3::ZERO - // }; - - // let clamped_layout = if v.layout().map(|v| v.value.can_grow).unwrap_or(false) { - // vec3(0.0, 0.0, 0.5) - // } else { - // Vec3::ZERO - // }; + let clamped_indicators = query.iter().filter_map(|(entity, v)| { + let clamped_query_vertical = if v + .get_query(Direction::Vertical) + .as_ref() + .is_some_and(|v| v.value.hints.can_grow.any()) + { + vec3(0.5, 0.0, 0.0) + } else { + Vec3::ZERO + }; - // let color: Vec3 = [ - // clamped_query_vertical, - // clamped_query_horizontal, - // clamped_layout, - // ] - // .into_iter() - // .sum(); + let clamped_query_horizontal = if v + .get_query(Direction::Horizontal) + .as_ref() + .is_some_and(|v| v.value.hints.can_grow.any()) + { + vec3(0.0, 0.5, 0.0) + } else { + Vec3::ZERO + }; - // if color == Vec3::ZERO { - // None - // } else { - // Some((entity, color.extend(1.0))) - // } - // }); + let clamped_layout = if v.layout().is_some_and(|v| v.value.can_grow.any()) { + vec3(0.0, 0.0, 0.5) + } else { + Vec3::ZERO + }; + + let color: Vec3 = [ + clamped_query_vertical, + clamped_query_horizontal, + clamped_layout, + ] + .into_iter() + .sum(); + + if color == Vec3::ZERO { + None + } else { + Some((entity, &self.corner_shader, color.extend(1.0))) + } + }); let mut query = Query::new((entity_refs(), layout_cache())); let mut query = query.borrow(&frame.world); @@ -182,43 +204,45 @@ impl DebugRenderer { .sum(); let entity = frame.world.entity(id).ok()?; - Some((entity, color)) + Some((entity, &self.border_shader, color)) }); - let objects = objects.filter_map(|(entity, color)| { - let screen_rect = entity.get(screen_rect()).ok()?.align_to_grid(); - - let model_matrix = Mat4::from_scale_rotation_translation( - screen_rect.size().extend(1.0), - Quat::IDENTITY, - screen_rect.pos().extend(0.2), - ); - - let object_data = ObjectData { - model_matrix, - color, - }; - - Some(( - DrawCommand { - shader: self.shader.clone(), - bind_group: self.bind_group.clone(), - mesh: self.mesh.clone(), - index_count: 6, - }, - object_data, - )) - }); + let objects = clamped_indicators + .chain(objects) + .filter_map(|(entity, shader, color)| { + let screen_rect = entity.get(screen_rect()).ok()?.align_to_grid(); + + let model_matrix = Mat4::from_scale_rotation_translation( + screen_rect.size().extend(1.0), + Quat::IDENTITY, + screen_rect.pos().extend(0.2), + ); + + let object_data = ObjectData { + model_matrix, + color, + }; + + Some(( + DrawCommand { + shader: shader.clone(), + bind_group: self.bind_group.clone(), + mesh: self.mesh.clone(), + index_count: 6, + }, + object_data, + )) + }); self.objects.clear(); self.objects.extend(objects); - self.layout_changes.clear(); - // self.layout_changes.retain(|_, lifetime| { - // *lifetime -= 1; + // self.layout_changes.clear(); + self.layout_changes.retain(|_, lifetime| { + *lifetime -= 1; - // *lifetime > 0 - // }); + *lifetime > 0 + }); } pub fn draw_commands(&self) -> &[(DrawCommand, ObjectData)] { diff --git a/violet-wgpu/src/renderer/mod.rs b/violet-wgpu/src/renderer/mod.rs index a62adb6..dc4052c 100644 --- a/violet-wgpu/src/renderer/mod.rs +++ b/violet-wgpu/src/renderer/mod.rs @@ -11,6 +11,7 @@ use glam::{vec4, Mat4, Vec4}; use itertools::Itertools; use palette::Srgba; use parking_lot::Mutex; +use smallvec::{smallvec, SmallVec}; use violet_core::{ components::{children, draw_shape}, layout::cache::LayoutUpdate, @@ -179,12 +180,14 @@ pub struct MainRenderer { debug_renderer: Option, object_bind_group_layout: BindGroupLayout, + root: Entity, } impl MainRenderer { pub(crate) fn new( frame: &mut Frame, ctx: &mut RendererContext, + root: Entity, text_system: Arc>, color_format: TextureFormat, layout_changes_rx: flume::Receiver<(Entity, LayoutUpdate)>, @@ -239,6 +242,7 @@ impl MainRenderer { register_objects, object_bind_group_layout, object_buffers: Vec::new(), + root, } } @@ -269,19 +273,12 @@ impl MainRenderer { } { - puffin::profile_scope!("collect_draw_commands"); + puffin::profile_scope!("create_draw_commands"); let query = DrawQuery::new(); - let roots = Query::new(entity_ids()) - .without_relation(child_of) - .borrow(&frame.world) - .iter() - .map(|id| frame.world.entity(id).unwrap()) - .collect(); - let commands = RendererIter { world: &frame.world, - stack: roots, + stack: smallvec![frame.world.entity(self.root).unwrap()], } .filter_map(|entity| { let mut query = entity.query(&query); @@ -353,6 +350,7 @@ fn collect_draw_commands( objects: &mut Vec, draw_cmds: &mut Vec<(usize, InstancedDrawCommand)>, ) { + puffin::profile_function!(); let chunks = entities.chunks(CHUNK_SIZE); for (chunk_index, chunk) in (&chunks).into_iter().enumerate() { @@ -400,7 +398,7 @@ pub(crate) struct ObjectData { struct RendererIter<'a> { world: &'a World, // queue: VecDeque>, - stack: Vec>, + stack: SmallVec<[EntityRef<'a>; 16]>, } impl<'a> Iterator for RendererIter<'a> { diff --git a/violet-wgpu/src/renderer/window_renderer.rs b/violet-wgpu/src/renderer/window_renderer.rs index 3b2fc71..4297d87 100644 --- a/violet-wgpu/src/renderer/window_renderer.rs +++ b/violet-wgpu/src/renderer/window_renderer.rs @@ -26,6 +26,7 @@ impl WindowRenderer { pub fn new( frame: &mut Frame, gpu: Gpu, + root: Entity, text_system: Arc>, surface: Surface, layout_changes_rx: flume::Receiver<(Entity, LayoutUpdate)>, @@ -36,6 +37,7 @@ impl WindowRenderer { let widget_renderer = MainRenderer::new( frame, &mut ctx, + root, text_system, surface.surface_format(), layout_changes_rx, diff --git a/violet-wgpu/src/text.rs b/violet-wgpu/src/text.rs index 9cde39a..6649adf 100644 --- a/violet-wgpu/src/text.rs +++ b/violet-wgpu/src/text.rs @@ -67,21 +67,25 @@ impl SizeResolver for TextSizeResolver { let line_height = state.buffer.metrics().line_height; + // Text wraps to the size of the container + // + // Wrapping text will decrease width, and increase height. + // + // + // To optimize for X, we wrap as much as possible, and then measure the height. + // + // To optimize for Y, we wrap as little as possible. This is equivalent to the preferred + // size as the widest width (which text wants) also gives the least height. + // If preferred is can_grow, so is min - let (min, _can_grow) = Self::resolve_text_size( + let (most_wrapped, _can_grow, wrapped_lines) = Self::resolve_text_size( state, text_system, font_size, - match args.direction { - Direction::Horizontal => vec2(1.0, args.limits.max_size.y.max(line_height)), - Direction::Vertical => vec2( - args.limits.max_size.x, - args.limits.max_size.y.max(line_height), - ), - }, + vec2(1.0, args.limits.max_size.y.max(line_height)), ); - let (preferred, can_grow) = Self::resolve_text_size( + let (preferred, can_grow, preferred_lines) = Self::resolve_text_size( state, text_system, font_size, @@ -89,15 +93,23 @@ impl SizeResolver for TextSizeResolver { ); // + vec2(5.0, 5.0); - if min.dot(args.direction.to_axis()) > preferred.dot(args.direction.to_axis()) { - tracing::error!(%entity, text=?state.text(), %min, %preferred, ?args.direction, %args.limits.max_size, "Text wrapping failed"); - } + // if min.dot(args.direction.to_axis()) > preferred.dot(args.direction.to_axis()) { + // tracing::error!(%entity, text=?state.text(), %min, %preferred, ?args.direction, %args.limits.max_size, "Text wrapping failed"); + // } + + // tracing::info!(?wrapped_lines, ?preferred_lines, "Text wrapping results"); + ( - min, + if args.direction.is_horizontal() { + most_wrapped + } else { + preferred + }, preferred, SizingHints { can_grow, relative_size: BVec2::TRUE, + coupled_size: wrapped_lines != preferred_lines, }, ) } @@ -119,7 +131,7 @@ impl SizeResolver for TextSizeResolver { let text_system = &mut *self.text_system.lock(); let line_height = state.buffer.metrics().line_height; - let (size, can_grow) = Self::resolve_text_size( + let (size, can_grow, _) = Self::resolve_text_size( state, text_system, font_size, @@ -146,7 +158,7 @@ impl TextSizeResolver { text_system: &mut TextSystem, font_size: f32, size: Vec2, - ) -> (Vec2, BVec2) { + ) -> (Vec2, BVec2, usize) { // let _span = tracing::debug_span!("resolve_text_size", font_size, ?text, ?limits).entered(); let mut buffer = state.buffer.borrow_with(&mut text_system.font_system); @@ -165,7 +177,7 @@ fn glyph_bounds(glyph: &LayoutGlyph) -> (f32, f32) { (glyph.x, glyph.x + glyph.w) } -fn measure(buffer: &Buffer) -> (Vec2, BVec2) { +fn measure(buffer: &Buffer) -> (Vec2, BVec2, usize) { let (width, total_lines) = buffer .layout_runs() @@ -191,6 +203,7 @@ fn measure(buffer: &Buffer) -> (Vec2, BVec2) { ( vec2(width, total_lines as f32 * buffer.metrics().line_height), BVec2::new(total_lines > buffer.lines.len(), false), + total_lines, ) } From 59beb44aec1d997b76bf38e39d966753fa923fbb Mon Sep 17 00:00:00 2001 From: Freja Roberts Date: Sat, 16 Mar 2024 20:09:49 +0100 Subject: [PATCH 06/16] feat: `maximize` and some performance --- .gitmodules | 0 Cargo.lock | 6 +- Cargo.toml | 1 + examples/basic.rs | 16 +-- examples/flow.rs | 24 +--- examples/row.rs | 24 +++- examples/sizing.rs | 9 +- recipes.json | 6 +- src/lib.rs | 1 + violet-core/src/components.rs | 2 + violet-core/src/effect/mod.rs | 39 +++++++ violet-core/src/executor.rs | 4 + violet-core/src/input.rs | 4 +- violet-core/src/layout/flow.rs | 88 +++++---------- violet-core/src/layout/mod.rs | 109 +++++++++++-------- violet-core/src/layout/stack.rs | 6 +- violet-core/src/scope.rs | 4 + violet-core/src/style/mod.rs | 29 +++-- violet-core/src/utils.rs | 61 ++++++++++- violet-core/src/widget/basic.rs | 77 +++++-------- violet-core/src/widget/container.rs | 5 +- violet-core/src/widget/future.rs | 61 ++++++----- violet-core/src/widget/interactive/button.rs | 7 +- violet-core/src/widget/interactive/input.rs | 5 +- violet-core/src/widget/interactive/slider.rs | 16 +-- violet-demo/Cargo.toml | 1 + violet-demo/index.html | 8 +- violet-demo/src/lib.rs | 20 +++- violet-wgpu/src/app.rs | 5 +- violet-wgpu/src/renderer/debug_renderer.rs | 14 +-- 30 files changed, 376 insertions(+), 276 deletions(-) create mode 100644 .gitmodules diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..e69de29 diff --git a/Cargo.lock b/Cargo.lock index 6764b7d..d9f3264 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -630,7 +630,7 @@ dependencies = [ [[package]] name = "flax" version = "0.6.2" -source = "git+https://github.com/ten3roberts/flax#bb23e6eaf91195371b96d8725cfef42efdc4b19c" +source = "git+https://github.com/ten3roberts/flax#b75a7275d9a119309d3d7f41d824347dc2f9806b" dependencies = [ "anyhow", "atomic_refcell", @@ -648,7 +648,7 @@ dependencies = [ [[package]] name = "flax-derive" version = "0.6.0" -source = "git+https://github.com/ten3roberts/flax#bb23e6eaf91195371b96d8725cfef42efdc4b19c" +source = "git+https://github.com/ten3roberts/flax#b75a7275d9a119309d3d7f41d824347dc2f9806b" dependencies = [ "itertools 0.11.0", "proc-macro-crate", @@ -2500,6 +2500,7 @@ dependencies = [ "tracing-tree", "violet-core", "violet-wgpu", + "web-time 1.0.0", ] [[package]] @@ -2550,6 +2551,7 @@ dependencies = [ "futures", "glam", "itertools 0.12.1", + "puffin", "tracing-subscriber", "tracing-tree", "tracing-web", diff --git a/Cargo.toml b/Cargo.toml index f867acc..8f85f89 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,6 +82,7 @@ palette.workspace = true futures-signals.workspace = true flax.workspace = true lru.workspace = true +web-time.workspace = true [dev-dependencies] anyhow.workspace = true diff --git a/examples/basic.rs b/examples/basic.rs index f7d3ba1..36059a5 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -24,7 +24,7 @@ use violet_core::{ danger_item, primary_background, secondary_background, spacing_medium, spacing_small, Background, SizeExt, ValueOrRef, }, - widget::{BoxSized, ContainerStyle}, + widget::ContainerStyle, }; struct MainApp; @@ -50,7 +50,7 @@ impl Widget for MainApp { .map(|i| { let size = Vec2::splat(128.0 / i as f32); Stack::new( - BoxSized::new(Image::new("./assets/images/statue.jpg")) + Image::new("./assets/images/statue.jpg") .with_min_size(Unit::px(size)) .with_aspect_ratio(1.0), ) @@ -100,10 +100,10 @@ impl Widget for MainApp { .with_padding(spacing_small()) .with_background(Background::new(primary_background())), Stack::new(( - BoxSized::new(Rectangle::new(danger_item())) + Rectangle::new(danger_item()) .with_min_size(Unit::px(vec2(100.0, 30.0))) .with_size(Unit::px(vec2(50.0, 30.0))), - BoxSized::new(Rectangle::new(danger_item())) + Rectangle::new(danger_item()) .with_min_size(Unit::px(vec2(200.0, 10.0))) .with_size(Unit::px(vec2(50.0, 10.0))), Text::new("This is some text").with_font_size(16.0), @@ -170,14 +170,14 @@ impl Widget for LayoutFlexTest { List::new( (0..8) .map(|i| { - let size = vec2(100.0, 20.0); + let size = vec2(50.0, 20.0); Stack::new( - BoxSized::new(Rectangle::new(ValueOrRef::value( + Rectangle::new(ValueOrRef::value( Hsva::new(i as f32 * 30.0, 1.0, 1.0, 1.0).into_color(), - ))) + )) .with_min_size(Unit::px(size)) - .with_size(Unit::px(size * vec2(i as f32, 1.0))), + .with_maximize(Vec2::X * i as f32), ) .with_margin(spacing_small()) }) diff --git a/examples/flow.rs b/examples/flow.rs index 761b510..b3555b8 100644 --- a/examples/flow.rs +++ b/examples/flow.rs @@ -18,28 +18,13 @@ use violet_core::{ style::{ self, colors::{EERIE_BLACK_600, EERIE_BLACK_DEFAULT}, - secondary_background, spacing_small, Background, SizeExt, + spacing_small, Background, SizeExt, }, text::Wrap, - widget::{ - card, column, row, BoxSized, Button, ButtonStyle, ContainerStyle, SliderWithLabel, - TextInput, - }, + widget::{card, column, label, row, Button, ButtonStyle, SliderWithLabel, TextInput}, }; use violet_wgpu::renderer::RendererConfig; -fn label(text: impl Into) -> Stack { - Stack::new(Text::new(text.into())) - .with_padding(spacing_small()) - .with_margin(spacing_small()) -} - -fn pill(widget: impl Widget) -> impl Widget { - Stack::new(widget).with_style(ContainerStyle { - background: Some(Background::new(secondary_background())), - }) -} - pub fn main() -> anyhow::Result<()> { registry() .with( @@ -94,8 +79,7 @@ impl Widget for MainApp { )) .with_stretch(true), ), - BoxSized::new(Rectangle::new(EERIE_BLACK_600)) - .with_size(Unit::rel2(1.0, 0.0) + Unit::px2(0.0, 1.0)), + Rectangle::new(EERIE_BLACK_600).with_size(Unit::rel2(1.0, 0.0) + Unit::px2(0.0, 1.0)), card(column(( column(( row(( @@ -147,7 +131,7 @@ impl Widget for Tints { ); card(column(( - BoxSized::new(Rectangle::new(color)).with_size(Unit::px2(100.0, 40.0)), + 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 3de8791..5509c44 100644 --- a/examples/row.rs +++ b/examples/row.rs @@ -1,6 +1,8 @@ use std::iter::repeat; +use glam::{vec2, Vec2}; use itertools::Itertools; +use palette::named::DARKCYAN; use tracing_subscriber::{layer::SubscriberExt, registry, util::SubscriberInitExt, EnvFilter}; use tracing_tree::HierarchicalLayer; @@ -15,10 +17,13 @@ use violet::core::{ }; use violet_core::{ style::{ - colors::{JADE_400, JADE_DEFAULT, LION_DEFAULT, REDWOOD_100, ULTRA_VIOLET_DEFAULT}, + colors::{ + DARK_CYAN_DEFAULT, JADE_400, JADE_DEFAULT, LION_DEFAULT, REDWOOD_100, + ULTRA_VIOLET_DEFAULT, + }, spacing_medium, spacing_small, SizeExt, }, - widget::{card, column, label, row, Stack}, + widget::{card, centered, column, label, row, Image, Stack}, }; use violet_wgpu::renderer::RendererConfig; @@ -66,7 +71,17 @@ impl Widget for MainApp { // .with_min_size(Unit::px2(100.0, 100.0)) // .with_size(Unit::px2(0.0, 100.0) + Unit::rel2(1.0, 0.0)), // .with_margin(spacing_medium()), - row((0..16).map(|_| Stack::new(Item)).collect_vec()), + row((0..4) + .map(|_| Box::new(Stack::new(Item)) as Box) + .chain([Box::new( + centered((Rectangle::new(JADE_DEFAULT) + .with_maximize(vec2(1.0, 0.0)) + .with_size(Unit::px2(0.0, 50.0)) + .with_max_size(Unit::px2(1000.0, 100.0)),)) + .with_maximize(Vec2::ONE), + ) as Box]) + .collect_vec()) + .with_padding(spacing_small()), )) // .with_padding(spacing_medium()) .contain_margins(true), @@ -81,8 +96,9 @@ struct Item; impl Widget for Item { fn mount(self, scope: &mut Scope<'_>) { - Rectangle::new(ULTRA_VIOLET_DEFAULT) + Image::new("./assets/images/statue.jpg") .with_size(Unit::px2(100.0, 100.0)) + // .with_aspect_ratio(1.0) .with_margin(spacing_small()) .mount(scope) } diff --git a/examples/sizing.rs b/examples/sizing.rs index 5e95038..9029756 100644 --- a/examples/sizing.rs +++ b/examples/sizing.rs @@ -23,19 +23,12 @@ use violet_core::{ state::MapRef, style::{colors::DARK_CYAN_DEFAULT, SizeExt}, text::Wrap, - widget::{card, centered, column, row, Slider}, + widget::{card, centered, column, label, row, Slider}, }; use violet_wgpu::renderer::RendererConfig; const MARGIN_SM: Edges = Edges::even(4.0); -fn label(text: impl Into) -> Stack { - Stack::new(Text::new(text.into())) - .with_padding(MARGIN_SM) - .with_margin(MARGIN_SM) - .with_background(Background::new(EERIE_BLACK_400)) -} - pub fn main() -> anyhow::Result<()> { registry() .with( diff --git a/recipes.json b/recipes.json index 71f4881..d8affb8 100644 --- a/recipes.json +++ b/recipes.json @@ -1,6 +1,6 @@ { "check": { - "cmd": "cargo check --all-targets --all-features" + "cmd": "cargo check --all-targets --all-features --workspace" }, "run demo": { "cmd": "cargo run --package violet-demo" @@ -56,10 +56,10 @@ }, "build web": { "cmd": "wasm-pack build --target web", - "cwd": "./violet-web-example/" + "cwd": "./violet-demo/" }, "host": { "cmd": "python3 -m http.server 8080", - "cwd": "./violet-web-example/" + "cwd": "./violet-demo/" } } diff --git a/src/lib.rs b/src/lib.rs index fb896e7..037eb4d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,3 +5,4 @@ pub use flax; pub use futures_signals; pub use glam; pub use palette; +pub use web_time; diff --git a/violet-core/src/components.rs b/violet-core/src/components.rs index 147dbf0..1a6c5a0 100644 --- a/violet-core/src/components.rs +++ b/violet-core/src/components.rs @@ -66,6 +66,8 @@ component! { /// A margin is in essence a minimum allowed distance to another items bounds pub margin: Edges => [ Debuggable ], + pub maximize: Vec2 => [ Debuggable ], + pub text: Vec => [ ], pub text_wrap: Wrap => [ Debuggable ], pub font_size: f32 => [ Debuggable ], diff --git a/violet-core/src/effect/mod.rs b/violet-core/src/effect/mod.rs index c858e0e..5ac02a5 100644 --- a/violet-core/src/effect/mod.rs +++ b/violet-core/src/effect/mod.rs @@ -7,6 +7,7 @@ use std::{ }; pub use future::FutureEffect; +use pin_project::pin_project; pub use stream::StreamEffect; /// An asynchronous computation which has access to `Data` when polled @@ -16,4 +17,42 @@ pub use stream::StreamEffect; pub trait Effect { /// Polls the effect fn poll(self: Pin<&mut Self>, context: &mut Context<'_>, data: &mut Data) -> Poll<()>; + fn label(&self) -> Option<&str> { + None + } + + fn with_label(self, label: impl Into) -> EffectWithLabel + where + Self: Sized, + { + EffectWithLabel::new(self, Some(label.into())) + } +} + +#[pin_project] +#[doc(hidden)] +pub struct EffectWithLabel { + #[pin] + effect: E, + label: Option, +} + +impl EffectWithLabel { + pub fn new(effect: E, label: Option) -> Self { + Self { effect, label } + } +} + +impl Effect for EffectWithLabel +where + E: Effect, +{ + #[inline] + fn poll(self: Pin<&mut Self>, context: &mut Context<'_>, data: &mut Data) -> Poll<()> { + self.project().effect.poll(context, data) + } + + fn label(&self) -> Option<&str> { + self.label.as_deref() + } } diff --git a/violet-core/src/executor.rs b/violet-core/src/executor.rs index 3db4c46..4238ffc 100644 --- a/violet-core/src/executor.rs +++ b/violet-core/src/executor.rs @@ -174,8 +174,10 @@ impl Executor { } pub fn tick(&mut self, data: &mut Data) { + puffin::profile_function!(); assert!(self.processing.is_empty()); loop { + puffin::profile_scope!("tick"); // Add new tasks self.processing .extend(self.incoming.borrow_mut().drain(..).map(|task| { @@ -201,6 +203,8 @@ impl Executor { // external waker continue; }; + + puffin::profile_scope!("process task", task.effect.label().unwrap_or_default()); let mut context = Context::from_waker(&*waker); tracing::trace!(?id, "Polling task"); diff --git a/violet-core/src/input.rs b/violet-core/src/input.rs index c205731..611c28b 100644 --- a/violet-core/src/input.rs +++ b/violet-core/src/input.rs @@ -1,8 +1,6 @@ -use std::str::FromStr; - use flax::{ component, components::child_of, entity_ids, fetch::Satisfied, filter::All, Component, Entity, - EntityIds, EntityRef, Fetch, FetchExt, Mutable, Query, Topo, World, + EntityIds, EntityRef, Fetch, FetchExt, Query, Topo, World, }; use glam::Vec2; diff --git a/violet-core/src/layout/flow.rs b/violet-core/src/layout/flow.rs index efeec59..553526a 100644 --- a/violet-core/src/layout/flow.rs +++ b/violet-core/src/layout/flow.rs @@ -165,6 +165,7 @@ pub(crate) struct Row { pub(crate) blocks: Arc>, pub(crate) margin: Edges, pub(crate) hints: SizingHints, + maximize_sum: Vec2, } #[derive(Default, Debug, Clone)] @@ -225,25 +226,9 @@ impl FlowLayout { // If everything was squished as much as possible let minimum_inner_size = row.min.size().dot(axis); - // if minimum_inner_size > limits.max_size.dot(axis) { - // tracing::error!( - // ?minimum_inner_size, - // ?limits.max_size, - // "minimum inner size exceeded max size", - // ); - // } - // If everything could take as much space as it wants let preferred_inner_size = row.preferred.size().dot(axis); - // if minimum_inner_size > preferred_inner_size { - // tracing::error!( - // ?minimum_inner_size, - // ?preferred_inner_size, - // "minimum inner size exceeded preferred size", - // ); - // } - // How much space there is left to distribute to the children let distribute_size = (preferred_inner_size - minimum_inner_size).max(0.0); // tracing::info!(?distribute_size); @@ -253,14 +238,9 @@ impl FlowLayout { .min(limits.max_size.dot(axis) - minimum_inner_size) .max(0.0); - // tracing::info!( - // ?row.preferred, - // distribute_size, - // target_inner_size, - // blocks = row.blocks.len(), - // "query size" - // ); + let remaining_size = (limits.max_size.dot(axis) - preferred_inner_size).max(0.0); + // for cross let available_size = limits.max_size; let mut cursor = @@ -300,10 +280,17 @@ impl FlowLayout { remaining / distribute_size }; - let given_size = block_min_size + target_inner_size * ratio; + let mut given_size = block_min_size + target_inner_size * ratio; sum += ratio; + let maximize = sizing.maximize.dot(axis); + + if maximize > 0.0 { + given_size += remaining_size * (maximize / row.maximize_sum.dot(axis)); + } + let axis_sizing = given_size * axis; + // tracing::info!(ratio, %axis_sizing, block_min_size, target_inner_size); assert!( @@ -342,23 +329,6 @@ impl FlowLayout { can_grow |= block.can_grow; tracing::debug!(?block, "updated subtree"); - // block.rect = block - // .rect - // .clamp_size(child_limits.min_size, child_limits.max_size); - - // if block.rect.size().x > child_limits.max_size.x - // || block.rect.size().y > child_limits.max_size.y - // { - // tracing::error!( - // block_min_size, - // block_preferred_size, - // "child {} exceeded max size: {:?} > {:?}", - // entity, - // block.rect.size(), - // child_limits.max_size, - // ); - // } - cursor.put(&block); (entity, block) @@ -425,25 +395,9 @@ impl FlowLayout { // If everything was squished as much as possible let minimum_inner_size = row.min.size().dot(axis); - // if minimum_inner_size > limits.max_size.dot(axis) { - // tracing::error!( - // ?minimum_inner_size, - // ?limits.max_size, - // "minimum inner size exceeded max size", - // ); - // } - // If everything could take as much space as it wants let preferred_inner_size = row.preferred.size().dot(axis); - // if minimum_inner_size > preferred_inner_size { - // tracing::error!( - // ?minimum_inner_size, - // ?preferred_inner_size, - // "minimum inner size exceeded preferred size", - // ); - // } - // How much space there is left to distribute out let distribute_size = (preferred_inner_size - minimum_inner_size).max(0.0); // tracing::info!(?distribute_size); @@ -453,6 +407,8 @@ impl FlowLayout { .min(args.limits.max_size.dot(axis) - minimum_inner_size) .max(0.0); + let remaining_size = args.limits.max_size.dot(axis) - preferred_inner_size; + tracing::debug!( min=?row.min.size(), preferre2=?row.preferred.size(), @@ -505,9 +461,16 @@ impl FlowLayout { remaining / distribute_size }; - let given_size = block_min_size + target_inner_size * ratio; + let mut given_size = block_min_size + target_inner_size * ratio; sum += ratio; + let maximize = sizing.maximize.dot(axis); + + if maximize > 0.0 { + given_size = + given_size.max(remaining_size * (maximize / row.maximize_sum).dot(axis)); + } + let axis_sizing = given_size * axis; // tracing::info!(ratio, %axis_sizing, block_min_size, target_inner_size); @@ -551,6 +514,7 @@ impl FlowLayout { direction: args.direction, }); + hints = hints.combine(sizing.hints); tracing::debug!(min=%sizing.min.size(), preferred=%sizing.preferred.size(), ?child_limits, "query"); @@ -573,6 +537,7 @@ impl FlowLayout { preferred: rect.max_size(args.limits.min_size), margin, hints, + maximize: row.maximize_sum, } } @@ -586,7 +551,7 @@ impl FlowLayout { puffin::profile_function!(); if let Some(value) = cache.query_row.as_ref() { if validate_cached_row(value, args.limits, args.content_area) { - return value.value.clone(); + // return value.value.clone(); } } @@ -606,6 +571,8 @@ impl FlowLayout { let mut hints = SizingHints::default(); + let mut maximize = Vec2::ZERO; + let blocks = children .iter() .map(|&child| { @@ -642,6 +609,7 @@ impl FlowLayout { }, ); + maximize += sizing.maximize; hints = hints.combine(sizing.hints); min_cursor.put(&Block::new( @@ -678,6 +646,7 @@ impl FlowLayout { blocks: Arc::new(blocks), hints, margin, + maximize_sum: maximize, }; cache.insert_query_row(CachedValue::new( @@ -775,6 +744,7 @@ impl FlowLayout { can_grow: can_grow | row.hints.can_grow, ..row.hints }, + maximize: row.maximize_sum, } } } diff --git a/violet-core/src/layout/mod.rs b/violet-core/src/layout/mod.rs index ba6e5dd..0b07e8a 100644 --- a/violet-core/src/layout/mod.rs +++ b/violet-core/src/layout/mod.rs @@ -9,8 +9,8 @@ use glam::{vec2, BVec2, Vec2}; use crate::{ components::{ - self, anchor, aspect_ratio, children, layout, max_size, min_size, offset, padding, size, - size_resolver, + self, anchor, aspect_ratio, children, layout, max_size, maximize, min_size, offset, + padding, size, size_resolver, }, layout::cache::{validate_cached_layout, validate_cached_query, CachedValue, LAYOUT_TOLERANCE}, Edges, Rect, @@ -137,6 +137,7 @@ pub struct Sizing { preferred: Rect, margin: Edges, pub hints: SizingHints, + maximize: Vec2, } impl Display for Sizing { @@ -283,7 +284,7 @@ fn validate_block(entity: &EntityRef, block: &Block, limits: LayoutLimits) { } } -pub(crate) fn query_size(world: &World, entity: &EntityRef, mut args: QueryArgs) -> Sizing { +pub(crate) fn query_size(world: &World, entity: &EntityRef, args: QueryArgs) -> Sizing { puffin::profile_function!(format!("{entity} {args:?}")); // assert!(limits.min_size.x <= limits.max_size.x); // assert!(limits.min_size.y <= limits.max_size.y); @@ -312,21 +313,19 @@ pub(crate) fn query_size(world: &World, entity: &EntityRef, mut args: QueryArgs) min_size.is_relative() | max_size.map(|v| v.is_relative()).unwrap_or(BVec2::FALSE); let min_size = min_size.resolve(args.content_area); - let max_size = max_size.map(|v| v.resolve(args.content_area)); - args.limits.min_size = args.limits.min_size.max(min_size); - - let external_max_size = args.limits.max_size; - - // Minimum size is *always* respected, even if that entails overflowing - args.limits.max_size = args.limits.max_size.max(args.limits.min_size); - - if let Some(max_size) = max_size { - args.limits.max_size = args.limits.max_size.min(max_size); - } + let max_size = max_size + .map(|v| v.resolve(args.content_area)) + .unwrap_or(Vec2::INFINITY); + + let mut limits = LayoutLimits { + // Minimum size is *always* respected, even if that entails overflowing + min_size: args.limits.min_size.max(min_size), + max_size: args.limits.max_size.min(max_size), + }; // Check if cache is valid if let Some(cache) = cache.get_query(args.direction) { - if validate_cached_query(cache, args.limits, args.content_area) { + if validate_cached_query(cache, limits, args.content_area) { return cache.value; } } @@ -339,12 +338,14 @@ pub(crate) fn query_size(world: &World, entity: &EntityRef, mut args: QueryArgs) let children = children.map(Vec::as_slice).unwrap_or(&[]); let resolved_size = size.resolve(args.content_area); - let hints = SizingHints { + + let maximized = entity.get_copy(maximize()).unwrap_or_default(); + let mut hints = SizingHints { relative_size: fixed_boundary_size | size.is_relative(), can_grow: BVec2::new( - resolved_size.x > external_max_size.x, - resolved_size.y > external_max_size.y, - ), + resolved_size.x > args.limits.max_size.x, + resolved_size.y > args.limits.max_size.y, + ) | maximized.cmpgt(Vec2::ZERO), coupled_size: false, }; @@ -353,7 +354,7 @@ pub(crate) fn query_size(world: &World, entity: &EntityRef, mut args: QueryArgs) // } // Clamp max size here since we ensure it is > min_size - let resolved_size = resolved_size.clamp(args.limits.min_size, args.limits.max_size); + let resolved_size = resolved_size.clamp(limits.min_size, limits.max_size); // Flow let mut sizing = if let Some(layout) = layout { @@ -363,8 +364,8 @@ pub(crate) fn query_size(world: &World, entity: &EntityRef, mut args: QueryArgs) children, QueryArgs { limits: LayoutLimits { - min_size: (args.limits.min_size - padding.size()).max(Vec2::ZERO), - max_size: (args.limits.max_size - padding.size()).max(Vec2::ZERO), + min_size: (limits.min_size - padding.size()).max(Vec2::ZERO), + max_size: (limits.max_size - padding.size()).max(Vec2::ZERO), }, content_area: args.content_area - padding.size(), ..args @@ -377,6 +378,7 @@ pub(crate) fn query_size(world: &World, entity: &EntityRef, mut args: QueryArgs) min: sizing.min.pad(&padding), preferred: sizing.preferred.pad(&padding), hints: sizing.hints.combine(hints), + maximize: sizing.maximize + entity.get_copy(maximize()).unwrap_or_default(), } } else if let [child] = children { let child = world.entity(*child).unwrap(); @@ -387,22 +389,27 @@ pub(crate) fn query_size(world: &World, entity: &EntityRef, mut args: QueryArgs) .unwrap_or((Vec2::ZERO, Vec2::ZERO, SizingHints::default())); // If intrinsic_min_size > max_size we overflow, but respect the minimum size nonetheless - args.limits.min_size = args.limits.min_size.max(instrisic_min_size); + limits.min_size = limits.min_size.max(instrisic_min_size); let size = intrinsic_size.max(resolved_size); - let min_size = instrisic_min_size.max(args.limits.min_size); + let min_size = instrisic_min_size.max(limits.min_size); Sizing { min: Rect::from_size(min_size), preferred: Rect::from_size(size), margin, hints: intrinsic_hints.combine(hints), + maximize: entity.get_copy(maximize()).unwrap_or_default(), } }; let constraints = Constraints::from_entity(entity); + if constraints.aspect_ratio.is_some() { + hints.coupled_size = true; + } + sizing.min = sizing.min.with_size(constraints.apply(sizing.min.size())); sizing.preferred = sizing .preferred @@ -429,7 +436,7 @@ pub(crate) fn query_size(world: &World, entity: &EntityRef, mut args: QueryArgs) cache.insert_query( args.direction, - CachedValue::new(args.limits, args.content_area, sizing), + CachedValue::new(limits, args.content_area, sizing), ); sizing @@ -444,7 +451,7 @@ pub(crate) fn apply_layout( entity: &EntityRef, // The size of the potentially available space for the subtree content_area: Vec2, - mut limits: LayoutLimits, + limits: LayoutLimits, ) -> Block { puffin::profile_function!(format!("{entity}")); // assert!(limits.min_size.x <= limits.max_size.x); @@ -467,17 +474,18 @@ pub(crate) fn apply_layout( let mut query = entity.query(&query); let (cache, &margin, &padding, min_size, max_size, size, size_resolver, children, layout) = query.get().unwrap(); - let min_size = min_size.resolve(content_area); - let max_size = max_size.map(|v| v.resolve(content_area)); - let external_max_size = limits.max_size; - - tracing::debug!(%min_size, ?max_size, %limits); - limits.min_size = limits.min_size.max(min_size); - limits.max_size = limits.max_size.max(limits.min_size); - if let Some(max_size) = max_size { - limits.max_size = limits.max_size.min(max_size); - } + let min_size = min_size.resolve(content_area); + let max_size = max_size + .map(|v| v.resolve(content_area)) + .unwrap_or(Vec2::INFINITY); + + let external_limits = limits; + let limits = LayoutLimits { + // Minimum size is *always* respected, even if that entails overflowing + min_size: limits.min_size.max(min_size), + max_size: limits.max_size.min(max_size), + }; // Check if cache is still valid @@ -486,8 +494,6 @@ pub(crate) fn apply_layout( tracing::debug!(%entity, %value.value.rect, %value.value.can_grow, "found valid cached layout"); return value.value; - } else { - tracing::debug!(%entity, ?value, "invalid cached layout"); } } @@ -500,12 +506,24 @@ pub(crate) fn apply_layout( let children = children.map(Vec::as_slice).unwrap_or(&[]); - let resolved_size = size.resolve(content_area); + let mut resolved_size = size.resolve(content_area); + + let maximized = entity.get_copy(maximize()).unwrap_or_default(); + + if maximized.x > 0.0 { + resolved_size.x = limits.max_size.x; + } + + if maximized.y > 0.0 { + resolved_size.y = limits.max_size.y; + } + + let can_maximize = maximized.cmpgt(Vec2::ZERO); let can_grow = BVec2::new( - resolved_size.x > external_max_size.x, - resolved_size.y > external_max_size.y, - ); + resolved_size.x > external_limits.max_size.x, + resolved_size.y > external_limits.max_size.y, + ) | can_maximize; // tracing::trace!(%entity, ?resolved_size, ?external_max_size, %can_grow); @@ -628,10 +646,11 @@ impl Constraints { fn apply(&self, mut size: Vec2) -> Vec2 { if let Some(aspect_ratio) = self.aspect_ratio { if aspect_ratio > 0.0 { - if size.x > size.y { - size = vec2(size.y * aspect_ratio, size.y); + // > 1.0 means width > height + if aspect_ratio > 1.0 { + size = vec2(size.x, size.y / aspect_ratio); } else { - size = vec2(size.x, size.x / aspect_ratio); + size = vec2(size.y * aspect_ratio, size.y); } } } diff --git a/violet-core/src/layout/stack.rs b/violet-core/src/layout/stack.rs index 203e471..e16879e 100644 --- a/violet-core/src/layout/stack.rs +++ b/violet-core/src/layout/stack.rs @@ -9,7 +9,7 @@ use crate::{ }; use super::{ - resolve_pos, apply_layout, Alignment, Block, Direction, LayoutLimits, QueryArgs, Sizing, + apply_layout, resolve_pos, Alignment, Block, Direction, LayoutLimits, QueryArgs, Sizing, }; #[derive(Debug)] @@ -185,6 +185,7 @@ impl StackLayout { let mut preferred_bounds = StackableBounds::from_rect(min_rect); let mut hints = SizingHints::default(); + let mut maximize = Vec2::ZERO; for &child in children.iter() { let entity = world.entity(child).expect("invalid child"); @@ -202,6 +203,8 @@ impl StackLayout { }, ); + maximize = maximize + sizing.maximize; + hints = hints.combine(sizing.hints); min_bounds = min_bounds.merge(&StackableBounds::new(sizing.min, sizing.margin)); @@ -220,6 +223,7 @@ impl StackLayout { // .clamp_size(limits.min_size, limits.max_size), margin: min_margin.max(preferred_margin), hints, + maximize, } } } diff --git a/violet-core/src/scope.rs b/violet-core/src/scope.rs index e63fd9e..0c41c15 100644 --- a/violet-core/src/scope.rs +++ b/violet-core/src/scope.rs @@ -257,4 +257,8 @@ impl Effect>> Effect for ScopedEffect { Poll::Ready(()) } } + + fn label(&self) -> Option<&str> { + self.effect.label() + } } diff --git a/violet-core/src/style/mod.rs b/violet-core/src/style/mod.rs index c2d2925..3b09902 100644 --- a/violet-core/src/style/mod.rs +++ b/violet-core/src/style/mod.rs @@ -8,15 +8,15 @@ use glam::Vec2; use palette::{IntoColor, Oklab, Srgba}; use crate::{ - components::{color, draw_shape, margin, max_size, min_size, padding, size}, + components::{color, draw_shape, margin, max_size, maximize, min_size, padding, size}, shape::shape_rectangle, unit::Unit, 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, + EERIE_BLACK_300, EERIE_BLACK_700, EERIE_BLACK_DEFAULT, JADE_400, JADE_600, JADE_DEFAULT, + LION_DEFAULT, PLATINUM_DEFAULT, REDWOOD_DEFAULT, }; #[macro_export] @@ -57,6 +57,7 @@ pub struct WidgetSize { pub max_size: Option>, pub margin: Option>, pub padding: Option>, + pub maximize: Option, } impl WidgetSize { @@ -75,7 +76,8 @@ impl WidgetSize { .set_opt(padding(), p) .set_opt(size(), self.size) .set_opt(min_size(), self.min_size) - .set_opt(max_size(), self.max_size); + .set_opt(max_size(), self.max_size) + .set_opt(maximize(), self.maximize); } /// Set the size @@ -107,6 +109,12 @@ impl WidgetSize { self.padding = Some(padding.into()); self } + + /// Maximize the widget to the available size with the given weight. + pub fn with_maximize(mut self, maximize: Vec2) -> Self { + self.maximize = Some(maximize); + self + } } /// A widget that allows you to set its sizing properties @@ -170,6 +178,14 @@ pub trait SizeExt { self } + fn with_maximize(mut self, maximize: Vec2) -> Self + where + Self: Sized, + { + self.size_mut().maximize = Some(maximize); + self + } + fn size_mut(&mut self) -> &mut WidgetSize; } @@ -211,10 +227,7 @@ impl ValueOrRef { pub(crate) fn resolve(self, stylesheet: EntityRef<'_>) -> T { match self { ValueOrRef::Value(value) => value, - ValueOrRef::Ref(component) => { - let value = stylesheet.get_copy(component).unwrap(); - value - } + ValueOrRef::Ref(component) => stylesheet.get_copy(component).unwrap(), } } } diff --git a/violet-core/src/utils.rs b/violet-core/src/utils.rs index e6ff011..334013a 100644 --- a/violet-core/src/utils.rs +++ b/violet-core/src/utils.rs @@ -1,6 +1,6 @@ use std::task::Poll; -use futures::Stream; +use futures::{ready, Future, Stream}; #[macro_export] macro_rules! to_owned { @@ -86,3 +86,62 @@ where } } } + +#[pin_project::pin_project] +pub struct Throttle { + #[pin] + stream: S, + #[pin] + fut: Option, + throttle: C, +} + +impl Throttle { + pub fn new(stream: S, throttle: C) -> Self { + Self { + stream, + fut: None, + throttle, + } + } +} + +impl Stream for Throttle +where + S: Stream, + F: Future, + C: FnMut() -> F, +{ + type Item = S::Item; + + fn poll_next( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + let mut p = self.project(); + + if let Some(fut) = p.fut.as_mut().as_pin_mut() { + ready!(fut.poll(cx)); + p.fut.set(None); + } + + let item = ready!(p.stream.poll_next(cx)); + + if let Some(item) = item { + p.fut.set(Some((p.throttle)())); + Poll::Ready(Some(item)) + } else { + Poll::Ready(None) + } + } +} + +/// Throttles a stream with the provided future +pub fn throttle(stream: S, throttle: C) -> Throttle +where + S: Stream, + F: Future, + C: FnMut() -> F, +{ + Throttle::new(stream, throttle) +} diff --git a/violet-core/src/widget/basic.rs b/violet-core/src/widget/basic.rs index 8634489..e44b539 100644 --- a/violet-core/src/widget/basic.rs +++ b/violet-core/src/widget/basic.rs @@ -8,7 +8,10 @@ use crate::{ self, aspect_ratio, color, draw_shape, font_size, min_size, size, text, text_wrap, }, shape, - style::{spacing_large, spacing_small, SizeExt, StyleExt, ValueOrRef, WidgetSize}, + style::{ + colors::REDWOOD_DEFAULT, spacing_large, spacing_small, SizeExt, StyleExt, ValueOrRef, + WidgetSize, + }, text::{TextSegment, Wrap}, unit::Unit, Scope, Widget, @@ -50,11 +53,22 @@ impl SizeExt for Rectangle { pub struct Image { image: K, + size: WidgetSize, + aspect_ratio: Option, } impl Image { pub fn new(image: K) -> Self { - Self { image } + Self { + image, + size: Default::default(), + aspect_ratio: None, + } + } + + pub fn with_aspect_ratio(mut self, aspect_ratio: f32) -> Self { + self.aspect_ratio = Some(aspect_ratio); + self } } @@ -65,18 +79,26 @@ where fn mount(self, scope: &mut Scope) { let image = scope.assets_mut().try_load(&self.image).ok(); if let Some(image) = image { + self.size.mount(scope); scope .set(color(), Srgba::new(1.0, 1.0, 1.0, 1.0)) .set(draw_shape(shape::shape_rectangle()), ()) - .set(components::image(), image); + .set(components::image(), image) + .set_opt(components::aspect_ratio(), self.aspect_ratio); } else { - Text::new("Image not found") - .with_color(Srgba::new(1.0, 0.0, 0.0, 1.0)) + label("Image not found") + .with_color(REDWOOD_DEFAULT) .mount(scope); } } } +impl SizeExt for Image { + fn size_mut(&mut self) -> &mut WidgetSize { + &mut self.size + } +} + /// Style and decorate text pub struct TextStyle { font_size: f32, @@ -204,48 +226,3 @@ where scope.set(components::offset(), self.offset); } } - -pub struct BoxSized { - size: Unit, - min_size: Unit, - aspect_ratio: f32, - widget: W, -} - -impl BoxSized { - pub fn new(widget: W) -> Self { - Self { - size: Unit::ZERO, - min_size: Unit::ZERO, - widget, - aspect_ratio: 0.0, - } - } - - pub fn with_size(mut self, size: Unit) -> Self { - self.size = size; - self - } - - pub fn with_min_size(mut self, min_size: Unit) -> Self { - self.min_size = min_size; - self - } - - /// Set the aspect ratio - pub fn with_aspect_ratio(mut self, aspect_ratio: f32) -> Self { - self.aspect_ratio = aspect_ratio; - self - } -} - -impl Widget for BoxSized { - fn mount(self, scope: &mut Scope<'_>) { - self.widget.mount(scope); - - scope - .set(size(), self.size) - .set(min_size(), self.min_size) - .set(aspect_ratio(), self.aspect_ratio); - } -} diff --git a/violet-core/src/widget/container.rs b/violet-core/src/widget/container.rs index 7ef8743..3b0609d 100644 --- a/violet-core/src/widget/container.rs +++ b/violet-core/src/widget/container.rs @@ -3,16 +3,15 @@ use glam::Vec2; use winit::event::ElementState; use crate::{ - components::{anchor, layout, margin, max_size, min_size, offset, padding, rect}, + components::{anchor, layout, offset, rect}, input::{focusable, on_cursor_move, on_mouse_input}, layout::{Alignment, Direction, FlowLayout, Layout, StackLayout}, style::{ - colors::{EERIE_BLACK_300, EERIE_BLACK_400}, primary_background, secondary_background, spacing_medium, spacing_small, Background, SizeExt, StyleExt, WidgetSize, }, unit::Unit, - Edges, Frame, Scope, Widget, WidgetCollection, + Frame, Scope, Widget, WidgetCollection, }; /// Style for most container type widgets. diff --git a/violet-core/src/widget/future.rs b/violet-core/src/widget/future.rs index afb72e1..dec5434 100644 --- a/violet-core/src/widget/future.rs +++ b/violet-core/src/widget/future.rs @@ -1,7 +1,7 @@ use futures::{Future, Stream}; use futures_signals::signal::{self, SignalExt}; -use crate::{components::layout, layout::Layout, FutureEffect, Scope, StreamEffect, Widget}; +use crate::{effect::Effect, FutureEffect, Scope, StreamEffect, Widget}; pub struct SignalWidget(pub S); @@ -15,25 +15,26 @@ impl SignalWidget { } } -impl Widget for SignalWidget +impl Widget for SignalWidget where - S: 'static + signal::Signal, - W: Widget, + S: 'static + signal::Signal, + S::Item: Widget, { fn mount(self, scope: &mut crate::Scope<'_>) { let mut child = None; let stream = self.0.to_stream(); + let label = std::any::type_name::(); - scope.spawn_effect(StreamEffect::new( - stream, - move |scope: &mut Scope<'_>, v| { + scope.spawn_effect( + StreamEffect::new(stream, move |scope: &mut Scope<'_>, v| { if let Some(child) = child { scope.detach(child); } child = Some(scope.attach(v)); - }, - )); + }) + .with_label(label), + ); } } @@ -42,24 +43,31 @@ where S: Stream, S::Item: Widget; -impl Widget for StreamWidget +impl Widget for StreamWidget where - S: 'static + Stream, - W: Widget, + S: 'static + Stream, + S::Item: Widget, { fn mount(self, scope: &mut crate::Scope<'_>) { let mut child = None; - scope.spawn_effect(StreamEffect::new( - self.0, - move |scope: &mut Scope<'_>, v| { + let label = std::any::type_name::(); + + scope.spawn_effect( + StreamEffect::new(self.0, move |scope: &mut Scope<'_>, v| { + puffin::profile_scope!("StreamWidget::mount", "update child widget"); if let Some(child) = child { + puffin::profile_scope!("detach"); scope.detach(child); } - child = Some(scope.attach(v)); - }, - )); + { + puffin::profile_scope!("attach"); + child = Some(scope.attach(v)); + } + }) + .with_label(label), + ); } } @@ -68,17 +76,18 @@ where S: Future, S::Output: Widget; -impl Widget for FutureWidget +impl Widget for FutureWidget where - S: 'static + Future, - W: Widget, + S: 'static + Future, + S::Output: Widget, { fn mount(self, scope: &mut crate::Scope<'_>) { - scope.spawn_effect(FutureEffect::new( - self.0, - move |scope: &mut Scope<'_>, v| { + let label = std::any::type_name::(); + scope.spawn_effect( + FutureEffect::new(self.0, move |scope: &mut Scope<'_>, v| { scope.attach(v); - }, - )); + }) + .with_label(label), + ); } } diff --git a/violet-core/src/widget/interactive/button.rs b/violet-core/src/widget/interactive/button.rs index 77e6332..b743fb0 100644 --- a/violet-core/src/widget/interactive/button.rs +++ b/violet-core/src/widget/interactive/button.rs @@ -1,5 +1,3 @@ -use futures::Stream; -use futures_signals::signal::Mutable; use palette::Srgba; use winit::event::{ElementState, MouseButton}; @@ -7,14 +5,13 @@ use crate::{ components::{self, color}, input::{focusable, on_mouse_input}, layout::Alignment, - state::{StateDuplex, StateMut, StateStream, WatchState}, + state::{StateDuplex, StateStream, WatchState}, style::{ danger_item, interactive_inactive, interactive_pressed, spacing_medium, success_item, warning_item, Background, SizeExt, StyleExt, ValueOrRef, WidgetSize, }, - to_owned, unit::Unit, - widget::{ContainerStyle, Rectangle, Stack, Text}, + widget::{ContainerStyle, Stack, Text}, Frame, Scope, Widget, }; diff --git a/violet-core/src/widget/interactive/input.rs b/violet-core/src/widget/interactive/input.rs index 66ad76c..69144ca 100644 --- a/violet-core/src/widget/interactive/input.rs +++ b/violet-core/src/widget/interactive/input.rs @@ -6,6 +6,7 @@ use futures_signals::signal::{self, Mutable, SignalExt}; use glam::{vec2, Vec2}; use itertools::Itertools; use palette::Srgba; +use web_time::Duration; use winit::{ event::ElementState, keyboard::{Key, NamedKey}, @@ -21,8 +22,10 @@ use crate::{ ValueOrRef, WidgetSize, }, text::{LayoutGlyphs, TextSegment}, + time::sleep, to_owned, unit::Unit, + utils::throttle, widget::{ row, NoOp, Positioned, Rectangle, SignalWidget, Stack, StreamWidget, Text, WidgetExt, }, @@ -114,7 +117,7 @@ impl Widget for TextInput { let mut cursor_pos = Vec2::ZERO; - let mut new_text = content.stream().fuse(); + let mut new_text = throttle(content.stream(), || sleep(Duration::from_millis(100))).fuse(); let mut focused = false; loop { diff --git a/violet-core/src/widget/interactive/slider.rs b/violet-core/src/widget/interactive/slider.rs index 03b4734..4288267 100644 --- a/violet-core/src/widget/interactive/slider.rs +++ b/violet-core/src/widget/interactive/slider.rs @@ -5,6 +5,7 @@ use futures::{stream::BoxStream, StreamExt}; use futures_signals::signal::Mutable; use glam::Vec2; use palette::Srgba; +use web_time::Duration; use winit::event::ElementState; use crate::{ @@ -13,10 +14,11 @@ use crate::{ layout::Alignment, state::{State, StateDuplex, StateStream}, style::{interactive_active, interactive_inactive, spacing_small, SizeExt, StyleExt}, + time::sleep, to_owned, unit::Unit, - utils::zip_latest, - widget::{row, BoxSized, ContainerStyle, Positioned, Rectangle, Stack, StreamWidget, Text}, + utils::{throttle, zip_latest}, + widget::{row, ContainerStyle, Positioned, Rectangle, Stack, StreamWidget, Text}, Scope, StreamEffect, Widget, }; @@ -90,7 +92,7 @@ impl Widget for Slider { let handle_size = self.style.handle_size; let track_size = self.style.track_size; - let track = scope.attach(BoxSized::new(Rectangle::new(track_color)).with_size(track_size)); + let track = scope.attach(Rectangle::new(track_color).with_size(track_size)); let min = self.min.to_progress(); let max = self.max.to_progress(); @@ -175,11 +177,9 @@ impl Widget for SliderHandle { } })); - Positioned::new( - BoxSized::new(Rectangle::new(self.handle_color)).with_min_size(self.handle_size), - ) - .with_anchor(Unit::rel2(0.5, 0.0)) - .mount(scope) + Positioned::new(Rectangle::new(self.handle_color).with_min_size(self.handle_size)) + .with_anchor(Unit::rel2(0.5, 0.0)) + .mount(scope) } } diff --git a/violet-demo/Cargo.toml b/violet-demo/Cargo.toml index 88e6b3f..1798624 100644 --- a/violet-demo/Cargo.toml +++ b/violet-demo/Cargo.toml @@ -28,3 +28,4 @@ futures.workspace = true wasm-bindgen-futures = "0.4" itertools.workspace = true tracing-tree.workspace = true +puffin.workspace = true diff --git a/violet-demo/index.html b/violet-demo/index.html index 30987a7..afa5406 100644 --- a/violet-demo/index.html +++ b/violet-demo/index.html @@ -12,7 +12,9 @@ /* This allows the flexbox to grow to max size, this is needed for WebGPU */ flex: 1; /* This forces CSS to ignore the width/height of the canvas, this is needed for WebGL */ - /* contain: size; */ + contain: size; + width: 100%; + height: 100%; background-color: #001133; } @@ -26,7 +28,7 @@ -
+
diff --git a/violet-demo/src/lib.rs b/violet-demo/src/lib.rs index 2f5fb76..dd30371 100644 --- a/violet-demo/src/lib.rs +++ b/violet-demo/src/lib.rs @@ -12,9 +12,10 @@ use violet::{ layout::Alignment, state::{DynStateDuplex, State, StateMut, StateStream, StateStreamRef}, style::{primary_background, Background, SizeExt, ValueOrRef}, + time::sleep, to_owned, unit::Unit, - utils::zip_latest_ref, + utils::{throttle, zip_latest_ref}, widget::{ card, column, label, pill, row, Button, Radio, Rectangle, SliderWithLabel, Stack, StreamWidget, Text, TextInput, WidgetExt, @@ -24,6 +25,7 @@ use violet::{ futures_signals::signal::Mutable, glam::vec2, palette::{FromColor, IntoColor, OklabHue, Oklch, Srgb}, + web_time::Duration, wgpu::renderer::RendererConfig, }; use wasm_bindgen::prelude::*; @@ -65,7 +67,7 @@ pub fn run() { setup(); violet::wgpu::App::new() - .with_renderer_config(RendererConfig { debug_mode: true }) + .with_renderer_config(RendererConfig { debug_mode: false }) .run(MainApp) .unwrap(); } @@ -108,6 +110,7 @@ impl Tints { impl Widget for Tints { fn mount(self, scope: &mut Scope<'_>) { + puffin::profile_function!(); row((1..=9) .map(|i| { let f = (i as f32) / 10.0; @@ -183,13 +186,20 @@ impl Widget for Palettes { to_owned![current_choice]; let discard = &discard; 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)), ); - card(row((checkbox, discard(i), StreamWidget(item.stream())))) + card(row(( + checkbox, + discard(i), + StreamWidget(throttle(item.stream(), || { + sleep(Duration::from_millis(100)) + })), + ))) } }) .collect_vec(); @@ -225,6 +235,7 @@ pub struct PaletteColor { impl Widget for PaletteColor { fn mount(self, scope: &mut Scope<'_>) { + puffin::profile_function!(); Stack::new(( row((Tints::new(self.color, self.falloff),)), pill(label(color_hex(self.color))), @@ -259,7 +270,8 @@ impl Widget for PaletteEditor { let color_rect = color.stream().map(|v| { Rectangle::new(ValueOrRef::value(v.into_color())) - .with_size(Unit::new(vec2(0.0, 100.0), vec2(1.0, 0.0))) + .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") }); diff --git a/violet-wgpu/src/app.rs b/violet-wgpu/src/app.rs index 31f5054..56440b2 100644 --- a/violet-wgpu/src/app.rs +++ b/violet-wgpu/src/app.rs @@ -207,10 +207,7 @@ impl App { stats.record_frame(frame_time); - { - puffin::profile_scope!("Tick"); - ex.tick(&mut frame); - } + ex.tick(&mut frame); update_animations(&mut frame, cur_time - start_time); diff --git a/violet-wgpu/src/renderer/debug_renderer.rs b/violet-wgpu/src/renderer/debug_renderer.rs index 330133a..2b9cffb 100644 --- a/violet-wgpu/src/renderer/debug_renderer.rs +++ b/violet-wgpu/src/renderer/debug_renderer.rs @@ -139,7 +139,7 @@ impl DebugRenderer { let mut query = query.borrow(&frame.world); let clamped_indicators = query.iter().filter_map(|(entity, v)| { - let clamped_query_vertical = if v + let can_grow_vert = if v .get_query(Direction::Vertical) .as_ref() .is_some_and(|v| v.value.hints.can_grow.any()) @@ -149,7 +149,7 @@ impl DebugRenderer { Vec3::ZERO }; - let clamped_query_horizontal = if v + let can_grow_hor = if v .get_query(Direction::Horizontal) .as_ref() .is_some_and(|v| v.value.hints.can_grow.any()) @@ -159,19 +159,13 @@ impl DebugRenderer { Vec3::ZERO }; - let clamped_layout = if v.layout().is_some_and(|v| v.value.can_grow.any()) { + let can_grow = if v.layout().is_some_and(|v| v.value.can_grow.any()) { vec3(0.0, 0.0, 0.5) } else { Vec3::ZERO }; - let color: Vec3 = [ - clamped_query_vertical, - clamped_query_horizontal, - clamped_layout, - ] - .into_iter() - .sum(); + let color: Vec3 = [can_grow_vert, can_grow_hor, can_grow].into_iter().sum(); if color == Vec3::ZERO { None From c516af0605f09fbb47096444fca7a375b13ca956 Mon Sep 17 00:00:00 2001 From: Freja Roberts Date: Sun, 17 Mar 2024 00:53:25 +0100 Subject: [PATCH 07/16] wip: app redesign --- recipes.json | 6 +- violet-core/src/layout/cache.rs | 2 +- violet-core/src/layout/mod.rs | 63 +----- violet-core/src/layout/stack.rs | 6 +- violet-core/src/state/dedup.rs | 7 +- violet-core/src/systems.rs | 2 - violet-core/src/time/mod.rs | 8 +- violet-core/src/types.rs | 2 +- violet-core/src/widget/basic.rs | 9 +- violet-core/src/widget/interactive/slider.rs | 4 +- violet-demo/Cargo.toml | 3 + violet-wgpu/src/app.rs | 211 ++++++++++++------- violet-wgpu/src/graphics/gpu.rs | 1 - violet-wgpu/src/graphics/texture.rs | 2 +- violet-wgpu/src/renderer/debug_renderer.rs | 1 - violet-wgpu/src/renderer/mod.rs | 1 - violet-wgpu/src/renderer/window_renderer.rs | 9 +- violet-wgpu/src/text.rs | 2 +- 18 files changed, 161 insertions(+), 178 deletions(-) diff --git a/recipes.json b/recipes.json index d8affb8..050b0e5 100644 --- a/recipes.json +++ b/recipes.json @@ -55,7 +55,11 @@ ] }, "build web": { - "cmd": "wasm-pack build --target web", + "cmd": "wasm-pack build --target web --dev", + "cwd": "./violet-demo/" + }, + "build web profile": { + "cmd": "wasm-pack build --target web --profiling", "cwd": "./violet-demo/" }, "host": { diff --git a/violet-core/src/layout/cache.rs b/violet-core/src/layout/cache.rs index ab2fa1b..d626ca3 100644 --- a/violet-core/src/layout/cache.rs +++ b/violet-core/src/layout/cache.rs @@ -69,7 +69,7 @@ impl LayoutCache { pub(crate) fn insert_query_row(&mut self, value: CachedValue) { self.query_row = Some(value); if let Some(f) = self.on_invalidated.as_ref() { - // f(LayoutUpdate::SizeQueryUpdate) + f(LayoutUpdate::SizeQueryUpdate) } } diff --git a/violet-core/src/layout/mod.rs b/violet-core/src/layout/mod.rs index 0b07e8a..78f2332 100644 --- a/violet-core/src/layout/mod.rs +++ b/violet-core/src/layout/mod.rs @@ -12,7 +12,7 @@ use crate::{ self, anchor, aspect_ratio, children, layout, max_size, maximize, min_size, offset, padding, size, size_resolver, }, - layout::cache::{validate_cached_layout, validate_cached_query, CachedValue, LAYOUT_TOLERANCE}, + layout::cache::{validate_cached_layout, validate_cached_query, CachedValue}, Edges, Rect, }; @@ -223,67 +223,6 @@ impl SizingHints { } } -fn validate_sizing(entity: &EntityRef, sizing: &Sizing, limits: LayoutLimits) { - const TOLERANCE: f32 = 0.2; - if sizing.min.size().x > limits.max_size.x + TOLERANCE - || sizing.min.size().y > limits.max_size.y + TOLERANCE - { - tracing::error!( - %entity, - min_size = %sizing.min.size(), - max_size = %limits.max_size, - "Minimum size exceeds size limit", - ); - } - - if sizing.preferred.size().x > limits.max_size.x + TOLERANCE - || sizing.preferred.size().y > limits.max_size.y + TOLERANCE - { - tracing::error!( - %entity, - preferred_size = %sizing.preferred.size(), - ?limits, - "Preferred size exceeds size limit", - ); - } - - if sizing.min.size().x + TOLERANCE < limits.min_size.x - || sizing.min.size().y + TOLERANCE < limits.min_size.y - { - tracing::error!( - %entity, - min_size = %sizing.min.size(), - ?limits, - "Minimum size is less than size limit", - ); - } -} - -fn validate_block(entity: &EntityRef, block: &Block, limits: LayoutLimits) { - const TOLERANCE: f32 = 0.2; - if block.rect.size().x > limits.max_size.x + TOLERANCE - || block.rect.size().y > limits.max_size.y + TOLERANCE - { - tracing::error!( - %entity, - rect_size = %block.rect.size(), - max_size = %limits.max_size, - "Widget size exceeds size limit", - ); - } - - if block.rect.size().x + TOLERANCE < limits.min_size.x - || block.rect.size().y + TOLERANCE < limits.min_size.y - { - tracing::error!( - %entity, - rect_size = %block.rect.size(), - min_size = %limits.min_size, - "Widget size is less than size limit", - ); - } -} - pub(crate) fn query_size(world: &World, entity: &EntityRef, args: QueryArgs) -> Sizing { puffin::profile_function!(format!("{entity} {args:?}")); // assert!(limits.min_size.x <= limits.max_size.x); diff --git a/violet-core/src/layout/stack.rs b/violet-core/src/layout/stack.rs index e16879e..a674fef 100644 --- a/violet-core/src/layout/stack.rs +++ b/violet-core/src/layout/stack.rs @@ -8,9 +8,7 @@ use crate::{ Edges, Rect, }; -use super::{ - apply_layout, resolve_pos, Alignment, Block, Direction, LayoutLimits, QueryArgs, Sizing, -}; +use super::{apply_layout, resolve_pos, Alignment, Block, LayoutLimits, QueryArgs, Sizing}; #[derive(Debug)] pub struct StackableBounds { @@ -203,7 +201,7 @@ impl StackLayout { }, ); - maximize = maximize + sizing.maximize; + maximize += sizing.maximize; hints = hints.combine(sizing.hints); diff --git a/violet-core/src/state/dedup.rs b/violet-core/src/state/dedup.rs index 2e43da1..e9172fd 100644 --- a/violet-core/src/state/dedup.rs +++ b/violet-core/src/state/dedup.rs @@ -1,9 +1,6 @@ -use std::{future::ready, sync::Arc}; +use std::future::ready; -use futures::{FutureExt, StreamExt}; -use futures_signals::signal::{Mutable, SignalExt}; -use parking_lot::Mutex; -use tracing::info; +use futures::StreamExt; use super::{State, StateSink, StateStream, StateStreamRef}; diff --git a/violet-core/src/systems.rs b/violet-core/src/systems.rs index 2e88154..2070e83 100644 --- a/violet-core/src/systems.rs +++ b/violet-core/src/systems.rs @@ -11,12 +11,10 @@ use flax::{ entity_ids, events::{EventData, EventSubscriber}, filter::Or, - sink::Sink, BoxedSystem, CommandBuffer, Dfs, DfsBorrow, Entity, Fetch, FetchExt, FetchItem, Query, QueryBorrow, System, World, }; use glam::Vec2; -use tracing::info; use crate::{ components::{ diff --git a/violet-core/src/time/mod.rs b/violet-core/src/time/mod.rs index 969da60..14c6bf9 100644 --- a/violet-core/src/time/mod.rs +++ b/violet-core/src/time/mod.rs @@ -11,7 +11,6 @@ use std::{ }; use futures::{ - channel::oneshot, task::{ArcWake, AtomicWaker}, Future, }; @@ -226,14 +225,17 @@ impl Timers { #[cfg(target_arch = "wasm32")] struct TickFuture { inner: Arc, - timeout: Option<(oneshot::Receiver<()>, gloo_timers::callback::Timeout)>, + timeout: Option<( + futures::channel::oneshot::Receiver<()>, + gloo_timers::callback::Timeout, + )>, } #[cfg(target_arch = "wasm32")] impl TickFuture { fn new(inner: Arc, timeout: Option) -> Self { let timeout = if let Some(timeout) = timeout { - let (tx, rx) = oneshot::channel(); + let (tx, rx) = futures::channel::oneshot::channel(); let timeout = gloo_timers::callback::Timeout::new( timeout.as_millis().try_into().unwrap(), diff --git a/violet-core/src/types.rs b/violet-core/src/types.rs index ef8e201..e696ec4 100644 --- a/violet-core/src/types.rs +++ b/violet-core/src/types.rs @@ -218,7 +218,7 @@ impl Rect { } #[must_use] - pub(crate) fn clamp_size(&self, min: Vec2, max: Vec2) -> Self { + pub fn clamp_size(&self, min: Vec2, max: Vec2) -> Self { let size = self.size().clamp(min, max); Self { min: self.min, diff --git a/violet-core/src/widget/basic.rs b/violet-core/src/widget/basic.rs index e44b539..6a2d436 100644 --- a/violet-core/src/widget/basic.rs +++ b/violet-core/src/widget/basic.rs @@ -4,14 +4,9 @@ use palette::Srgba; use crate::{ assets::AssetKey, - components::{ - self, aspect_ratio, color, draw_shape, font_size, min_size, size, text, text_wrap, - }, + components::{self, color, draw_shape, font_size, text, text_wrap}, shape, - style::{ - colors::REDWOOD_DEFAULT, spacing_large, spacing_small, SizeExt, StyleExt, ValueOrRef, - WidgetSize, - }, + style::{colors::REDWOOD_DEFAULT, spacing_small, SizeExt, StyleExt, ValueOrRef, WidgetSize}, text::{TextSegment, Wrap}, unit::Unit, Scope, Widget, diff --git a/violet-core/src/widget/interactive/slider.rs b/violet-core/src/widget/interactive/slider.rs index 4288267..718b2d9 100644 --- a/violet-core/src/widget/interactive/slider.rs +++ b/violet-core/src/widget/interactive/slider.rs @@ -5,7 +5,6 @@ use futures::{stream::BoxStream, StreamExt}; use futures_signals::signal::Mutable; use glam::Vec2; use palette::Srgba; -use web_time::Duration; use winit::event::ElementState; use crate::{ @@ -14,10 +13,9 @@ use crate::{ layout::Alignment, state::{State, StateDuplex, StateStream}, style::{interactive_active, interactive_inactive, spacing_small, SizeExt, StyleExt}, - time::sleep, to_owned, unit::Unit, - utils::{throttle, zip_latest}, + utils::zip_latest, widget::{row, ContainerStyle, Positioned, Rectangle, Stack, StreamWidget, Text}, Scope, StreamEffect, Widget, }; diff --git a/violet-demo/Cargo.toml b/violet-demo/Cargo.toml index 1798624..faa1d55 100644 --- a/violet-demo/Cargo.toml +++ b/violet-demo/Cargo.toml @@ -29,3 +29,6 @@ wasm-bindgen-futures = "0.4" itertools.workspace = true tracing-tree.workspace = true puffin.workspace = true + +[package.metadata.wasm-pack.profile.profiling] +wasm-opt = false diff --git a/violet-wgpu/src/app.rs b/violet-wgpu/src/app.rs index 56440b2..92eecf4 100644 --- a/violet-wgpu/src/app.rs +++ b/violet-wgpu/src/app.rs @@ -6,10 +6,9 @@ use flax::{components::name, Entity, Schedule, World}; use glam::{vec2, Vec2}; use parking_lot::Mutex; use winit::{ - dpi::PhysicalSize, - event::{ElementState, Event, KeyEvent, WindowEvent}, - event_loop::EventLoopBuilder, - keyboard::Key, + dpi::{LogicalSize, PhysicalSize}, + event::{Event, WindowEvent}, + event_loop::{EventLoopBuilder, EventLoopWindowTarget}, window::WindowBuilder, }; @@ -77,9 +76,9 @@ impl App { } pub fn run(self, root: impl Widget) -> anyhow::Result<()> { - let mut ex = Executor::new(); + let executor = Executor::new(); - let spawner = ex.spawner(); + let spawner = executor.spawner(); let mut frame = Frame::new(spawner, AssetCache::new(), World::new()); @@ -121,8 +120,6 @@ impl App { let window = builder.build(&event_loop)?; - let mut window_size = window.inner_size(); - let mut input_state = InputState::new(Vec2::ZERO); let stylesheet = setup_stylesheet().spawn(frame.world_mut()); @@ -130,12 +127,10 @@ impl App { // Mount the root widget let root = frame.new_root(Canvas { stylesheet, - size: vec2(window_size.width as f32, window_size.height as f32), + size: vec2(0.0, 0.0), root, }); - let mut stats = AppStats::new(60); - tracing::info!("creating gpu"); let window = Arc::new(window); @@ -162,9 +157,7 @@ impl App { } })); - let mut renderer = None; - - let mut schedule = Schedule::new() + let schedule = Schedule::new() .with_system(templating_system(root, layout_changes_tx)) .flush() .with_system(hydrate_text()) @@ -177,71 +170,57 @@ impl App { .with_system(transform_system()); let start_time = Instant::now(); - let mut cur_time = start_time; #[cfg(not(target_arch = "wasm32"))] let _puffin_server = setup_puffin(); - let mut minimized = true; - - event_loop.run(move |event, ctl| match event { + let mut instance = AppInstance { + frame, + renderer: None, + root, + scale_factor: window.scale_factor(), + stats: AppStats::new(60), + current_time: start_time, + start_time, + executor, + schedule, + window_size: window.inner_size(), + }; + + let on_event = move |event, ctl: &EventLoopWindowTarget<()>| match event { Event::AboutToWait => { puffin::profile_scope!("AboutToWait"); if let Some(mut window_renderer) = renderer_rx.try_recv().ok().flatten() { - window_renderer.resize(window_size); - renderer = Some(window_renderer); - } - - if minimized { - return; + window_renderer.resize(instance.window_size, instance.scale_factor); + instance.renderer = Some(window_renderer); } - let new_time = Instant::now(); - - let frame_time = new_time.duration_since(cur_time); - - cur_time = new_time; - - // tracing::info!(?dt, fps = 1.0 / delta_time); - - stats.record_frame(frame_time); - - ex.tick(&mut frame); - - update_animations(&mut frame, cur_time - start_time); + instance.update(); - { - puffin::profile_scope!("Schedule"); - schedule.execute_seq(&mut frame.world).unwrap(); + if let Err(err) = instance.draw() { + tracing::error!("Failed to draw to window: {err:?}"); } - if let Some(renderer) = &mut renderer { - puffin::profile_scope!("Draw"); - if let Err(err) = renderer.draw(&mut frame) { - tracing::error!("Failed to draw to window: {err:?}"); - } + if !instance.is_minimized() { + let report = instance.stats.report(); + window.set_title(&format!( + "Violet - {:>4.1?} {:>4.1?} {:>4.1?}", + report.min_frame_time, report.average_frame_time, report.max_frame_time, + )); } - - let report = stats.report(); - window.set_title(&format!( - "Violet - {:>4.1?} {:>4.1?} {:>4.1?}", - report.min_frame_time, report.average_frame_time, report.max_frame_time, - )); puffin::GlobalProfiler::lock().new_frame(); } Event::WindowEvent { window_id, event } => match event { WindowEvent::RedrawRequested => { puffin::profile_scope!("RedrawRequested"); - if let Some(renderer) = &mut renderer { - if let Err(err) = renderer.draw(&mut frame) { - tracing::error!("Failed to draw to window: {err:?}"); - } + if let Err(err) = instance.draw() { + tracing::error!("Failed to draw to window: {err:?}"); } } WindowEvent::MouseInput { state, button, .. } => { puffin::profile_scope!("MouseInput"); - input_state.on_mouse_input(&mut frame, state, button); + input_state.on_mouse_input(&mut instance.frame, state, button); } WindowEvent::ModifiersChanged(modifiers) => { puffin::profile_scope!("ModifiersChanged"); @@ -249,34 +228,26 @@ impl App { } WindowEvent::KeyboardInput { event, .. } => { puffin::profile_scope!("KeyboardInput", format!("{event:?}")); - input_state.on_keyboard_input(&mut frame, event) + input_state.on_keyboard_input(&mut instance.frame, event) } WindowEvent::CursorMoved { position, .. } => { puffin::profile_scope!("CursorMoved"); - input_state - .on_cursor_move(&mut frame, vec2(position.x as f32, position.y as f32)) + input_state.on_cursor_move( + &mut instance.frame, + vec2(position.x as f32, position.y as f32), + ) + } + WindowEvent::ScaleFactorChanged { + scale_factor: s, .. + } => { + tracing::info!("Scale factor changed to {s}"); + instance.scale_factor = s; + + let size = instance.window_size; + instance.on_resize(size); } WindowEvent::Resized(size) => { - puffin::profile_scope!("Resized"); - minimized = size.width == 0 || size.height == 0; - - window_size = size; - - frame - .world_mut() - .set( - root, - components::rect(), - Rect { - min: vec2(0.0, 0.0), - max: vec2(size.width as f32, size.height as f32), - }, - ) - .unwrap(); - - if let Some(renderer) = &mut renderer { - renderer.resize(size); - } + instance.on_resize(size); } WindowEvent::CloseRequested => { ctl.exit(); @@ -288,10 +259,90 @@ impl App { event => { tracing::trace!(?event, "Event") } - })?; + }; + + // #[cfg(not(target_arch = "wasm32"))] + // { + // event_loop.run(on_event)?; + // } + #[cfg(target_arch = "wasm32")] + { + use winit::platform::web::EventLoopExtWebSys; + event_loop.spawn(on_event); + } + + Ok(()) + } +} + +pub struct AppInstance { + frame: Frame, + renderer: Option, + root: Entity, + scale_factor: f64, + stats: AppStats, + current_time: Instant, + start_time: Instant, + executor: Executor, + schedule: Schedule, + window_size: PhysicalSize, +} + +impl AppInstance { + pub fn on_resize(&mut self, size: PhysicalSize) { + self.window_size = size; + + tracing::info!(?size, self.scale_factor, "Resizing window"); + + let logical_size: LogicalSize = size.to_logical(self.scale_factor); + + self.frame + .world_mut() + .set( + self.root, + components::rect(), + Rect::from_size(vec2(logical_size.width, logical_size.height)), + ) + .unwrap(); + + if let Some(renderer) = &mut self.renderer { + renderer.resize(size, self.scale_factor); + } + } + + pub fn update(&mut self) { + if self.is_minimized() { + return; + } + + let new_time = Instant::now(); + + let frame_time = new_time.duration_since(self.current_time); + + self.current_time = new_time; + self.stats.record_frame(frame_time); + + self.executor.tick(&mut self.frame); + + update_animations(&mut self.frame, self.current_time - self.start_time); + + { + self.schedule.execute_seq(&mut self.frame.world).unwrap(); + } + } + + pub fn draw(&mut self) -> anyhow::Result<()> { + puffin::profile_function!(); + if let Some(renderer) = &mut self.renderer { + puffin::profile_scope!("Draw"); + renderer.draw(&mut self.frame)?; + } Ok(()) } + pub fn is_minimized(&self) -> bool { + self.window_size.width == 0 || self.window_size.height == 0 + } } #[cfg(not(target_arch = "wasm32"))] diff --git a/violet-wgpu/src/graphics/gpu.rs b/violet-wgpu/src/graphics/gpu.rs index 3e6bca2..2339c2c 100644 --- a/violet-wgpu/src/graphics/gpu.rs +++ b/violet-wgpu/src/graphics/gpu.rs @@ -1,6 +1,5 @@ use std::sync::Arc; -use glam::UVec2; use wgpu::{Adapter, Backends, SurfaceConfiguration, SurfaceError, SurfaceTexture, TextureFormat}; use winit::{dpi::PhysicalSize, window::Window}; diff --git a/violet-wgpu/src/graphics/texture.rs b/violet-wgpu/src/graphics/texture.rs index 9e4510d..07f736e 100644 --- a/violet-wgpu/src/graphics/texture.rs +++ b/violet-wgpu/src/graphics/texture.rs @@ -61,7 +61,7 @@ impl Texture { // The layout of the texture wgpu::ImageDataLayout { offset: 0, - bytes_per_row: Some(format.block_size(None).unwrap() * dimensions.0), + bytes_per_row: Some(format.block_copy_size(None).unwrap() * dimensions.0), rows_per_image: Some(dimensions.1), }, size, diff --git a/violet-wgpu/src/renderer/debug_renderer.rs b/violet-wgpu/src/renderer/debug_renderer.rs index 2b9cffb..2528fb4 100644 --- a/violet-wgpu/src/renderer/debug_renderer.rs +++ b/violet-wgpu/src/renderer/debug_renderer.rs @@ -4,7 +4,6 @@ use flax::{fetch::entity_refs, Entity, Query}; use glam::{vec2, vec3, vec4, Mat4, Quat, Vec3, Vec4}; use image::DynamicImage; use itertools::Itertools; -use palette::bool_mask::Select; use violet_core::{ assets::Asset, components::screen_rect, diff --git a/violet-wgpu/src/renderer/mod.rs b/violet-wgpu/src/renderer/mod.rs index dc4052c..fb53254 100644 --- a/violet-wgpu/src/renderer/mod.rs +++ b/violet-wgpu/src/renderer/mod.rs @@ -2,7 +2,6 @@ use std::sync::Arc; use bytemuck::Zeroable; use flax::{ - components::child_of, entity_ids, fetch::{entity_refs, EntityRefs, NthRelation}, CommandBuffer, Component, Entity, EntityRef, Fetch, Query, QueryBorrow, RelationExt, World, diff --git a/violet-wgpu/src/renderer/window_renderer.rs b/violet-wgpu/src/renderer/window_renderer.rs index 4297d87..08dad6d 100644 --- a/violet-wgpu/src/renderer/window_renderer.rs +++ b/violet-wgpu/src/renderer/window_renderer.rs @@ -6,7 +6,7 @@ use glam::Mat4; use parking_lot::Mutex; use puffin::profile_scope; use wgpu::{Operations, RenderPassDescriptor, StoreOp, SurfaceError}; -use winit::dpi::PhysicalSize; +use winit::dpi::{LogicalSize, PhysicalSize}; use violet_core::{layout::cache::LayoutUpdate, Frame}; @@ -51,9 +51,10 @@ impl WindowRenderer { } } - pub fn resize(&mut self, new_size: PhysicalSize) { - let w = new_size.width as f32; - let h = new_size.height as f32; + pub fn resize(&mut self, new_size: PhysicalSize, scale_factor: f64) { + let logical_size: LogicalSize = new_size.to_logical(scale_factor); + let w = logical_size.width; + let h = logical_size.height; self.ctx.globals.projview = Mat4::orthographic_lh(0.0, w, h, 0.0, 0.0, 1000.0); self.ctx diff --git a/violet-wgpu/src/text.rs b/violet-wgpu/src/text.rs index 6649adf..057a026 100644 --- a/violet-wgpu/src/text.rs +++ b/violet-wgpu/src/text.rs @@ -10,7 +10,7 @@ use parking_lot::Mutex; use violet_core::{ components::font_size, - layout::{Direction, LayoutLimits, QueryArgs, SizeResolver, SizingHints}, + layout::{LayoutLimits, QueryArgs, SizeResolver, SizingHints}, text::{LayoutGlyphs, LayoutLineGlyphs, TextSegment}, Rect, }; From 45d7210ede169238f434e79a920100dc238dc7d6 Mon Sep 17 00:00:00 2001 From: Freja Roberts Date: Mon, 18 Mar 2024 19:54:10 +0100 Subject: [PATCH 08/16] feat: async save/load system for demo --- .github/workflows/rust.yml | 8 + .gitmodules | 3 + Cargo.lock | 768 +++++++++++++++++++- Cargo.toml | 10 +- examples/basic.rs | 2 +- examples/color.rs | 2 +- examples/counter.rs | 2 +- examples/flow.rs | 2 +- examples/row.rs | 2 +- examples/sizing.rs | 2 +- flax | 1 + violet-core/src/atom.rs | 19 +- violet-core/src/components.rs | 6 +- violet-core/src/state/constant.rs | 28 + violet-core/src/state/mod.rs | 1 + violet-core/src/widget/interactive/input.rs | 4 +- violet-demo/Cargo.toml | 6 + violet-demo/src/lib.rs | 401 ++++++++-- violet-wgpu/src/app.rs | 36 +- violet-wgpu/src/lib.rs | 2 +- violet-wgpu/src/renderer/rect_renderer.rs | 20 +- 21 files changed, 1199 insertions(+), 126 deletions(-) create mode 160000 flax create mode 100644 violet-core/src/state/constant.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 8780524..36d1c99 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -8,6 +8,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + with: + submodules: true - uses: actions-rs/toolchain@v1 with: profile: minimal @@ -22,6 +24,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + with: + submodules: true - name: Install nightly uses: actions-rs/toolchain@v1 with: @@ -40,6 +44,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + with: + submodules: true - name: Install nightly uses: actions-rs/toolchain@v1 with: @@ -61,6 +67,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + with: + submodules: true - uses: actions-rs/toolchain@v1 with: profile: minimal diff --git a/.gitmodules b/.gitmodules index e69de29..3eb6e5e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "flax"] + path = flax + url = https://github.com/ten3roberts/flax diff --git a/Cargo.lock b/Cargo.lock index d9f3264..b9c8b0e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -139,6 +139,188 @@ dependencies = [ "libloading 0.7.4", ] +[[package]] +name = "ashpd" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd884d7c72877a94102c3715f3b1cd09ff4fac28221add3e57cfbe25c236d093" +dependencies = [ + "async-fs", + "async-net", + "enumflags2", + "futures-channel", + "futures-util", + "rand", + "serde", + "serde_repr", + "url", + "zbus", +] + +[[package]] +name = "async-broadcast" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258b52a1aa741b9f09783b2d86cf0aeeb617bbf847f6933340a39644227acbdb" +dependencies = [ + "event-listener 5.2.0", + "event-listener-strategy 0.5.0", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28243a43d821d11341ab73c80bed182dc015c514b951616cf79bd4af39af0c3" +dependencies = [ + "concurrent-queue", + "event-listener 5.2.0", + "event-listener-strategy 0.5.0", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ae5ebefcc48e7452b4987947920dac9450be1110cadf34d1b8c116bdbaf97c" +dependencies = [ + "async-lock 3.3.0", + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc19683171f287921f2405677dd2ed2549c3b3bda697a563ebc3a121ace2aba1" +dependencies = [ + "async-lock 3.3.0", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcccb0f599cfa2f8ace422d3555572f47424da5648a4382a9dd0310ff8210884" +dependencies = [ + "async-lock 3.3.0", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener 2.5.3", +] + +[[package]] +name = "async-lock" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" +dependencies = [ + "event-listener 4.0.3", + "event-listener-strategy 0.4.0", + "pin-project-lite", +] + +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-process" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451e3cf68011bd56771c79db04a9e333095ab6349f7e47592b788e9b98720cc8" +dependencies = [ + "async-channel", + "async-io", + "async-lock 3.3.0", + "async-signal", + "blocking", + "cfg-if", + "event-listener 5.2.0", + "futures-lite", + "rustix", + "windows-sys 0.52.0", +] + +[[package]] +name = "async-recursion" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30c5ef0ede93efbf733c1a727f3b6b5a1060bbedd5600183e66f6e4be4af0ec5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "async-signal" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e47d90f65a225c4527103a8d747001fc56e375203592b25ad103e1ca13124c5" +dependencies = [ + "async-io", + "async-lock 2.8.0", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.48.0", +] + +[[package]] +name = "async-task" +version = "4.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799" + +[[package]] +name = "async-trait" +version = "0.1.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "461abc97219de0eaaf81fe3ef974a540158f3d079c2ab200f891f1a2ef201e85" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -226,6 +408,15 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "block-sys" version = "0.2.1" @@ -245,6 +436,22 @@ dependencies = [ "objc2", ] +[[package]] +name = "blocking" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" +dependencies = [ + "async-channel", + "async-lock 3.3.0", + "async-task", + "fastrand", + "futures-io", + "futures-lite", + "piper", + "tracing", +] + [[package]] name = "bumpalo" version = "3.14.0" @@ -353,12 +560,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ecdffb913a326b6c642290a0d0ec8e8d6597291acdc07cc4c9cb4b3635d44cf9" -[[package]] -name = "color_quant" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" - [[package]] name = "com" version = "0.6.0" @@ -482,6 +683,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.3.2" @@ -525,6 +735,16 @@ version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "cursor-icon" version = "1.1.0" @@ -544,6 +764,27 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "discard" version = "1.0.4" @@ -577,6 +818,33 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + +[[package]] +name = "enumflags2" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3278c9d5fb675e0a51dabcf4c0d355f692b064171535ba72361be1528a9d8e8d" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -602,12 +870,66 @@ dependencies = [ "num-traits", ] +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b5fb89194fa3cad959b833185b3063ba881dbfc7030680b314250779fb4cc91" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" +dependencies = [ + "event-listener 4.0.3", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feedafcaa9b749175d5ac357452a9d41ea2911da598fde46ce1fe02c37751291" +dependencies = [ + "event-listener 5.2.0", + "pin-project-lite", +] + [[package]] name = "fast-srgb8" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + [[package]] name = "fdeflate" version = "0.3.4" @@ -630,7 +952,6 @@ dependencies = [ [[package]] name = "flax" version = "0.6.2" -source = "git+https://github.com/ten3roberts/flax#b75a7275d9a119309d3d7f41d824347dc2f9806b" dependencies = [ "anyhow", "atomic_refcell", @@ -648,10 +969,9 @@ dependencies = [ [[package]] name = "flax-derive" version = "0.6.0" -source = "git+https://github.com/ten3roberts/flax#b75a7275d9a119309d3d7f41d824347dc2f9806b" dependencies = [ "itertools 0.11.0", - "proc-macro-crate", + "proc-macro-crate 2.0.1", "proc-macro2", "quote", "syn 2.0.48", @@ -725,6 +1045,15 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + [[package]] name = "funty" version = "2.0.0" @@ -792,6 +1121,19 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +[[package]] +name = "futures-lite" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.30" @@ -849,6 +1191,16 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "gensym" version = "0.1.1" @@ -903,9 +1255,9 @@ dependencies = [ [[package]] name = "glam" -version = "0.24.2" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5418c17512bdf42730f9032c74e1ae39afc408745ebb2acf72fbc4691c17945" +checksum = "151665d9be52f9bb40fc7966565d39666f2d1e69233571b71b87791c7e0528b3" dependencies = [ "bytemuck", ] @@ -1028,6 +1380,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hexf-parse" version = "0.2.1" @@ -1045,18 +1403,28 @@ dependencies = [ "objc2", ] +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "image" -version = "0.24.8" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "034bbe799d1909622a74d1193aa50147769440040ff36cb2baa947609b0a4e23" +checksum = "a9b4f005360d32e9325029b38ba47ebd7a56f3316df09249368939562d518645" dependencies = [ "bytemuck", "byteorder", - "color_quant", - "jpeg-decoder", "num-traits", "png", + "zune-core", + "zune-jpeg", ] [[package]] @@ -1067,6 +1435,7 @@ checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", "hashbrown", + "serde", ] [[package]] @@ -1124,12 +1493,6 @@ dependencies = [ "libc", ] -[[package]] -name = "jpeg-decoder" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" - [[package]] name = "js-sys" version = "0.3.67" @@ -1275,6 +1638,15 @@ dependencies = [ "libc", ] +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + [[package]] name = "metal" version = "0.27.0" @@ -1371,6 +1743,18 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags 2.4.2", + "cfg-if", + "libc", + "memoffset", +] + [[package]] name = "nom" version = "7.1.3" @@ -1424,7 +1808,7 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 2.0.1", "proc-macro2", "quote", "syn 2.0.48", @@ -1440,6 +1824,17 @@ dependencies = [ "objc_exception", ] +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + [[package]] name = "objc-sys" version = "0.3.2" @@ -1471,6 +1866,15 @@ dependencies = [ "cc", ] +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + [[package]] name = "object" version = "0.32.2" @@ -1495,6 +1899,16 @@ dependencies = [ "libredox", ] +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "overload" version = "0.1.1" @@ -1532,6 +1946,12 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "parking" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" + [[package]] name = "parking_lot" version = "0.12.1" @@ -1641,6 +2061,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkg-config" version = "0.3.29" @@ -1674,12 +2105,34 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "pollster" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "presser" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + [[package]] name = "proc-macro-crate" version = "2.0.1" @@ -1687,7 +2140,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97dc5fea232fc28d2f597b37c4876b348a40e33f3b02cc975c8d006d78d94b1a" dependencies = [ "toml_datetime", - "toml_edit", + "toml_edit 0.20.2", ] [[package]] @@ -1764,6 +2217,18 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", "rand_core", ] @@ -1772,6 +2237,9 @@ name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] [[package]] name = "rangemap" @@ -1882,6 +2350,29 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "216080ab382b992234dda86873c18d4c48358f5cfcb70fd693d7f6f2131b628b" +[[package]] +name = "rfd" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373d2fc6310e2d14943d4e66ebed5b774a2b6b3b1610e7377edf124fb2760d6b" +dependencies = [ + "ashpd", + "block", + "dispatch", + "js-sys", + "log", + "objc", + "objc-foundation", + "objc_id", + "pollster", + "raw-window-handle", + "urlencoding", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.48.0", +] + [[package]] name = "roxmltree" version = "0.19.0" @@ -2007,6 +2498,28 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -2016,6 +2529,15 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -2179,6 +2701,18 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tempfile" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if", + "fastrand", + "rustix", + "windows-sys 0.52.0", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -2286,6 +2820,17 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + [[package]] name = "toml_edit" version = "0.20.2" @@ -2398,6 +2943,23 @@ dependencies = [ "nom", ] +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi", +] + [[package]] name = "unicode-bidi" version = "0.3.15" @@ -2428,6 +2990,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-properties" version = "0.1.1" @@ -2458,6 +3029,24 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "uuid" version = "1.7.0" @@ -2547,11 +3136,17 @@ dependencies = [ name = "violet-demo" version = "0.0.1" dependencies = [ + "anyhow", "console_error_panic_hook", + "flume", "futures", "glam", + "indexmap", "itertools 0.12.1", "puffin", + "rfd", + "serde", + "serde_json", "tracing-subscriber", "tracing-tree", "tracing-web", @@ -3270,6 +3865,16 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a0ccd7b4a5345edfcd0c3535718a4e9ff7798ffc536bb5b5a0e26ff84732911" +[[package]] +name = "xdg-home" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21e5a325c3cb8398ad6cf859c1135b25dd29e186679cf2da7581d9679f63b38e" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "xkbcommon-dl" version = "0.4.2" @@ -3301,6 +3906,70 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c94451ac9513335b5e23d7a8a2b61a7102398b8cca5160829d313e84c9d98be1" +[[package]] +name = "zbus" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b8e3d6ae3342792a6cc2340e4394334c7402f3d793b390d2c5494a4032b3030" +dependencies = [ + "async-broadcast", + "async-executor", + "async-fs", + "async-io", + "async-lock 3.3.0", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "derivative", + "enumflags2", + "event-listener 5.2.0", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix", + "ordered-stream", + "rand", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "windows-sys 0.52.0", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7a3e850ff1e7217a3b7a07eba90d37fe9bb9e89a310f718afcde5885ca9b6d7" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "regex", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + [[package]] name = "zeno" version = "0.2.3" @@ -3326,3 +3995,56 @@ dependencies = [ "quote", "syn 2.0.48", ] + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-jpeg" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec866b44a2a1fd6133d363f073ca1b179f438f99e7e5bfb1e33f7181facfe448" +dependencies = [ + "zune-core", +] + +[[package]] +name = "zvariant" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e09e8be97d44eeab994d752f341e67b3b0d80512a8b315a0671d47232ef1b65" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "url", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a5857e2856435331636a9fbb415b09243df4521a267c5bedcd5289b4d5799e" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00bedb16a193cc12451873fee2a1bc6550225acece0e36f333e68326c73c8172" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] diff --git a/Cargo.toml b/Cargo.toml index 8f85f89..e428727 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ "violet-wgpu", "violet-core", "violet-demo" ] +exclude = [ "flax" ] [workspace.package] version = "0.0.1" @@ -23,7 +24,7 @@ documentation = "https://docs.rs/violet" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [workspace.dependencies] -flax = { git = "https://github.com/ten3roberts/flax", version = "0.6.0", features = [ +flax = { path = "./flax", version = "0.6.0", features = [ "derive", "puffin", ] } @@ -31,7 +32,7 @@ flax = { git = "https://github.com/ten3roberts/flax", version = "0.6.0", feature atomic_refcell = "0.1" futures-signals = "0.3" itertools = "0.12" -glam = { version = "0.24", features = ["bytemuck"] } +glam = { version = "0.25", features = ["bytemuck"] } futures = "0.3" futures-concurrency = "7.0" flume = "0.11" @@ -51,9 +52,12 @@ winit = "0.29" wgpu = { version = "0.19", default-features = false, features = ["fragile-send-sync-non-atomic-wasm", "webgl", "wgsl"] } palette = { version = "0.7", features = ["serializing"] } dashmap = "5.4" -image = { version = "0.24", default_features = false, features = ["png", "jpeg"] } +image = { version = "0.25", default_features = false, features = ["png", "jpeg"] } color-hex = "0.2" serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +indexmap = { version = "2.0", features = ["serde"] } +rfd = "0.14" tracing = "0.1" pin-project = "1.1" diff --git a/examples/basic.rs b/examples/basic.rs index 36059a5..fdfd9f9 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -267,5 +267,5 @@ pub fn main() -> anyhow::Result<()> { .with(EnvFilter::from_default_env()) .init(); - violet_wgpu::App::new().run(MainApp) + violet_wgpu::AppBuilder::new().run(MainApp) } diff --git a/examples/color.rs b/examples/color.rs index 79eaf18..095f9b0 100644 --- a/examples/color.rs +++ b/examples/color.rs @@ -29,7 +29,7 @@ pub fn main() -> anyhow::Result<()> { .with(EnvFilter::from_default_env()) .init(); - violet_wgpu::App::new() + violet_wgpu::AppBuilder::new() .with_renderer_config(RendererConfig { debug_mode: false }) .run(MainApp) } diff --git a/examples/counter.rs b/examples/counter.rs index dbd8192..1b49052 100644 --- a/examples/counter.rs +++ b/examples/counter.rs @@ -77,5 +77,5 @@ pub fn main() -> anyhow::Result<()> { .with(EnvFilter::from_default_env()) .init(); - violet_wgpu::App::new().run(MainApp) + violet_wgpu::AppBuilder::new().run(MainApp) } diff --git a/examples/flow.rs b/examples/flow.rs index b3555b8..dd2b419 100644 --- a/examples/flow.rs +++ b/examples/flow.rs @@ -37,7 +37,7 @@ pub fn main() -> anyhow::Result<()> { .with(EnvFilter::from_default_env()) .init(); - violet_wgpu::App::new() + violet_wgpu::AppBuilder::new() .with_renderer_config(RendererConfig { debug_mode: false }) .run(MainApp) } diff --git a/examples/row.rs b/examples/row.rs index 5509c44..974050d 100644 --- a/examples/row.rs +++ b/examples/row.rs @@ -39,7 +39,7 @@ pub fn main() -> anyhow::Result<()> { .with(EnvFilter::from_default_env()) .init(); - violet_wgpu::App::new() + violet_wgpu::AppBuilder::new() .with_renderer_config(RendererConfig { debug_mode: true }) .run(MainApp) } diff --git a/examples/sizing.rs b/examples/sizing.rs index 9029756..2a59923 100644 --- a/examples/sizing.rs +++ b/examples/sizing.rs @@ -41,7 +41,7 @@ pub fn main() -> anyhow::Result<()> { .with(EnvFilter::from_default_env()) .init(); - violet_wgpu::App::new() + violet_wgpu::AppBuilder::new() .with_renderer_config(RendererConfig { debug_mode: true }) .run(MainApp) } diff --git a/flax b/flax new file mode 160000 index 0000000..ef3e4b3 --- /dev/null +++ b/flax @@ -0,0 +1 @@ +Subproject commit ef3e4b3860e9f3934bf6df818d4e373b96e5cc9f diff --git a/violet-core/src/atom.rs b/violet-core/src/atom.rs index 1aa2904..d347936 100644 --- a/violet-core/src/atom.rs +++ b/violet-core/src/atom.rs @@ -1,7 +1,16 @@ +#[doc(hidden)] +pub use flax; use flax::Component; pub struct Atom(pub(crate) Component); +impl Atom { + #[doc(hidden)] + pub fn from_component(component: Component) -> Self { + Self(component) + } +} + impl Clone for Atom { fn clone(&self) -> Self { *self @@ -23,14 +32,14 @@ macro_rules! declare_atom { ($(#[$outer:meta])* $vis: vis $name: ident: $ty: ty $(=> [$($metadata: ty),*])?, $($rest:tt)*) => { $(#[$outer])* $vis fn $name() -> $crate::atom::Atom<$ty> { - use flax::entity::EntityKind; + use $crate::atom::flax::entity::EntityKind; - static COMPONENT_ID: ::core::sync::atomic::AtomicU32 = ::core::sync::atomic::AtomicU32::new(flax::entity::EntityIndex::MAX); - static VTABLE: &flax::vtable::ComponentVTable<$ty> = flax::component_vtable!($name: $ty $(=> [$($metadata),*])?); - $crate::atom::Atom(flax::Component::static_init(&COMPONENT_ID, EntityKind::COMPONENT, VTABLE)) + static COMPONENT_ID: ::core::sync::atomic::AtomicU32 = ::core::sync::atomic::AtomicU32::new($crate::atom::flax::entity::EntityIndex::MAX); + static VTABLE: &$crate::atom::flax::vtable::ComponentVTable<$ty> = $crate::atom::flax::component_vtable!($name: $ty $(=> [$($metadata),*])?); + $crate::atom::Atom::from_component($crate::atom::flax::Component::static_init(&COMPONENT_ID, EntityKind::COMPONENT, VTABLE)) } - flax::component!{ $($rest)* } + $crate::atom::flax::component!{ $($rest)* } }; } diff --git a/violet-core/src/components.rs b/violet-core/src/components.rs index 1a6c5a0..8c34937 100644 --- a/violet-core/src/components.rs +++ b/violet-core/src/components.rs @@ -28,6 +28,8 @@ component! { /// Specifies in screen space where the widget rect upper left corner is pub screen_position: Vec2 => [ Debuggable ], + pub rotation: f32 => [ Debuggable ], + /// Offset the widget from its original position pub offset: Unit => [ Debuggable ], @@ -48,7 +50,9 @@ component! { /// Constrain the aspect ratio of a widget pub aspect_ratio: f32 => [ Debuggable ], - /// Sets the anchor point withing the bounds of the widget where position is applied + /// Set the origin or anchor point of a widget. + /// + /// This determines the center of positioning and rotation pub anchor: Unit => [ Debuggable ], diff --git a/violet-core/src/state/constant.rs b/violet-core/src/state/constant.rs new file mode 100644 index 0000000..402c0e4 --- /dev/null +++ b/violet-core/src/state/constant.rs @@ -0,0 +1,28 @@ +use futures::{prelude::stream::BoxStream, StreamExt}; + +use super::{State, StateRef, StateStream}; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Constant(pub T); + +impl State for Constant { + type Item = T; +} + +impl StateRef for Constant { + type Item = T; + + fn read_ref V, V>(&self, f: F) -> V { + (f)(&self.0) + } +} + +impl StateStream for Constant +where + T: 'static + Send + Sync + Clone, +{ + fn stream(&self) -> BoxStream<'static, Self::Item> { + let value = self.0.clone(); + futures::stream::once(async move { value }).boxed() + } +} diff --git a/violet-core/src/state/mod.rs b/violet-core/src/state/mod.rs index accce60..61d9f18 100644 --- a/violet-core/src/state/mod.rs +++ b/violet-core/src/state/mod.rs @@ -7,6 +7,7 @@ use std::{marker::PhantomData, rc::Rc, sync::Arc}; use futures::{stream::BoxStream, FutureExt, Stream, StreamExt}; use futures_signals::signal::{Mutable, SignalExt}; +pub mod constant; mod dedup; mod feedback; mod filter; diff --git a/violet-core/src/widget/interactive/input.rs b/violet-core/src/widget/interactive/input.rs index 69144ca..bde662a 100644 --- a/violet-core/src/widget/interactive/input.rs +++ b/violet-core/src/widget/interactive/input.rs @@ -16,7 +16,7 @@ use crate::{ components::{self, screen_rect}, editor::{CursorMove, EditAction, EditorAction, TextEditor}, input::{focus_sticky, focusable, on_focus, on_keyboard_input, on_mouse_input, KeyboardInput}, - state::{StateDuplex, StateSink, StateStream}, + state::{State, StateDuplex, StateSink, StateStream}, style::{ interactive_active, interactive_inactive, spacing_small, Background, SizeExt, StyleExt, ValueOrRef, WidgetSize, @@ -104,7 +104,7 @@ impl Widget for TextInput { editor.set_cursor_at_end(); let (editor_props_tx, editor_props_rx) = signal::channel(Box::new(NoOp) as Box); - let content = self.content; + let content = self.content.prevent_feedback(); scope.spawn({ let mut layout_glyphs = layout_glyphs.signal_cloned().to_stream(); diff --git a/violet-demo/Cargo.toml b/violet-demo/Cargo.toml index faa1d55..9c85582 100644 --- a/violet-demo/Cargo.toml +++ b/violet-demo/Cargo.toml @@ -29,6 +29,12 @@ wasm-bindgen-futures = "0.4" itertools.workspace = true tracing-tree.workspace = true puffin.workspace = true +serde.workspace = true +serde_json.workspace = true +indexmap.workspace = true +rfd.workspace = true +anyhow.workspace = true +flume.workspace = true [package.metadata.wasm-pack.profile.profiling] wasm-opt = false diff --git a/violet-demo/src/lib.rs b/violet-demo/src/lib.rs index dd30371..7422540 100644 --- a/violet-demo/src/lib.rs +++ b/violet-demo/src/lib.rs @@ -1,37 +1,45 @@ -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc}; -use futures::StreamExt; -use glam::{Vec2, Vec3}; +use anyhow::Context; +use flume::Sender; +use futures::{Stream, StreamExt}; +use glam::Vec2; +use indexmap::IndexMap; use itertools::Itertools; -use tracing_subscriber::{ - filter::LevelFilter, fmt::format::Pretty, layer::SubscriberExt, util::SubscriberInitExt, Layer, -}; -use tracing_web::{performance_layer, MakeWebConsoleWriter}; +use rfd::AsyncFileDialog; +use serde::{Deserialize, Serialize}; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use violet::{ core::{ + declare_atom, layout::Alignment, state::{DynStateDuplex, State, StateMut, StateStream, StateStreamRef}, - style::{primary_background, Background, SizeExt, ValueOrRef}, - time::sleep, + style::{ + danger_item, primary_background, success_item, warning_item, Background, SizeExt, + ValueOrRef, + }, + time::interval, to_owned, unit::Unit, - utils::{throttle, zip_latest_ref}, + utils::zip_latest_ref, widget::{ - card, column, label, pill, row, Button, Radio, Rectangle, SliderWithLabel, Stack, + card, column, label, row, Button, Radio, Rectangle, SliderWithLabel, Stack, StreamWidget, Text, TextInput, WidgetExt, }, - Edges, Scope, Widget, + Edges, Frame, FutureEffect, Scope, Widget, }, futures_signals::signal::Mutable, - glam::vec2, palette::{FromColor, IntoColor, OklabHue, Oklch, Srgb}, web_time::Duration, - wgpu::renderer::RendererConfig, + wgpu::{app::App, renderer::RendererConfig}, }; use wasm_bindgen::prelude::*; #[cfg(target_arch = "wasm32")] fn setup() { + use tracing_subscriber::{filter::LevelFilter, fmt::format::Pretty, Layer}; + use tracing_web::{performance_layer, MakeWebConsoleWriter}; + let fmt_layer = tracing_subscriber::fmt::layer() .with_ansi(false) .without_time() @@ -66,7 +74,7 @@ fn setup() { pub fn run() { setup(); - violet::wgpu::App::new() + App::builder() .with_renderer_config(RendererConfig { debug_mode: false }) .run(MainApp) .unwrap(); @@ -84,55 +92,47 @@ impl Widget for MainApp { Mutable::new(PaletteColor { color: Oklch::new(0.5, 0.27, (i as f32 * 60.0) % 360.0), falloff: DEFAULT_FALLOFF, + name: format!("color_{i}"), }) }) .collect(), ); - column((Palettes::new(palette_item),)) - .with_size(Unit::rel2(1.0, 1.0)) - .with_background(Background::new(primary_background())) - .contain_margins(true) - .mount(scope); - } -} + let (notify_tx, notify_rx) = flume::unbounded(); -struct Tints { - base: Oklch, - falloff: f32, -} + scope.frame_mut().set_atom(crate::notify_tx(), notify_tx); -impl Tints { - fn new(base: Oklch, falloff: f32) -> Self { - Self { base, falloff } + Stack::new(( + Palettes::new(palette_item), + Stack::new(Notifications { + items: notify_rx.into_stream(), + }) + .with_maximize(Vec2::ONE) + .with_horizontal_alignment(Alignment::End), + )) + .with_size(Unit::rel2(1.0, 1.0)) + .with_background(Background::new(primary_background())) + .mount(scope); } } -impl Widget for Tints { - fn mount(self, scope: &mut Scope<'_>) { - puffin::profile_function!(); - row((1..=9) - .map(|i| { +fn tints(color: impl StateStream) -> impl Widget { + puffin::profile_function!(); + row((1..=9) + .map(move |i| { + let color = color.stream().map(move |v| { let f = (i as f32) / 10.0; - let chroma = self.base.chroma * (1.0 / (1.0 + self.falloff * (f - 0.5).powi(2))); + let color = v.tint(f); - // let color = self.base.lighten(f); - let color = Oklch { - chroma, - l: f, - ..self.base - }; + Rectangle::new(ValueOrRef::value(color.into_color())) + .with_size(Unit::px2(80.0, 60.0)) + }); - Stack::new(column((Rectangle::new(ValueOrRef::value( - color.into_color(), - )) - .with_size(Unit::px2(80.0, 60.0)),))) + Stack::new(column(StreamWidget(color))) .with_margin(Edges::even(4.0)) .with_name("Tint") - }) - .collect_vec()) - .mount(scope) - } + }) + .collect_vec()) } pub fn color_hex(color: impl IntoColor) -> String { @@ -150,8 +150,13 @@ impl Palettes { } } +declare_atom! { + notify_tx: flume::Sender, +} + 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 discard = move |i| { @@ -196,9 +201,7 @@ impl Widget for Palettes { card(row(( checkbox, discard(i), - StreamWidget(throttle(item.stream(), || { - sleep(Duration::from_millis(100)) - })), + palette_color_view(item.clone()), ))) } }) @@ -211,6 +214,7 @@ impl Widget for Palettes { let items = self.items.clone(); column(( + menu_bar(self.items.clone(), notify_tx), StreamWidget(editor), palettes, Button::label("+").on_press(move |_, _| { @@ -218,6 +222,7 @@ impl Widget for Palettes { 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)); }) @@ -227,23 +232,243 @@ impl Widget for Palettes { } } -#[derive(Debug, Clone, Copy)] +struct Notification { + message: String, + kind: NotificationKind, +} + +enum NotificationKind { + Info, + Warning, + Error, +} + +pub struct Notifications { + items: S, +} + +impl Notifications { + pub fn new(items: S) -> Self { + Self { items } + } +} + +impl Widget for Notifications +where + S: 'static + Stream, +{ + fn mount(self, scope: &mut Scope<'_>) { + let notifications = Mutable::new(Vec::new()); + + let notifications_stream = notifications.stream_ref(|v| { + let items = v + .iter() + .map(|(_, v): &(f32, Notification)| { + let color = match v.kind { + NotificationKind::Info => success_item(), + NotificationKind::Warning => warning_item(), + NotificationKind::Error => danger_item(), + }; + card(label(v.message.clone())).with_background(Background::new(color)) + }) + .collect_vec(); + + column(items) + }); + + scope.spawn(async move { + let stream = self.items; + + let mut interval = interval(Duration::from_secs(1)).fuse(); + + let stream = stream.fuse(); + futures::pin_mut!(stream); + + loop { + futures::select! { + _ = interval.next() => { + let notifications = &mut *notifications.lock_mut(); + notifications.retain(|(time, _)| *time > 0.0); + for (time, _) in notifications { + *time -= 1.0; + } + }, + notification = stream.select_next_some() => { + notifications.lock_mut().push((5.0, notification)); + } + complete => break, + } + } + }); + + StreamWidget(notifications_stream).mount(scope); + } +} + +fn menu_bar( + items: Mutable>>, + notify_tx: Sender, +) -> impl Widget { + fn notify_result( + notify_tx: &Sender, + on_success: &str, + ) -> impl Fn(&mut Frame, anyhow::Result<()>) { + let notify_tx = notify_tx.clone(); + move |_, result| match result { + Ok(()) => { + notify_tx + .send(Notification { + message: "Saved".to_string(), + kind: NotificationKind::Info, + }) + .unwrap(); + } + Err(e) => { + notify_tx + .send(Notification { + message: format!("Failed to save: {e}"), + kind: NotificationKind::Error, + }) + .unwrap(); + } + } + } + + let export = Button::label("Export Tints").on_press({ + to_owned![items, notify_tx]; + move |frame, _| { + let data = items + .lock_ref() + .iter() + .map(|item| { + let item = item.lock_ref(); + let tints = (1..=9) + .map(|i| { + let color = item.tint(i as f32 / 10.0); + ( + format!("{}", i * 100), + HexColor(Srgb::from_color(color).into_format()), + ) + }) + .collect::>(); + + (item.name.clone(), tints) + }) + .collect::>(); + + let json = serde_json::to_string_pretty(&data).unwrap(); + + let fut = async move { + let Some(file) = AsyncFileDialog::new().set_directory(".").save_file().await else { + return anyhow::Ok(()); + }; + + file.write(json.as_bytes()) + .await + .context("Failed to write to save file")?; + + Ok(()) + }; + + frame.spawn(FutureEffect::new(fut, notify_result(¬ify_tx, "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(".").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(FutureEffect::new(fut, notify_result(¬ify_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(".").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(FutureEffect::new(fut, notify_result(¬ify_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(( + label("Palette editor"), + save, + load, + export, + test_notification, + )) +} + +#[derive(Clone, Serialize, Deserialize)] pub struct PaletteColor { color: Oklch, falloff: f32, + name: String, } -impl Widget for PaletteColor { - fn mount(self, scope: &mut Scope<'_>) { - puffin::profile_function!(); - Stack::new(( - row((Tints::new(self.color, self.falloff),)), - pill(label(color_hex(self.color))), - )) +impl PaletteColor { + pub fn tint(&self, tint: f32) -> Oklch { + let chroma = self.color.chroma * (1.0 / (1.0 + self.falloff * (tint - 0.5).powi(2))); + // let color = self.base.lighten(f); + Oklch { + chroma, + l: tint, + ..self.color + } + } +} + +fn palette_color_view(color: Mutable) -> impl Widget { + puffin::profile_function!(); + // let label = color.stream().map(|v| label(color_hex(v.color))); + let label = color.clone().map_ref(|v| &v.name, |v| &mut v.name); + + let label = TextInput::new(label); + Stack::new((row((tints(color),)), label)) .with_vertical_alignment(Alignment::End) .with_horizontal_alignment(Alignment::Center) - .mount(scope) - } } pub struct PaletteEditor { @@ -330,3 +555,55 @@ impl Widget for ColorHexEditor { TextInput::new(value).mount(scope) } } + +pub struct HexColor(Srgb); + +impl Serialize for HexColor { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let s = format!( + "#{:0>2x}{:0>2x}{:0>2x}", + self.0.red, self.0.green, self.0.blue + ); + + serializer.serialize_str(&s) + } +} + +impl<'de> Deserialize<'de> for HexColor { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + let color: Srgb = s.trim().parse().map_err(serde::de::Error::custom)?; + Ok(HexColor(color)) + } +} + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct TintsData { + #[serde(rename = "100")] + _100: HexColor, + #[serde(rename = "200")] + _200: HexColor, + #[serde(rename = "300")] + _300: HexColor, + #[serde(rename = "400")] + _400: HexColor, + #[serde(rename = "500")] + _500: HexColor, + #[serde(rename = "600")] + _600: HexColor, + #[serde(rename = "700")] + _700: HexColor, + #[serde(rename = "800")] + _800: HexColor, + #[serde(rename = "900")] + _900: HexColor, +} + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct ColorPalettes(HashMap); diff --git a/violet-wgpu/src/app.rs b/violet-wgpu/src/app.rs index 92eecf4..c2391f5 100644 --- a/violet-wgpu/src/app.rs +++ b/violet-wgpu/src/app.rs @@ -8,7 +8,7 @@ use parking_lot::Mutex; use winit::{ dpi::{LogicalSize, PhysicalSize}, event::{Event, WindowEvent}, - event_loop::{EventLoopBuilder, EventLoopWindowTarget}, + event_loop::{ControlFlow, EventLoopBuilder, EventLoopWindowTarget}, window::WindowBuilder, }; @@ -58,11 +58,11 @@ impl Widget for Canvas { } } -pub struct App { +pub struct AppBuilder { renderer_config: RendererConfig, } -impl App { +impl AppBuilder { pub fn new() -> Self { Self { renderer_config: Default::default(), @@ -85,7 +85,7 @@ impl App { let event_loop = EventLoopBuilder::new().build()?; #[allow(unused_mut)] - let mut builder = WindowBuilder::new().with_inner_size(PhysicalSize::new(800, 600)); + let mut builder = WindowBuilder::new().with_inner_size(PhysicalSize::new(1920, 1080)); #[cfg(target_arch = "wasm32")] { @@ -174,7 +174,7 @@ impl App { #[cfg(not(target_arch = "wasm32"))] let _puffin_server = setup_puffin(); - let mut instance = AppInstance { + let mut instance = App { frame, renderer: None, root, @@ -198,10 +198,6 @@ impl App { instance.update(); - if let Err(err) = instance.draw() { - tracing::error!("Failed to draw to window: {err:?}"); - } - if !instance.is_minimized() { let report = instance.stats.report(); window.set_title(&format!( @@ -209,6 +205,9 @@ impl App { report.min_frame_time, report.average_frame_time, report.max_frame_time, )); } + + ctl.set_control_flow(ControlFlow::Poll); + window.request_redraw(); puffin::GlobalProfiler::lock().new_frame(); } Event::WindowEvent { window_id, event } => match event { @@ -261,10 +260,10 @@ impl App { } }; - // #[cfg(not(target_arch = "wasm32"))] - // { - // event_loop.run(on_event)?; - // } + #[cfg(not(target_arch = "wasm32"))] + { + event_loop.run(on_event)?; + } #[cfg(target_arch = "wasm32")] { use winit::platform::web::EventLoopExtWebSys; @@ -275,7 +274,8 @@ impl App { } } -pub struct AppInstance { +/// A running application instance of violet +pub struct App { frame: Frame, renderer: Option, root: Entity, @@ -288,7 +288,11 @@ pub struct AppInstance { window_size: PhysicalSize, } -impl AppInstance { +impl App { + pub fn builder() -> AppBuilder { + AppBuilder::new() + } + pub fn on_resize(&mut self, size: PhysicalSize) { self.window_size = size; @@ -361,7 +365,7 @@ fn setup_puffin() -> Option { Some(server) } -impl Default for App { +impl Default for AppBuilder { fn default() -> Self { Self::new() } diff --git a/violet-wgpu/src/lib.rs b/violet-wgpu/src/lib.rs index a94225a..2290faa 100644 --- a/violet-wgpu/src/lib.rs +++ b/violet-wgpu/src/lib.rs @@ -10,4 +10,4 @@ mod texture; pub use graphics::Gpu; -pub use app::App; +pub use app::AppBuilder; diff --git a/violet-wgpu/src/renderer/rect_renderer.rs b/violet-wgpu/src/renderer/rect_renderer.rs index 8b237a1..915eafa 100644 --- a/violet-wgpu/src/renderer/rect_renderer.rs +++ b/violet-wgpu/src/renderer/rect_renderer.rs @@ -5,16 +5,17 @@ use flax::{ filter::{All, With}, CommandBuffer, Component, EntityIds, Fetch, FetchExt, Mutable, Opt, OptOr, Query, }; -use glam::{vec2, vec3, Mat4, Quat, Vec4}; +use glam::{vec2, vec3, Mat4, Quat, Vec2, Vec4}; use image::{DynamicImage, ImageBuffer}; use palette::Srgba; use wgpu::{BindGroup, BindGroupLayout, SamplerDescriptor, ShaderStages, TextureFormat}; use violet_core::{ assets::{map::HandleMap, Asset, AssetCache, AssetKey}, - components::{color, draw_shape, image, screen_rect}, + components::{anchor, color, draw_shape, image, rotation, screen_rect}, shape::{self, shape_rectangle}, stored::{self, WeakHandle}, + unit::Unit, Frame, Rect, }; @@ -63,6 +64,8 @@ impl AssetKey for ImageFromColor { #[derive(Fetch)] struct RectObjectQuery { screen_rect: Component, + rotation: OptOr, f32>, + anchor: OptOr>, Unit>, // pos: Component, // local_pos: Component, color: OptOr, Srgba>, @@ -73,6 +76,8 @@ impl RectObjectQuery { fn new() -> Self { Self { screen_rect: screen_rect(), + rotation: rotation().opt_or(0.0), + anchor: anchor().opt_or_default(), object_data: object_data().as_mut(), color: color().opt_or(Srgba::new(1.0, 1.0, 1.0, 1.0)), } @@ -231,11 +236,12 @@ impl RectRenderer { // return; // } - let model_matrix = Mat4::from_scale_rotation_translation( - rect.size().extend(1.0), - Quat::IDENTITY, - rect.pos().extend(0.1), - ); + let anchor = item.anchor.resolve(rect.size()).extend(0.0); + + let model_matrix = Mat4::from_translation(rect.pos().extend(0.1) + anchor) + * Mat4::from_rotation_z(*item.rotation) + * Mat4::from_translation(-anchor) + * Mat4::from_scale(rect.size().extend(1.0)); *item.object_data = ObjectData { model_matrix, From 35be6f6d82b2e232e2b785e4a43e7d79b26b084c Mon Sep 17 00:00:00 2001 From: Freja Roberts Date: Wed, 20 Mar 2024 01:23:05 +0100 Subject: [PATCH 09/16] feat: clipboard and selectable text --- .cargo/config.toml | 2 + Cargo.lock | 83 +++++- Cargo.toml | 6 +- flax | 2 +- violet-core/Cargo.toml | 4 + violet-core/src/components.rs | 3 + violet-core/src/editor.rs | 161 +++++++++-- violet-core/src/frame.rs | 4 +- violet-core/src/input.rs | 21 +- violet-core/src/io.rs | 89 ++++++ violet-core/src/layout/float.rs | 91 ++++++ violet-core/src/layout/mod.rs | 14 +- violet-core/src/lib.rs | 1 + violet-core/src/scope.rs | 135 +++++++-- violet-core/src/stored.rs | 9 + violet-core/src/style/mod.rs | 6 +- violet-core/src/systems.rs | 51 ++-- violet-core/src/text.rs | 3 +- violet-core/src/types.rs | 4 + violet-core/src/widget/container.rs | 35 ++- violet-core/src/widget/interactive/button.rs | 17 +- violet-core/src/widget/interactive/input.rs | 274 +++++++++++++++---- violet-core/src/widget/interactive/slider.rs | 6 +- violet-core/src/widget/mod.rs | 15 +- violet-demo/src/lib.rs | 66 +++-- violet-wgpu/src/app.rs | 6 +- violet-wgpu/src/text.rs | 16 +- 27 files changed, 924 insertions(+), 200 deletions(-) create mode 100644 .cargo/config.toml create mode 100644 violet-core/src/io.rs create mode 100644 violet-core/src/layout/float.rs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..18ffca9 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +rustflags = [ "--cfg=web_sys_unstable_apis" ] diff --git a/Cargo.lock b/Cargo.lock index b9c8b0e..a093c2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,6 +112,25 @@ dependencies = [ "num-traits", ] +[[package]] +name = "arboard" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2041f1943049c7978768d84e6d0fd95de98b76d6c4727b09e78ec253d29fa58" +dependencies = [ + "clipboard-win", + "core-graphics", + "image 0.24.9", + "log", + "objc", + "objc-foundation", + "objc_id", + "parking_lot", + "thiserror", + "windows-sys 0.48.0", + "x11rb", +] + [[package]] name = "arrayref" version = "0.3.7" @@ -544,6 +563,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +[[package]] +name = "clipboard-win" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d517d4b86184dbb111d3556a10f1c8a04da7428d2987bf1081602bf11c3aa9ee" +dependencies = [ + "error-code", +] + [[package]] name = "codespan-reporting" version = "0.11.1" @@ -560,6 +588,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ecdffb913a326b6c642290a0d0ec8e8d6597291acdc07cc4c9cb4b3635d44cf9" +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "com" version = "0.6.0" @@ -861,6 +895,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "error-code" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b" + [[package]] name = "euclid" version = "0.22.9" @@ -1413,6 +1453,20 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "image" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "num-traits", + "png", + "tiff", +] + [[package]] name = "image" version = "0.25.0" @@ -1493,6 +1547,12 @@ dependencies = [ "libc", ] +[[package]] +name = "jpeg-decoder" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" + [[package]] name = "js-sys" version = "0.3.67" @@ -2752,6 +2812,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + [[package]] name = "tiny-skia" version = "0.11.4" @@ -3097,6 +3168,7 @@ name = "violet-core" version = "0.0.1" dependencies = [ "anyhow", + "arboard", "arrayvec", "atomic_refcell", "bytemuck", @@ -3111,7 +3183,7 @@ dependencies = [ "futures-signals", "glam", "gloo-timers", - "image", + "image 0.25.0", "itertools 0.12.1", "more-asserts", "once_cell", @@ -3128,6 +3200,7 @@ dependencies = [ "tynm", "unicode-segmentation", "wasm-bindgen-futures", + "web-sys", "web-time 1.0.0", "winit", ] @@ -3169,7 +3242,7 @@ dependencies = [ "futures", "glam", "guillotiere", - "image", + "image 0.25.0", "itertools 0.12.1", "palette", "parking_lot", @@ -3406,6 +3479,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + [[package]] name = "wgpu" version = "0.19.1" diff --git a/Cargo.toml b/Cargo.toml index e428727..e2a9c92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ tokio = { version = "1.0", default-features = false, features = ["macros", "rt"] arrayvec = "0.7" sync_wrapper = "1.0" smallvec = "1.0" +arboard = "3.0" bytemuck = { version = "1.13", features = ["derive"] } winit = "0.29" @@ -75,7 +76,7 @@ gloo-timers = "0.3" web-time = "1.0" wasm-bindgen-futures = "0.4" wasm-bindgen = "0.2" -web-sys = "0.3" +web-sys = { version = "0.3", features = ["Clipboard"] } tracing-tree = "0.3" [dependencies] @@ -118,6 +119,9 @@ opt-level = 2 [profile.dev.package.puffin_server] opt-level = 2 +[profile.dev.package.cosmic-text] +opt-level = 2 + [patch.crates-io] palette = { git = "https://github.com/Ogeon/palette" } diff --git a/flax b/flax index ef3e4b3..d0ba35e 160000 --- a/flax +++ b/flax @@ -1 +1 @@ -Subproject commit ef3e4b3860e9f3934bf6df818d4e373b96e5cc9f +Subproject commit d0ba35eacd28423cbb7f8e96b3d3c45a6a6cf683 diff --git a/violet-core/Cargo.toml b/violet-core/Cargo.toml index da41b50..4dfef1d 100644 --- a/violet-core/Cargo.toml +++ b/violet-core/Cargo.toml @@ -54,3 +54,7 @@ tokio.workspace = true gloo-timers.workspace = true wasm-bindgen-futures.workspace = true cosmic-text = { workspace = true, features = ["wasm-web"] } +web-sys.workspace = true + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +arboard.workspace = true diff --git a/violet-core/src/components.rs b/violet-core/src/components.rs index 8c34937..7f83022 100644 --- a/violet-core/src/components.rs +++ b/violet-core/src/components.rs @@ -8,6 +8,7 @@ use palette::Srgba; use crate::{ assets::Asset, layout::{Layout, SizeResolver}, + stored::UntypedHandle, text::{LayoutGlyphs, TextSegment, Wrap}, unit::Unit, Edges, Frame, Rect, @@ -97,6 +98,8 @@ component! { pub(crate) atoms, pub on_animation_frame: OnAnimationFrame, + + pub handles: Vec, } pub type OnAnimationFrame = Box; diff --git a/violet-core/src/editor.rs b/violet-core/src/editor.rs index 941649e..63b446c 100644 --- a/violet-core/src/editor.rs +++ b/violet-core/src/editor.rs @@ -70,12 +70,13 @@ impl EditorLine { } } -/// Text editor driver +/// The core text editor buffer pub struct TextEditor { text: Vec, /// The current cursor position /// cursor: CursorLocation, + selection: Option, } /// Movement action for the cursor @@ -99,6 +100,8 @@ pub enum EditAction { pub enum EditorAction { CursorMove(CursorMove), + SelectionMove(CursorMove), + SelectionClear, Edit(EditAction), SetText(Vec), } @@ -108,17 +111,28 @@ impl TextEditor { Self { cursor: CursorLocation { row: 0, col: 0 }, text: vec![EditorLine::default()], + selection: None, } } - pub fn move_cursor(&mut self, direction: CursorMove) { - match direction { - CursorMove::Up => { - self.cursor.row = self.cursor.row.saturating_sub(1); - } - CursorMove::Down => { - self.cursor.row = (self.cursor.row + 1).min(self.text.len() - 1); - } + pub fn move_cursor(&mut self, m: CursorMove) { + self.cursor = self.get_new_cursor(m, self.cursor) + } + + pub fn move_selection(&mut self, m: CursorMove) { + self.selection = Some(self.get_new_cursor(m, self.selection.unwrap_or(self.cursor))) + } + + fn get_new_cursor(&self, m: CursorMove, cursor: CursorLocation) -> CursorLocation { + match m { + CursorMove::Up => CursorLocation { + row: cursor.row.saturating_sub(1), + col: cursor.col, + }, + CursorMove::Down => CursorLocation { + row: (cursor.row + 1).min(self.text.len() - 1), + col: cursor.col, + }, CursorMove::Left => { if let Some((i, _)) = self .line() @@ -126,20 +140,34 @@ impl TextEditor { .take_while(|(i, _)| *i < self.cursor.col) .last() { - self.cursor.col = i; + CursorLocation { + row: cursor.row, + col: i, + } } else if self.cursor.row > 0 { - self.cursor.row -= 1; - self.cursor.col = self.line().len(); + CursorLocation { + row: cursor.row - 1, + col: self.line().len(), + } + } else { + cursor } } CursorMove::Right => { let next_glyph = self.line().graphemes().find(|(i, _)| *i == self.cursor.col); if let Some((i, g)) = next_glyph { - self.cursor.col = i + g.len(); + CursorLocation { + row: cursor.row, + col: i + g.len(), + } } else if self.cursor.row < self.text.len() - 1 { - self.cursor.row += 1; - self.cursor.col = 0; + CursorLocation { + row: cursor.row + 1, + col: 0, + } + } else { + cursor } } CursorMove::ForwardWord => { @@ -149,7 +177,12 @@ impl TextEditor { .find_or_last(|(i, _)| *i >= self.cursor.col); tracing::info!(?word, "current word"); if let Some((i, word)) = word { - self.cursor.col = i + word.len(); + CursorLocation { + row: cursor.row, + col: i + word.len(), + } + } else { + cursor } } CursorMove::BackwardWord => { @@ -161,20 +194,29 @@ impl TextEditor { .find(|(i, _)| *i < self.cursor.col); tracing::info!(?word, "current word"); if let Some((i, _)) = word { - self.cursor.col = i; + CursorLocation { + row: cursor.row, + col: i, + } + } else { + cursor } } else if self.cursor.row > 0 { - self.cursor.row -= 1; - self.cursor.col = self.line().len(); + CursorLocation { + row: cursor.row - 1, + col: self.line().len(), + } + } else { + cursor } } CursorMove::SetPosition(pos) => { if (pos.row > self.text.len() - 1) || (pos.col > self.text[pos.row].len()) { tracing::error!(?pos, "invalid cursor position"); - return; + cursor + } else { + pos } - - self.cursor = pos; } } } @@ -186,8 +228,10 @@ impl TextEditor { "expected cursor to be on a grapheme" ); } + match action { EditAction::InsertText(text) => { + self.delete_selected_text(); let mut insert_lines = text.as_ref().lines(); if let Some(text) = insert_lines.next() { @@ -208,6 +252,9 @@ impl TextEditor { } } EditAction::DeleteBackwardChar => { + if self.delete_selected_text() { + return; + } if self.cursor.col > 0 { let col = self.cursor.col; let current_grapheme = @@ -227,6 +274,9 @@ impl TextEditor { } } EditAction::DeleteBackwardWord => { + if self.delete_selected_text() { + return; + } let line = &mut self.text[self.cursor.row]; if self.cursor.col > 0 { let graphemes = line.graphemes().peekable(); @@ -259,6 +309,7 @@ impl TextEditor { } } EditAction::InsertLine => { + self.delete_selected_text(); let col = self.insert_column(); let line = &mut self.text[self.cursor.row]; let new_line = line.text.split_off(col); @@ -270,6 +321,9 @@ impl TextEditor { self.cursor.col = 0; } EditAction::DeleteLine => { + if self.delete_selected_text() { + return; + } if self.cursor.row == 0 && self.text.len() == 1 { self.text[0].clear(); self.cursor.col = 0; @@ -286,6 +340,8 @@ impl TextEditor { EditorAction::CursorMove(m) => self.move_cursor(m), EditorAction::Edit(e) => self.edit(e), EditorAction::SetText(v) => self.set_text(v.iter().map(|v| v.as_ref())), + EditorAction::SelectionMove(m) => self.move_selection(m), + EditorAction::SelectionClear => self.clear_selection(), } } @@ -341,6 +397,67 @@ impl TextEditor { fn insert_column(&self) -> usize { self.cursor.col.min(self.line().len()) } + + pub fn selection_bounds(&self) -> Option<(CursorLocation, CursorLocation)> { + let sel = self.selection?; + if sel < self.cursor { + Some((sel, self.cursor)) + } else { + Some((self.cursor, sel)) + } + } + + pub fn selected_text(&self) -> Option> { + let (start, end) = self.selection_bounds()?; + + let mut text = Vec::new(); + for (i, line) in self.text[start.row..=end.row].iter().enumerate() { + let row = start.row + i; + + if row == start.row && row == end.row { + text.push(&line.text[start.col..end.col]); + } else if row == start.row { + text.push(&line.text[start.col..]); + } else if row == end.row { + text.push(&line.text[..end.col]); + } else { + text.push(&line.text); + } + } + + Some(text) + } + + pub fn delete_selected_text(&mut self) -> bool { + let Some((start, end)) = self.selection_bounds() else { + return false; + }; + + if start.row == end.row { + self.text[start.row].text.drain(start.col..end.col); + } else { + self.text[start.row].text.truncate(start.col); + self.text[end.row].text.drain(0..end.col); + self.text.drain(start.row + 1..end.row); + } + + self.cursor = start; + self.clear_selection(); + + true + } + + pub fn set_selection(&mut self, sel: Option) { + self.selection = sel; + } + + pub fn clear_selection(&mut self) { + self.selection = None; + } + + pub fn selection(&self) -> Option { + self.selection + } } impl Default for TextEditor { diff --git a/violet-core/src/frame.rs b/violet-core/src/frame.rs index d8a41e3..6fc1486 100644 --- a/violet-core/src/frame.rs +++ b/violet-core/src/frame.rs @@ -48,8 +48,8 @@ impl Frame { &self.world } - pub fn new_root(&mut self, widget: impl Widget) -> Entity { - let mut scope = Scope::new(self); + pub fn new_root(&mut self, widget: W) -> Entity { + let mut scope = Scope::new(self, tynm::type_name::()); widget.mount(&mut scope); scope.id() } diff --git a/violet-core/src/input.rs b/violet-core/src/input.rs index 611c28b..07e8914 100644 --- a/violet-core/src/input.rs +++ b/violet-core/src/input.rs @@ -13,6 +13,7 @@ use winit::{ use crate::{ components::{rect, screen_position, screen_rect}, + scope::ScopeRef, Frame, Rect, }; @@ -102,9 +103,9 @@ impl InputState { local_pos: self.pos - origin, }; if let Ok(mut on_input) = entity.get_mut(on_mouse_input()) { + let s = ScopeRef::new(frame, entity); on_input( - frame, - &entity, + &s, MouseInput { modifiers: self.modifiers, state, @@ -122,9 +123,9 @@ impl InputState { if let Some(entity) = &self.focused(&frame.world) { let screen_rect = entity.get_copy(screen_rect()).unwrap_or_default(); if let Ok(mut on_input) = entity.get_mut(on_cursor_move()) { + let s = ScopeRef::new(frame, *entity); on_input( - frame, - entity, + &s, CursorMove { modifiers: self.modifiers, absolute_pos: pos, @@ -142,9 +143,9 @@ impl InputState { pub fn on_keyboard_input(&mut self, frame: &mut Frame, event: KeyEvent) { if let Some(entity) = &self.focused(frame.world()) { if let Ok(mut on_input) = entity.get_mut(on_keyboard_input()) { + let s = ScopeRef::new(frame, *entity); on_input( - frame, - entity, + &s, KeyboardInput { modifiers: self.modifiers, event, @@ -167,15 +168,17 @@ impl InputState { if let Some(cur) = cur { if let Ok(mut on_focus) = cur.get_mut(on_focus()) { - on_focus(frame, &cur, false); + let s = ScopeRef::new(frame, cur); + on_focus(&s, false); } } if let Some(new) = focused { let entity = frame.world().entity(new).unwrap(); + let s = ScopeRef::new(frame, entity); if let Ok(mut on_focus) = entity.get_mut(on_focus()) { - on_focus(frame, &entity, true); + on_focus(&s, true); } let sticky = entity.has(focus_sticky()); @@ -209,7 +212,7 @@ pub struct KeyboardInput { pub event: KeyEvent, } -pub type InputEventHandler = Box; +pub type InputEventHandler = Box, T)>; component! { pub focus_sticky: (), diff --git a/violet-core/src/io.rs b/violet-core/src/io.rs new file mode 100644 index 0000000..d262898 --- /dev/null +++ b/violet-core/src/io.rs @@ -0,0 +1,89 @@ +use std::sync::Arc; + +use parking_lot::Mutex; + +use crate::{declare_atom, stored::Handle}; + +pub struct Clipboard { + inner: ClipboardInner, +} + +impl Clipboard { + pub fn new() -> Self { + Self { + inner: ClipboardInner::new(), + } + } + + pub async fn get_text(&self) -> Option { + self.inner.get_text().await + } + + pub async fn set_text(&self, text: String) { + self.inner.set_text(text).await + } +} + +impl Default for Clipboard { + fn default() -> Self { + Self::new() + } +} + +#[cfg(not(target_arch = "wasm32"))] +struct ClipboardInner { + clipboard: Mutex, +} + +#[cfg(not(target_arch = "wasm32"))] +impl ClipboardInner { + pub fn new() -> Self { + Self { + clipboard: Mutex::new(arboard::Clipboard::new().unwrap()), + } + } + + pub async fn get_text(&self) -> Option { + self.clipboard.lock().get_text().ok() + } + + pub async fn set_text(&self, text: String) { + self.clipboard.lock().set_text(text).ok(); + } +} + +#[cfg(target_arch = "wasm32")] +struct ClipboardInner { + clipboard: Option, +} + +#[cfg(target_arch = "wasm32")] +impl ClipboardInner { + pub fn new() -> Self { + Self { + clipboard: web_sys::window().unwrap().navigator().clipboard(), + } + } + + pub async fn get_text(&self) -> Option { + Some( + wasm_bindgen_futures::JsFuture::from(self.clipboard.as_ref()?.read_text()) + .await + .ok()? + .as_string() + .expect("Result should be a string"), + ) + } + + pub async fn set_text(&self, text: String) { + if let Some(clipboard) = &self.clipboard { + wasm_bindgen_futures::JsFuture::from(clipboard.write_text(&text)) + .await + .ok(); + } + } +} + +declare_atom! { + pub clipboard: Handle>, +} diff --git a/violet-core/src/layout/float.rs b/violet-core/src/layout/float.rs new file mode 100644 index 0000000..7e3a029 --- /dev/null +++ b/violet-core/src/layout/float.rs @@ -0,0 +1,91 @@ +use flax::{Entity, EntityRef, World}; +use glam::{BVec2, Vec2}; + +use crate::{ + components, + layout::{query_size, Direction, SizingHints}, + Edges, Rect, +}; + +use super::{apply_layout, Block, LayoutLimits, QueryArgs, Sizing}; + +/// A floating layout positions its children similar to the stack layout, but it does grow to accommodate the children. +/// +/// This means that the children are *detached* from the normal flow of the layout, and they can overlap with other neighboring widgets. +/// +/// This is the preferred layout for things like tooltips, popups, and other floating UI elements. +#[derive(Default, Debug, Clone)] +pub struct FloatLayout {} + +impl FloatLayout { + pub(crate) fn apply( + &self, + world: &World, + entity: &EntityRef, + children: &[Entity], + content_area: Rect, + limits: LayoutLimits, + preferred_size: Vec2, + ) -> Block { + puffin::profile_function!(); + let _span = tracing::debug_span!("FloatLayout::apply").entered(); + + let blocks = children.iter().for_each(|&child| { + let entity = world.entity(child).expect("invalid child"); + + // let pos = resolve_pos(&entity, content_area, preferred_size); + + let limits = LayoutLimits { + min_size: Vec2::ZERO, + max_size: Vec2::INFINITY, + }; + + let block = apply_layout(world, &entity, content_area.size(), limits); + + entity.update_dedup(components::rect(), block.rect); + entity.update_dedup(components::local_position(), Vec2::ZERO); + }); + + Block::new(Rect::ZERO, Edges::ZERO, BVec2::FALSE) + } + + pub(crate) fn query_size( + &self, + world: &World, + children: &[Entity], + args: QueryArgs, + preferred_size: Vec2, + ) -> Sizing { + puffin::profile_function!(); + let min_rect = Rect::from_size(args.limits.min_size); + + let mut hints = SizingHints::default(); + + for &child in children.iter() { + let entity = world.entity(child).expect("invalid child"); + + let sizing = query_size( + world, + &entity, + QueryArgs { + limits: LayoutLimits { + min_size: Vec2::ZERO, + max_size: Vec2::INFINITY, + }, + content_area: args.content_area, + direction: Direction::Horizontal, + }, + ); + + hints = hints.combine(sizing.hints); + } + + Sizing { + min: Rect::ZERO, + preferred: Rect::ZERO, + margin: Edges::ZERO, + hints, + maximize: Vec2::ZERO, + } + } +} diff --git a/violet-core/src/layout/mod.rs b/violet-core/src/layout/mod.rs index 78f2332..4012f50 100644 --- a/violet-core/src/layout/mod.rs +++ b/violet-core/src/layout/mod.rs @@ -1,4 +1,5 @@ pub mod cache; +mod float; mod flow; mod stack; @@ -16,6 +17,7 @@ use crate::{ Edges, Rect, }; +pub use float::FloatLayout; pub use flow::{Alignment, FlowLayout}; pub use stack::StackLayout; @@ -75,6 +77,7 @@ impl Direction { pub enum Layout { Stack(StackLayout), Flow(FlowLayout), + Float(FloatLayout), } impl Layout { @@ -106,6 +109,14 @@ impl Layout { limits, preferred_size, ), + Layout::Float(v) => v.apply( + world, + entity, + children, + content_area, + limits, + preferred_size, + ), } } @@ -120,6 +131,7 @@ impl Layout { match self { Layout::Stack(v) => v.query_size(world, children, args, preferred_size), Layout::Flow(v) => v.query_size(world, cache, children, args, preferred_size), + Layout::Float(v) => v.query_size(world, children, args, preferred_size), } } } @@ -172,7 +184,7 @@ impl Display for LayoutLimits { pub struct Block { pub(crate) rect: Rect, pub(crate) margin: Edges, - /// See: [SizingHints::can_grow] + /// See: [`SizingHints::can_grow`] pub can_grow: BVec2, } diff --git a/violet-core/src/lib.rs b/violet-core/src/lib.rs index 753ad89..e927df7 100644 --- a/violet-core/src/lib.rs +++ b/violet-core/src/lib.rs @@ -22,6 +22,7 @@ mod types; pub mod unit; pub mod utils; pub mod widget; +pub mod io; pub use effect::{FutureEffect, StreamEffect}; pub use frame::Frame; diff --git a/violet-core/src/scope.rs b/violet-core/src/scope.rs index 0c41c15..d4e9377 100644 --- a/violet-core/src/scope.rs +++ b/violet-core/src/scope.rs @@ -1,4 +1,5 @@ use std::{ + ops::Deref, pin::Pin, task::{Context, Poll}, }; @@ -12,11 +13,17 @@ use futures::{Future, Stream}; use pin_project::pin_project; use crate::{ - assets::AssetCache, components::children, effect::Effect, input::InputEventHandler, - stored::Handle, style::get_stylesheet_from_entity, Frame, FutureEffect, StreamEffect, Widget, + assets::AssetCache, + components::{children, handles}, + effect::Effect, + input::InputEventHandler, + stored::{Handle, UntypedHandle, WeakHandle}, + style::get_stylesheet_from_entity, + systems::widget_template, + Frame, FutureEffect, StreamEffect, Widget, }; -/// The scope within a [`Widget`][crate::Widget] is mounted or modified +/// The scope to modify and mount a widget pub struct Scope<'a> { frame: &'a mut Frame, id: Entity, @@ -32,8 +39,10 @@ impl<'a> std::fmt::Debug for Scope<'a> { } impl<'a> Scope<'a> { - pub(crate) fn new(frame: &'a mut Frame) -> Self { - let id = frame.world_mut().spawn(); + pub(crate) fn new(frame: &'a mut Frame, name: String) -> Self { + let mut entity = EntityBuilder::new(); + widget_template(&mut entity, name); + let id = entity.spawn(frame.world_mut()); Self { frame, @@ -108,7 +117,9 @@ impl<'a> Scope<'a> { /// Attaches a widget in a sub-scope. pub fn attach(&mut self, widget: W) -> Entity { self.flush(); - let id = self.frame.world.spawn(); + let mut entity = EntityBuilder::new(); + widget_template(&mut entity, tynm::type_name::()); + let id = entity.spawn(self.frame.world_mut()); self.frame .world_mut() @@ -152,14 +163,14 @@ impl<'a> Scope<'a> { } /// Spawns an effect scoped to the lifetime of this entity and scope - pub fn spawn_effect(&mut self, effect: impl 'static + for<'x> Effect>) { + pub fn spawn_effect(&self, effect: impl 'static + for<'x> Effect>) { self.frame.spawn(ScopedEffect { id: self.id, effect, }); } - pub fn spawn(&mut self, fut: impl 'static + Future) { + pub fn spawn(&self, fut: impl 'static + Future) { self.spawn_effect(FutureEffect::new(fut, |_: &mut Scope<'_>, _| {})) } @@ -173,7 +184,7 @@ impl<'a> Scope<'a> { } /// Spawns an effect which is *not* scoped to the widget - pub fn spawn_unscoped(&mut self, effect: impl 'static + for<'x> Effect) { + pub fn spawn_unscoped(&self, effect: impl 'static + for<'x> Effect) { self.frame.spawn(effect); } @@ -195,20 +206,30 @@ impl<'a> Scope<'a> { /// Stores an arbitrary value and returns a handle to it. /// - /// The value is stored for the duration of the returned handle, and *not* the widgets scope. + /// The value is stored for the duration of the widgets lifetime. /// /// A handle can be used to safely store state across multiple widgets and will not panic if /// the original widget is despawned. - pub fn store(&mut self, value: T) -> Handle { - self.frame.store_mut().insert(value) + pub fn store(&mut self, value: T) -> WeakHandle { + let handle = self.frame.store_mut().insert(value); + let weak_handle = handle.downgrade(); + self.entity_mut() + .entry(handles()) + .or_default() + .push(UntypedHandle::new(handle)); + weak_handle } - pub fn read(&self, handle: &Handle) -> &T { - self.frame.store().get(handle) + pub fn read(&self, handle: &WeakHandle) -> &T { + let store = self.frame.store().store::().expect("Handle is invalid"); + let handle = handle.upgrade(store).expect("Handle is invalid"); + self.frame.store().get(&handle) } - pub fn write(&mut self, handle: &Handle) -> &mut T { - self.frame.store_mut().get_mut(handle) + pub fn write(&mut self, handle: WeakHandle) -> &mut T { + let store = self.frame.store().store::().expect("Handle is invalid"); + let handle = handle.upgrade(store).expect("Handle is invalid"); + self.frame.store_mut().get_mut(&handle) } pub fn monitor( @@ -223,7 +244,7 @@ impl<'a> Scope<'a> { pub fn on_event( &mut self, event: Component>, - func: impl 'static + Send + Sync + FnMut(&Frame, &EntityRef, T), + func: impl 'static + Send + Sync + FnMut(&ScopeRef<'_>, T), ) -> &mut Self { self.set(event, Box::new(func) as _) } @@ -240,6 +261,86 @@ impl Drop for Scope<'_> { } } +/// A non-mutable view into a widgets scope. +/// +/// This is used for accessing state and modifying components (but not adding) of a widget during +/// callbacks. +pub struct ScopeRef<'a> { + frame: &'a Frame, + entity: EntityRef<'a>, +} + +impl<'a> std::fmt::Debug for ScopeRef<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ScopeRef") + .field("id", &self.entity.id()) + .finish_non_exhaustive() + } +} + +impl<'a> Deref for ScopeRef<'a> { + type Target = EntityRef<'a>; + + fn deref(&self) -> &Self::Target { + &self.entity + } +} + +impl<'a> ScopeRef<'a> { + pub fn new(frame: &'a Frame, entity: EntityRef<'a>) -> Self { + Self { frame, entity } + } + + pub fn entity(&self) -> &EntityRef { + &self.entity + } + + /// Returns the active stylesheet for this scope + pub fn stylesheet(&self) -> EntityRef { + get_stylesheet_from_entity(&self.entity()) + } + + /// Spawns an effect scoped to the lifetime of this entity and scope + pub fn spawn_effect(&self, effect: impl 'static + for<'x> Effect>) { + self.frame.spawn(ScopedEffect { + id: self.entity.id(), + effect, + }); + } + + pub fn spawn(&self, fut: impl 'static + Future) { + self.spawn_effect(FutureEffect::new(fut, |_: &mut Scope<'_>, _| {})) + } + + /// Spawns a scoped stream invoking the callback in with the widgets scope for each item + pub fn spawn_stream( + &mut self, + stream: S, + func: impl 'static + FnMut(&mut Scope<'_>, S::Item), + ) { + self.spawn_effect(StreamEffect::new(stream, func)) + } + + /// Spawns an effect which is *not* scoped to the widget + pub fn spawn_unscoped(&self, effect: impl 'static + for<'x> Effect) { + self.frame.spawn(effect); + } + + pub fn id(&self) -> Entity { + self.entity.id() + } + + pub fn frame(&self) -> &Frame { + self.frame + } + + pub fn read(&self, handle: WeakHandle) -> &T { + let store = self.frame.store().store::().expect("Handle is invalid"); + let handle = handle.upgrade(store).expect("Handle is invalid"); + self.frame.store().get(&handle) + } +} + #[pin_project] pub(crate) struct ScopedEffect { pub(crate) id: Entity, diff --git a/violet-core/src/stored.rs b/violet-core/src/stored.rs index b6ef8a7..f35dbee 100644 --- a/violet-core/src/stored.rs +++ b/violet-core/src/stored.rs @@ -178,6 +178,15 @@ pub struct UntypedHandle { } impl UntypedHandle { + pub fn new(handle: Handle) -> Self { + Self { + index: handle.index, + free_tx: handle.free_tx.clone(), + ty: TypeId::of::(), + refs: handle.refs.clone(), + } + } + pub fn downgrade(&self) -> WeakUntypedHandle { WeakUntypedHandle { index: self.index, diff --git a/violet-core/src/style/mod.rs b/violet-core/src/style/mod.rs index 3b09902..8ffd73f 100644 --- a/violet-core/src/style/mod.rs +++ b/violet-core/src/style/mod.rs @@ -15,8 +15,8 @@ use crate::{ }; use self::colors::{ - EERIE_BLACK_300, EERIE_BLACK_700, EERIE_BLACK_DEFAULT, JADE_400, JADE_600, JADE_DEFAULT, - LION_DEFAULT, PLATINUM_DEFAULT, REDWOOD_DEFAULT, + EERIE_BLACK_300, EERIE_BLACK_600, EERIE_BLACK_700, EERIE_BLACK_DEFAULT, JADE_400, JADE_600, + JADE_DEFAULT, LION_DEFAULT, PLATINUM_DEFAULT, REDWOOD_DEFAULT, }; #[macro_export] @@ -283,7 +283,7 @@ pub fn setup_stylesheet() -> EntityBuilder { .set(interactive_active(), JADE_DEFAULT) .set(interactive_hover(), JADE_600) .set(interactive_pressed(), JADE_400) - .set(interactive_inactive(), EERIE_BLACK_700) + .set(interactive_inactive(), EERIE_BLACK_600) // spacing .set(spacing_small(), 4.0.into()) .set(spacing_medium(), 8.0.into()) diff --git a/violet-core/src/systems.rs b/violet-core/src/systems.rs index 2070e83..3f943c2 100644 --- a/violet-core/src/systems.rs +++ b/violet-core/src/systems.rs @@ -1,6 +1,7 @@ use std::{ collections::HashSet, sync::{Arc, Weak}, + thread::scope, }; use atomic_refcell::AtomicRefCell; @@ -11,8 +12,9 @@ use flax::{ entity_ids, events::{EventData, EventSubscriber}, filter::Or, - BoxedSystem, CommandBuffer, Dfs, DfsBorrow, Entity, Fetch, FetchExt, FetchItem, Query, - QueryBorrow, System, World, + query::TopoBorrow, + BoxedSystem, CommandBuffer, Dfs, DfsBorrow, Entity, EntityBuilder, Fetch, FetchExt, FetchItem, + Query, QueryBorrow, System, Topo, World, }; use glam::Vec2; @@ -41,40 +43,37 @@ pub fn hydrate_text() -> BoxedSystem { .boxed() } -pub fn templating_system( - root: Entity, - layout_changes_tx: flume::Sender<(Entity, LayoutUpdate)>, -) -> BoxedSystem { - let query = Query::new(entity_ids()) - .filter(Or(( - screen_position().without(), - local_position().without(), - rect().without(), - screen_rect().without(), - ))) - .filter(root.traverse(child_of)); +pub fn widget_template(entity: &mut EntityBuilder, name: String) { + entity + .set(flax::components::name(), name) + .set_default(screen_position()) + .set_default(local_position()) + .set_default(screen_rect()) + .set_default(rect()); +} + +pub fn templating_system(layout_changes_tx: flume::Sender<(Entity, LayoutUpdate)>) -> BoxedSystem { + let query = Query::new(entity_ids()).filter(Or((rect().with(), layout_cache().without()))); System::builder() + .with_name("templating_system") .with_query(query) .with_cmd_mut() .build( move |mut query: QueryBorrow<_, _>, cmd: &mut CommandBuffer| { puffin::profile_scope!("templating_system"); - for id in &mut query { + for id in query.iter() { + puffin::profile_scope!("apply", format!("{id}")); tracing::debug!(%id, "incomplete widget"); let layout_changes_tx = layout_changes_tx.clone(); - cmd.set_missing(id, screen_position(), Vec2::ZERO) - .set_missing(id, local_position(), Vec2::ZERO) - .set_missing(id, screen_rect(), Rect::default()) - .set_missing( - id, - layout_cache(), - LayoutCache::new(Some(Box::new(move |layout| { - layout_changes_tx.send((id, layout)).ok(); - }))), - ) - .set_missing(id, rect(), Rect::default()); + cmd.set_missing( + id, + layout_cache(), + LayoutCache::new(Some(Box::new(move |layout| { + layout_changes_tx.send((id, layout)).ok(); + }))), + ); } }, ) diff --git a/violet-core/src/text.rs b/violet-core/src/text.rs index e760b84..3dc1993 100644 --- a/violet-core/src/text.rs +++ b/violet-core/src/text.rs @@ -206,6 +206,7 @@ impl LayoutGlyphs { None } + /// Returns all layout lines for the specified row pub fn find_lines(&self, row: usize) -> impl Iterator { self.lines .iter() @@ -257,7 +258,7 @@ impl Index for LayoutGlyphs { } } -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct CursorLocation { /// The row index of the non-wrapped original text pub row: usize, diff --git a/violet-core/src/types.rs b/violet-core/src/types.rs index e696ec4..67a4fba 100644 --- a/violet-core/src/types.rs +++ b/violet-core/src/types.rs @@ -138,6 +138,10 @@ impl Display for Rect { } impl Rect { + pub fn new(min: Vec2, max: Vec2) -> Self { + Self { min, max } + } + pub const ZERO: Self = Self { min: Vec2::ZERO, max: Vec2::ZERO, diff --git a/violet-core/src/widget/container.rs b/violet-core/src/widget/container.rs index 3b0609d..866db0c 100644 --- a/violet-core/src/widget/container.rs +++ b/violet-core/src/widget/container.rs @@ -5,7 +5,7 @@ use winit::event::ElementState; use crate::{ components::{anchor, layout, offset, rect}, input::{focusable, on_cursor_move, on_mouse_input}, - layout::{Alignment, Direction, FlowLayout, Layout, StackLayout}, + layout::{Alignment, Direction, FloatLayout, FlowLayout, Layout, StackLayout}, style::{ primary_background, secondary_background, spacing_medium, spacing_small, Background, SizeExt, StyleExt, WidgetSize, @@ -204,16 +204,16 @@ impl Widget for Movable { .set(offset(), Unit::default()) .on_event(on_mouse_input(), { let start_offset = start_offset.clone(); - move |_, _, input| { + move |_, input| { if input.state == ElementState::Pressed { let cursor_pos = input.cursor.local_pos; *start_offset.lock_mut() = cursor_pos; } } }) - .on_event(on_cursor_move(), move |frame, entity, input| { - let rect = entity.get_copy(rect()).unwrap(); - let anchor = entity + .on_event(on_cursor_move(), move |scope, input| { + let rect = scope.get_copy(rect()).unwrap(); + let anchor = scope .get_copy(anchor()) .unwrap_or_default() .resolve(rect.size()); @@ -221,14 +221,35 @@ impl Widget for Movable { let cursor_pos = input.local_pos + rect.min; let new_offset = cursor_pos - start_offset.get() + anchor; - let new_offset = (self.on_move)(frame, new_offset); - entity.update_dedup(offset(), Unit::px(new_offset)); + let new_offset = (self.on_move)(scope.frame(), new_offset); + scope.update_dedup(offset(), Unit::px(new_offset)); }); Stack::new(self.content).mount(scope) } } +pub struct Float { + items: W, +} + +impl Float { + pub fn new(items: W) -> Self { + Self { items } + } +} + +impl Widget for Float +where + W: WidgetCollection, +{ + fn mount(self, scope: &mut Scope<'_>) { + self.items.attach(scope); + + scope.set(layout(), Layout::Float(FloatLayout {})); + } +} + pub fn row(widgets: W) -> List { List::new(widgets).with_direction(Direction::Horizontal) } diff --git a/violet-core/src/widget/interactive/button.rs b/violet-core/src/widget/interactive/button.rs index b743fb0..0aacf31 100644 --- a/violet-core/src/widget/interactive/button.rs +++ b/violet-core/src/widget/interactive/button.rs @@ -5,6 +5,7 @@ use crate::{ components::{self, color}, input::{focusable, on_mouse_input}, layout::Alignment, + scope::ScopeRef, state::{StateDuplex, StateStream, WatchState}, style::{ danger_item, interactive_inactive, interactive_pressed, spacing_medium, success_item, @@ -15,7 +16,7 @@ use crate::{ Frame, Scope, Widget, }; -type ButtonCallback = Box; +type ButtonCallback = Box, winit::event::MouseButton)>; #[derive(Debug, Clone)] pub struct ButtonStyle { @@ -59,7 +60,7 @@ impl Button { /// Handle the button press pub fn on_press( mut self, - on_press: impl 'static + Send + Sync + FnMut(&Frame, MouseButton), + on_press: impl 'static + Send + Sync + FnMut(&ScopeRef<'_>, MouseButton), ) -> Self { self.on_press = Box::new(on_press); self @@ -111,12 +112,12 @@ impl Widget for Button { scope .set(focusable(), ()) - .on_event(on_mouse_input(), move |frame, entity, input| { + .on_event(on_mouse_input(), move |scope, input| { if input.state == ElementState::Pressed { - entity.update_dedup(color(), pressed_color); - (self.on_press)(frame, input.button); + scope.update_dedup(color(), pressed_color); + (self.on_press)(scope, input.button); } else { - entity.update_dedup(color(), normal_color); + scope.update_dedup(color(), normal_color); } }); @@ -169,7 +170,7 @@ impl Widget for Checkbox { scope .set(focusable(), ()) - .on_event(on_mouse_input(), move |_, _, input| { + .on_event(on_mouse_input(), move |_, input| { if input.state == ElementState::Pressed { if let Some(state) = last_state.get() { self.state.send(!state) @@ -225,7 +226,7 @@ impl Widget for Radio { scope .set(focusable(), ()) - .on_event(on_mouse_input(), move |_, _, input| { + .on_event(on_mouse_input(), move |_, input| { if input.state == ElementState::Pressed { self.state.send(true) } diff --git a/violet-core/src/widget/interactive/input.rs b/violet-core/src/widget/interactive/input.rs index bde662a..b721330 100644 --- a/violet-core/src/widget/interactive/input.rs +++ b/violet-core/src/widget/interactive/input.rs @@ -1,11 +1,11 @@ use core::panic; use std::{fmt::Display, future::ready, str::FromStr, sync::Arc}; -use futures::{FutureExt, StreamExt}; +use futures::StreamExt; use futures_signals::signal::{self, Mutable, SignalExt}; use glam::{vec2, Vec2}; use itertools::Itertools; -use palette::Srgba; +use palette::{Srgba, WithAlpha}; use web_time::Duration; use winit::{ event::ElementState, @@ -15,25 +15,30 @@ use winit::{ use crate::{ components::{self, screen_rect}, editor::{CursorMove, EditAction, EditorAction, TextEditor}, - input::{focus_sticky, focusable, on_focus, on_keyboard_input, on_mouse_input, KeyboardInput}, + input::{ + focus_sticky, focusable, on_cursor_move, on_focus, on_keyboard_input, on_mouse_input, + KeyboardInput, + }, + io, state::{State, StateDuplex, StateSink, StateStream}, style::{ - interactive_active, interactive_inactive, spacing_small, Background, SizeExt, StyleExt, - ValueOrRef, WidgetSize, + interactive_active, interactive_hover, interactive_inactive, spacing_small, Background, + SizeExt, StyleExt, ValueOrRef, WidgetSize, }, - text::{LayoutGlyphs, TextSegment}, + text::{CursorLocation, LayoutGlyphs, TextSegment}, time::sleep, to_owned, unit::Unit, utils::throttle, widget::{ - row, NoOp, Positioned, Rectangle, SignalWidget, Stack, StreamWidget, Text, WidgetExt, + row, Float, NoOp, Positioned, Rectangle, SignalWidget, Stack, StreamWidget, Text, WidgetExt, }, Rect, Scope, Widget, }; pub struct TextInputStyle { pub cursor_color: ValueOrRef, + pub selection_color: ValueOrRef, pub background: Background, pub font_size: f32, } @@ -42,6 +47,7 @@ impl Default for TextInputStyle { fn default() -> Self { Self { cursor_color: interactive_active().into(), + selection_color: interactive_hover().into(), background: Background::new(interactive_inactive()), font_size: 16.0, } @@ -87,6 +93,11 @@ impl Widget for TextInput { let stylesheet = scope.stylesheet(); let cursor_color = self.style.cursor_color.resolve(stylesheet); + let selection_color = self + .style + .selection_color + .resolve(stylesheet) + .with_alpha(0.2); let (tx, rx) = flume::unbounded(); @@ -100,24 +111,30 @@ impl Widget for TextInput { let layout_glyphs = Mutable::new(None); let text_bounds: Mutable> = Mutable::new(None); - // editor.set_text(content.lock_mut().split('\n')); editor.set_cursor_at_end(); let (editor_props_tx, editor_props_rx) = signal::channel(Box::new(NoOp) as Box); let content = self.content.prevent_feedback(); + let clipboard = scope + .frame() + .get_atom(io::clipboard()) + .expect("Missing clipboard") + .clone(); + + let clipboard = scope.frame().store().get(&clipboard).clone(); + scope.spawn({ - let mut layout_glyphs = layout_glyphs.signal_cloned().to_stream(); + let mut layout_glyphs = layout_glyphs.signal_cloned().to_stream().fuse(); let mut focused_signal = focused.stream().fuse(); to_owned![text_content]; async move { let mut rx = rx.into_stream().fuse(); - let mut glyphs: LayoutGlyphs; + let mut glyphs: Option = None; - let mut cursor_pos = Vec2::ZERO; - - let mut new_text = throttle(content.stream(), || sleep(Duration::from_millis(100))).fuse(); + let mut new_text = + throttle(content.stream(), || sleep(Duration::from_millis(100))).fuse(); let mut focused = false; loop { @@ -130,7 +147,25 @@ impl Widget for TextInput { text_content.send(new_text); } action = rx.select_next_some() => { - editor.apply_action(action); + match action { + Action::Editor(editor_action) => editor.apply_action(editor_action), + Action::Copy => { + if let Some(sel) = editor.selected_text() { + clipboard.set_text(sel.join("\n")).await; + } + } + Action::Paste => { + if let Some(text) = clipboard.get_text().await { + editor.edit(EditAction::InsertText(text)); + } + } + Action::Cut => { + if let Some(sel) = editor.selected_text() { + clipboard.set_text(sel.join("\n")).await; + editor.delete_selected_text(); + } + } + } let mut text = text_content.lock_mut(); text.clear(); @@ -140,68 +175,147 @@ impl Widget for TextInput { content.send(editor.lines().iter().map(|v| v.text()).join("\n")); // text_content.send(editor.lines().iter().map(|v| v.text()).join("\n")); } - new_glyphs = layout_glyphs.next().fuse() => { - if let Some(Some(new_glyphs)) = new_glyphs { + new_glyphs = layout_glyphs.select_next_some() => { glyphs = new_glyphs; - if let Some(loc) = glyphs.to_glyph_boundary(editor.cursor()) { - cursor_pos = loc; - } else if editor.past_eol() { - cursor_pos = glyphs - .find_lines_indices(editor.cursor().row) - .last() - .map(|(ln, line)| { - vec2(line.bounds.max.x, ln as f32 * glyphs.line_height()) - }) - .unwrap_or_default(); - } else { - cursor_pos = Vec2::ZERO; - } } - } } - editor_props_tx - .send(Box::new(Stack::new( - ( - focused.then(|| Positioned::new(Rectangle::new(cursor_color) - .with_min_size(Unit::px2(2.0, 16.0))) - .with_offset(Unit::px(cursor_pos))), + if let Some(glyphs) = &glyphs { + let cursor_pos = calculate_position(glyphs, editor.cursor()); + + let selection = if let Some((start, end)) = editor.selection_bounds() { + tracing::info!(?start, ?end, "selection"); + + let selected_lines = + glyphs.lines().iter().enumerate().filter(|(_, v)| { + tracing::info!(?v.row); + v.row >= start.row && v.row <= end.row + }); + + let selection = selected_lines + .filter_map(|(ln, v)| { + tracing::info!(?ln, glyphs = v.glyphs.len()); + + let left = if v.row == start.row { + v.glyphs.iter().find(|v| { + v.start >= start.col + && (start.row != end.row || v.start < end.col) + }) + } else { + // None + v.glyphs.first() + }?; + let right = if v.row == end.row { + v.glyphs.iter().rev().find(|v| { + v.end <= end.col + && (start.row != end.row || v.end > start.col) + }) + } else { + // None + v.glyphs.last() + }?; + + // dbg!(left, right); + + let rect = Rect::new( + left.bounds.min - vec2(0.0, 2.0), + right.bounds.max + vec2(0.0, 2.0), + ); + + Some( + Positioned::new( + Rectangle::new(selection_color) + .with_min_size(Unit::px(rect.size())), + ) + .with_offset(Unit::px(rect.pos())), ) - ))) - .ok(); + }) + .collect_vec(); + + Some(Stack::new(selection)) + } else { + None + }; + let props = Stack::new(( + focused.then(|| { + Positioned::new( + Rectangle::new(cursor_color) + .with_min_size(Unit::px2(2.0, 16.0)), + ) + .with_offset(Unit::px(cursor_pos)) + }), + selection, + )); + + editor_props_tx.send(Box::new(props)).ok(); } + } } }); + let dragging = Mutable::new(None); + scope .set(focusable(), ()) .set(focus_sticky(), ()) - .on_event(on_focus(), move |_, _, focus| { + .on_event(on_focus(), move |_, focus| { focused.set(focus); }) .on_event(on_mouse_input(), { - to_owned![layout_glyphs, text_bounds, tx]; - move |_, _, input| { + to_owned![layout_glyphs, text_bounds, tx, dragging]; + move |_, input| { let glyphs = layout_glyphs.lock_ref(); if let (Some(glyphs), Some(text_bounds)) = (&*glyphs, &*text_bounds.lock_ref()) { if input.state == ElementState::Pressed { let text_pos = input.cursor.absolute_pos - text_bounds.min; + if let Some(hit) = glyphs.hit(text_pos) { - tx.send(EditorAction::CursorMove(CursorMove::SetPosition(hit))) - .ok(); + dragging.set(Some(hit)); + tx.send(Action::Editor(EditorAction::CursorMove( + CursorMove::SetPosition(hit), + ))) + .ok(); + tx.send(Action::Editor(EditorAction::SelectionClear)).ok(); } tracing::info!(?input, "click"); + } else { + dragging.set(None) + } + } + } + }) + .on_event(on_cursor_move(), { + to_owned![layout_glyphs, tx, dragging]; + move |_, input| { + let dragging = dragging.get(); + + if let Some(dragging) = dragging { + let glyphs = layout_glyphs.lock_ref(); + + if let Some(glyphs) = &*glyphs { + let text_pos = input.local_pos; + + if let Some(hit) = glyphs.hit(text_pos) { + tx.send(Action::Editor(EditorAction::SelectionMove( + CursorMove::SetPosition(dragging), + ))) + .ok(); + tx.send(Action::Editor(EditorAction::CursorMove( + CursorMove::SetPosition(hit), + ))) + .ok(); + } } } } }) .on_event(on_keyboard_input(), { to_owned![tx]; - move |_, _, input| { + move |_, input| { if input.event.state == ElementState::Pressed { if let Some(action) = handle_input(input) { tx.send(action).ok(); @@ -218,7 +332,7 @@ impl Widget for TextInput { .monitor_signal(components::layout_glyphs(), layout_glyphs.clone()) .monitor_signal(screen_rect(), text_bounds.clone()) })), - SignalWidget(editor_props_rx), + Float::new(SignalWidget(editor_props_rx)), )) .with_background(self.style.background) .with_size_props(self.size) @@ -226,31 +340,79 @@ impl Widget for TextInput { } } -fn handle_input(input: KeyboardInput) -> Option { +enum Action { + Editor(EditorAction), + Copy, + Paste, + Cut, +} + +pub fn calculate_position(glyphs: &LayoutGlyphs, cursor: CursorLocation) -> Vec2 { + if let Some(loc) = glyphs.to_glyph_boundary(cursor) { + loc + } else { + glyphs + .find_lines_indices(cursor.row) + .last() + .map(|(ln, line)| vec2(line.bounds.max.x, ln as f32 * glyphs.line_height())) + .unwrap_or_default() + } +} + +fn handle_input(input: KeyboardInput) -> Option { let ctrl = input.modifiers.control_key(); if let Key::Named(key) = input.event.logical_key { match key { NamedKey::Backspace if ctrl => { - return Some(EditorAction::Edit(EditAction::DeleteBackwardWord)) + return Some(Action::Editor(EditorAction::Edit( + EditAction::DeleteBackwardWord, + ))) + } + NamedKey::Backspace => { + return Some(Action::Editor(EditorAction::Edit( + EditAction::DeleteBackwardChar, + ))) + } + NamedKey::Enter => { + return Some(Action::Editor(EditorAction::Edit(EditAction::InsertLine))) } - NamedKey::Backspace => return Some(EditorAction::Edit(EditAction::DeleteBackwardChar)), - NamedKey::Enter => return Some(EditorAction::Edit(EditAction::InsertLine)), NamedKey::ArrowLeft if ctrl => { - return Some(EditorAction::CursorMove(CursorMove::BackwardWord)) + return Some(Action::Editor(EditorAction::CursorMove( + CursorMove::BackwardWord, + ))) } NamedKey::ArrowRight if ctrl => { - return Some(EditorAction::CursorMove(CursorMove::ForwardWord)) + return Some(Action::Editor(EditorAction::CursorMove( + CursorMove::ForwardWord, + ))) + } + NamedKey::ArrowLeft => { + return Some(Action::Editor(EditorAction::CursorMove(CursorMove::Left))) } - NamedKey::ArrowLeft => return Some(EditorAction::CursorMove(CursorMove::Left)), - NamedKey::ArrowRight => return Some(EditorAction::CursorMove(CursorMove::Right)), - NamedKey::ArrowUp => return Some(EditorAction::CursorMove(CursorMove::Up)), - NamedKey::ArrowDown => return Some(EditorAction::CursorMove(CursorMove::Down)), + NamedKey::ArrowRight => { + return Some(Action::Editor(EditorAction::CursorMove(CursorMove::Right))) + } + NamedKey::ArrowUp => { + return Some(Action::Editor(EditorAction::CursorMove(CursorMove::Up))) + } + NamedKey::ArrowDown => { + return Some(Action::Editor(EditorAction::CursorMove(CursorMove::Down))) + } + _ => {} + } + } else if let Key::Character(c) = input.event.logical_key { + match &*c { + "c" if ctrl => return Some(Action::Copy), + "v" if ctrl => return Some(Action::Paste), + "x" if ctrl => return Some(Action::Cut), _ => {} } } if let Some(text) = input.event.text { - return Some(EditorAction::Edit(EditAction::InsertText(text.into()))); + return Some(Action::Editor(EditorAction::Edit(EditAction::InsertText( + text.into(), + )))); } None diff --git a/violet-core/src/widget/interactive/slider.rs b/violet-core/src/widget/interactive/slider.rs index 718b2d9..c15827d 100644 --- a/violet-core/src/widget/interactive/slider.rs +++ b/violet-core/src/widget/interactive/slider.rs @@ -125,15 +125,15 @@ impl Widget for Slider { .set(focusable(), ()) .on_event(on_mouse_input(), { to_owned![value]; - move |_, entity, input| { + move |scope, input| { if input.state == ElementState::Pressed { - update(entity, input.cursor, min, max, &*value); + update(scope, input.cursor, min, max, &*value); } } }) .on_event(on_cursor_move(), { to_owned![value]; - move |_, entity, input| update(entity, input, min, max, &*value) + move |scope, input| update(scope, input, min, max, &*value) }); Stack::new(handle) diff --git a/violet-core/src/widget/mod.rs b/violet-core/src/widget/mod.rs index babdc1e..c997a60 100644 --- a/violet-core/src/widget/mod.rs +++ b/violet-core/src/widget/mod.rs @@ -32,9 +32,9 @@ where } } -impl Widget for Box -where - T: ?Sized + Widget, +impl Widget for Box +// where +// T: ?Sized + Widget, { fn mount(self, scope: &mut Scope<'_>) { self.mount_boxed(scope) @@ -49,6 +49,15 @@ impl Widget for Option { } } +impl Widget for F +where + F: FnOnce(&mut Scope<'_>), +{ + fn mount(self, scope: &mut Scope<'_>) { + self(scope); + } +} + pub trait WidgetExt: Widget + Sized { fn boxed<'a>(self) -> Box where diff --git a/violet-demo/src/lib.rs b/violet-demo/src/lib.rs index 7422540..8b21793 100644 --- a/violet-demo/src/lib.rs +++ b/violet-demo/src/lib.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, sync::Arc}; use anyhow::Context; use flume::Sender; -use futures::{Stream, StreamExt}; +use futures::{Future, Stream, StreamExt}; use glam::Vec2; use indexmap::IndexMap; use itertools::Itertools; @@ -23,10 +23,10 @@ use violet::{ unit::Unit, utils::zip_latest_ref, widget::{ - card, column, label, row, Button, Radio, Rectangle, SliderWithLabel, Stack, + card, centered, column, label, row, Button, Radio, Rectangle, SliderWithLabel, Stack, StreamWidget, Text, TextInput, WidgetExt, }, - Edges, Frame, FutureEffect, Scope, Widget, + Edges, Scope, Widget, }, futures_signals::signal::Mutable, palette::{FromColor, IntoColor, OklabHue, Oklch, Srgb}, @@ -213,20 +213,22 @@ impl Widget for Palettes { 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 editor_column = column((StreamWidget(editor), palettes, new_color)); + column(( menu_bar(self.items.clone(), notify_tx), - StreamWidget(editor), - palettes, - 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)); - }) - }), + row((editor_column, description())), )) .mount(scope) } @@ -305,20 +307,31 @@ where } } +fn description() -> impl Widget { + let content = Mutable::new( + r#"This is a palette editor. You can add, remove and select the colors in the list. Edit the color by selecting them and using the sliders or typing in the slider labels +You can then export the various generated tints of the colors to a tailwind style `.json` + +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 { - fn notify_result( - notify_tx: &Sender, + async fn notify_result( + fut: impl Future>, + notify_tx: Sender, on_success: &str, - ) -> impl Fn(&mut Frame, anyhow::Result<()>) { - let notify_tx = notify_tx.clone(); - move |_, result| match result { + ) { + match fut.await { Ok(()) => { notify_tx .send(Notification { - message: "Saved".to_string(), + message: on_success.into(), kind: NotificationKind::Info, }) .unwrap(); @@ -326,7 +339,7 @@ fn menu_bar( Err(e) => { notify_tx .send(Notification { - message: format!("Failed to save: {e}"), + message: format!("{e:?}"), kind: NotificationKind::Error, }) .unwrap(); @@ -370,7 +383,7 @@ fn menu_bar( Ok(()) }; - frame.spawn(FutureEffect::new(fut, notify_result(¬ify_tx, "Saves"))); + frame.spawn(notify_result(fut, notify_tx.clone(), "Saves")); } }); @@ -394,7 +407,7 @@ fn menu_bar( Ok(()) }; - frame.spawn(FutureEffect::new(fut, notify_result(¬ify_tx, "Saves"))); + frame.spawn(notify_result(fut, notify_tx, "Saves")); } }); @@ -416,7 +429,7 @@ fn menu_bar( Ok(()) }; - frame.spawn(FutureEffect::new(fut, notify_result(¬ify_tx, "Loaded"))); + frame.spawn(notify_result(fut, notify_tx, "Loaded")); } }); @@ -433,12 +446,13 @@ fn menu_bar( }); row(( - label("Palette editor"), + centered(label("Palette editor")), save, load, export, test_notification, )) + .with_stretch(true) } #[derive(Clone, Serialize, Deserialize)] diff --git a/violet-wgpu/src/app.rs b/violet-wgpu/src/app.rs index c2391f5..d2e9475 100644 --- a/violet-wgpu/src/app.rs +++ b/violet-wgpu/src/app.rs @@ -18,6 +18,7 @@ use violet_core::{ components::{self, local_position, rect, screen_position}, executor::Executor, input::InputState, + io::{self, Clipboard}, style::{setup_stylesheet, stylesheet}, systems::{ hydrate_text, invalidate_cached_layout_system, layout_system, templating_system, @@ -124,6 +125,9 @@ impl AppBuilder { let stylesheet = setup_stylesheet().spawn(frame.world_mut()); + let clipboard = frame.store_mut().insert(Arc::new(Clipboard::new())); + frame.set_atom(io::clipboard(), clipboard); + // Mount the root widget let root = frame.new_root(Canvas { stylesheet, @@ -158,7 +162,7 @@ impl AppBuilder { })); let schedule = Schedule::new() - .with_system(templating_system(root, layout_changes_tx)) + .with_system(templating_system(layout_changes_tx)) .flush() .with_system(hydrate_text()) .flush() diff --git a/violet-wgpu/src/text.rs b/violet-wgpu/src/text.rs index 057a026..60606bd 100644 --- a/violet-wgpu/src/text.rs +++ b/violet-wgpu/src/text.rs @@ -242,15 +242,6 @@ impl TextBufferState { Attrs::new(), Shaping::Advanced, ); - // self.buffer.set_text( - // font_system, - // text, - // Attrs::new() - // .family(cosmic_text::Family::Name("Inter")) - // .style(Style::Normal) - // .weight(400.0) - // Shaping::Advanced, - // ); } fn text(&self) -> Vec { @@ -267,6 +258,7 @@ impl TextBufferState { let mut result = Vec::new(); + let mut ln = 0; for (row, line) in self.buffer.lines.iter().enumerate() { let mut current_offset = 0; @@ -275,10 +267,12 @@ impl TextBufferState { continue; }; - result.extend(layout.iter().enumerate().map(|(i, run)| { - let top = i as f32 * lh; + result.extend(layout.iter().map(|run| { + let top = ln as f32 * lh; let bottom = top + lh; + ln += 1; + let start = current_offset; let glyphs = run .glyphs From c336744d8cacffe3cfd9e7c0165e0a42be94ca2f Mon Sep 17 00:00:00 2001 From: Freja Roberts Date: Wed, 20 Mar 2024 15:42:13 +0100 Subject: [PATCH 10/16] chore: update submodule --- flax | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flax b/flax index d0ba35e..f75c463 160000 --- a/flax +++ b/flax @@ -1 +1 @@ -Subproject commit d0ba35eacd28423cbb7f8e96b3d3c45a6a6cf683 +Subproject commit f75c463de4fc8390c7f71b211b8cf0a26782bf12 From 964adedd397b112e465db0bd7200a97fd38a0083 Mon Sep 17 00:00:00 2001 From: Freja Roberts Date: Wed, 20 Mar 2024 16:54:20 +0100 Subject: [PATCH 11/16] tmp --- Cargo.lock | 7 +++ Cargo.toml | 1 + colors.js | 105 ++++------------------------------------- violet-demo/Cargo.toml | 1 + violet-demo/src/lib.rs | 88 ++++++++++++++++++---------------- violet-wgpu/src/app.rs | 39 ++++++++++----- 6 files changed, 93 insertions(+), 148 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a093c2c..2d76c04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1420,6 +1420,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hex" version = "0.4.3" @@ -3214,6 +3220,7 @@ dependencies = [ "flume", "futures", "glam", + "heck", "indexmap", "itertools 0.12.1", "puffin", diff --git a/Cargo.toml b/Cargo.toml index e2a9c92..93c1678 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,6 +78,7 @@ wasm-bindgen-futures = "0.4" wasm-bindgen = "0.2" web-sys = { version = "0.3", features = ["Clipboard"] } tracing-tree = "0.3" +heck = "0.5" [dependencies] violet-wgpu = { path = "violet-wgpu" } diff --git a/colors.js b/colors.js index 4373758..6ed27dc 100644 --- a/colors.js +++ b/colors.js @@ -1,111 +1,24 @@ -const colors = { - eerie_black: { - '50': '#d9d9d9', - '100': '#cccccc', - '200': '#b5b5b5', - '300': '#949494', - '400': '#6b6b6b', - '500': '#525252', - '600': '#404040', - '700': '#333333', - '800': '#292929', - '900': '#212121', - '950': '#1b1b1b', - }, - platinum: { - '50': '#f6f6f6', - '100': '#e5e4e2', - '200': '#d6d4d2', - '300': '#bcbab5', - '400': '#a19c96', - '500': '#8e8781', - '600': '#817a75', - '700': '#6c6662', - '800': '#5a5552', - '900': '#4a4744', - '950': '#272423', - }, - emerald: { - '50': '#effaf2', - '100': '#d9f2dd', - '200': '#b5e5c1', - '300': '#85d09b', - '400': '#57b777', - '500': '#309956', - '600': '#207b44', - '700': '#1a6238', - '800': '#174e2e', - '900': '#144028', - '950': '#0a2416', - }, - cyan: { - '50': '#f3faf9', - '100': '#d8efed', - '200': '#b0dfdb', - '300': '#80c8c4', - '400': '#56aba9', - '500': '#409999', - '600': '#2e7173', - '700': '#285b5d', - '800': '#24494b', - '900': '#213f40', - '950': '#0f2224', - }, - ultra_violet: { - '50': '#f1f2fc', - '100': '#e6e6f9', - '200': '#d1d1f4', - '300': '#b6b5ec', - '400': '#9f97e2', - '500': '#8d7dd7', - '600': '#7e63c8', - '700': '#6d53af', - '800': '#534185', - '900': '#4a3d72', - '950': '#2c2442', - }, - redwood: { - '50': '#fbf6f5', - '100': '#f8eae8', - '200': '#f2d9d6', - '300': '#e8beb9', - '400': '#da978f', - '500': '#c8756b', - '600': '#b35a4f', - '700': '#96493f', - '800': '#7d3f37', - '900': '#693933', - '950': '#381b17', - }, - lion: { - '50': '#f8f6ee', - '100': '#eee9d3', - '200': '#ded2aa', - '300': '#cbb479', - '400': '#bb9954', - '500': '#b38c49', - '600': '#946c3a', - '700': '#775131', - '800': '#64442f', - '900': '#573b2c', - '950': '#321f16', - }, -}; +colors = require("colors.json") console.log(`use palette::Srgba; use crate::srgba; `); +function kebabToSnakeCase(kebabCaseString) { + return kebabCaseString.replace(/-/g, '_').toUpperCase(); +} + for (var color_name in colors) { 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( - `pub const ${color_name.toUpperCase()}_${tint.toUpperCase()}: Srgba = srgba!("${color}"); `, + `pub const ${color_name}_${tint.toUpperCase()}: Srgba = srgba!("${color}"); `, ); } - const tint_names = Object.keys(tints).map((tint) => ` ${color_name.toUpperCase()}_${tint.toUpperCase()},\n`).join(""); - console.log(`pub const ${color_name.toUpperCase()}_TINTS: [Srgba; ${Object.keys(tints).length}] = [\n${tint_names}];`) + 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}];`) } diff --git a/violet-demo/Cargo.toml b/violet-demo/Cargo.toml index 9c85582..34ccb1d 100644 --- a/violet-demo/Cargo.toml +++ b/violet-demo/Cargo.toml @@ -35,6 +35,7 @@ indexmap.workspace = true rfd.workspace = true anyhow.workspace = true flume.workspace = true +heck.workspace = true [package.metadata.wasm-pack.profile.profiling] wasm-opt = false diff --git a/violet-demo/src/lib.rs b/violet-demo/src/lib.rs index 8b21793..c8cc8d1 100644 --- a/violet-demo/src/lib.rs +++ b/violet-demo/src/lib.rs @@ -1,9 +1,10 @@ -use std::{collections::HashMap, sync::Arc}; +use std::sync::Arc; use anyhow::Context; use flume::Sender; use futures::{Future, Stream, StreamExt}; use glam::Vec2; +use heck::ToKebabCase; use indexmap::IndexMap; use itertools::Itertools; use rfd::AsyncFileDialog; @@ -75,6 +76,7 @@ pub fn run() { setup(); App::builder() + .with_title("Palette Editor") .with_renderer_config(RendererConfig { debug_mode: false }) .run(MainApp) .unwrap(); @@ -92,7 +94,7 @@ impl Widget for MainApp { Mutable::new(PaletteColor { color: Oklch::new(0.5, 0.27, (i as f32 * 60.0) % 360.0), falloff: DEFAULT_FALLOFF, - name: format!("color_{i}"), + name: format!("Color {i}"), }) }) .collect(), @@ -118,14 +120,15 @@ impl Widget for MainApp { fn tints(color: impl StateStream) -> impl Widget { puffin::profile_function!(); - row((1..=9) - .map(move |i| { + row(TINTS + .iter() + .map(move |&i| { let color = color.stream().map(move |v| { - let f = (i as f32) / 10.0; + let f = (i as f32) / 1000.0; let color = v.tint(f); Rectangle::new(ValueOrRef::value(color.into_color())) - .with_size(Unit::px2(80.0, 60.0)) + .with_size(Unit::px2(120.0, 80.0)) }); Stack::new(column(StreamWidget(color))) @@ -218,7 +221,7 @@ impl Widget for Palettes { 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), + name: format!("Color {}", v.len() + 1), })); current_choice.set(Some(v.len() - 1)); }) @@ -239,7 +242,7 @@ struct Notification { kind: NotificationKind, } -enum NotificationKind { +pub enum NotificationKind { Info, Warning, Error, @@ -307,6 +310,17 @@ where } } +fn local_dir() -> std::path::PathBuf { + #[cfg(not(target_arch = "wasm32"))] + { + std::env::current_dir().unwrap() + } + #[cfg(target_arch = "wasm32")] + { + PathBuf::from(".") + } +} + fn description() -> impl Widget { let content = Mutable::new( r#"This is a palette editor. You can add, remove and select the colors in the list. Edit the color by selecting them and using the sliders or typing in the slider labels @@ -347,7 +361,7 @@ fn menu_bar( } } - let export = Button::label("Export Tints").on_press({ + let export = Button::label("Export Json").on_press({ to_owned![items, notify_tx]; move |frame, _| { let data = items @@ -355,24 +369,30 @@ fn menu_bar( .iter() .map(|item| { let item = item.lock_ref(); - let tints = (1..=9) - .map(|i| { - let color = item.tint(i as f32 / 10.0); + let tints = TINTS + .iter() + .map(|&i| { + let color = item.tint(i as f32 / 1000.0); ( - format!("{}", i * 100), + format!("{}", i), HexColor(Srgb::from_color(color).into_format()), ) }) .collect::>(); - (item.name.clone(), tints) + (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(".").save_file().await else { + let Some(file) = AsyncFileDialog::new() + .set_directory(local_dir()) + .set_file_name("colors.json") + .save_file() + .await + else { return anyhow::Ok(()); }; @@ -392,7 +412,12 @@ fn menu_bar( move |frame, _| { to_owned![items, notify_tx]; let fut = async move { - let Some(file) = AsyncFileDialog::new().set_directory(".").save_file().await else { + let Some(file) = AsyncFileDialog::new() + .set_directory(local_dir()) + .set_file_name("colors.save.json") + .save_file() + .await + else { return anyhow::Ok(()); }; @@ -416,7 +441,11 @@ fn menu_bar( move |frame, _| { to_owned![items, notify_tx]; let fut = async move { - let Some(file) = AsyncFileDialog::new().set_directory(".").pick_file().await else { + let Some(file) = AsyncFileDialog::new() + .set_directory(local_dir()) + .pick_file() + .await + else { return anyhow::Ok(()); }; @@ -597,27 +626,4 @@ impl<'de> Deserialize<'de> for HexColor { } } -#[derive(serde::Serialize, serde::Deserialize)] -pub struct TintsData { - #[serde(rename = "100")] - _100: HexColor, - #[serde(rename = "200")] - _200: HexColor, - #[serde(rename = "300")] - _300: HexColor, - #[serde(rename = "400")] - _400: HexColor, - #[serde(rename = "500")] - _500: HexColor, - #[serde(rename = "600")] - _600: HexColor, - #[serde(rename = "700")] - _700: HexColor, - #[serde(rename = "800")] - _800: HexColor, - #[serde(rename = "900")] - _900: HexColor, -} - -#[derive(serde::Serialize, serde::Deserialize)] -pub struct ColorPalettes(HashMap); +static TINTS: &[i32] = &[50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]; diff --git a/violet-wgpu/src/app.rs b/violet-wgpu/src/app.rs index d2e9475..5ce6ad8 100644 --- a/violet-wgpu/src/app.rs +++ b/violet-wgpu/src/app.rs @@ -2,7 +2,7 @@ use futures::channel::oneshot; use std::sync::Arc; use web_time::{Duration, Instant}; -use flax::{components::name, Entity, Schedule, World}; +use flax::{components::name, entity_ids, Entity, Query, Schedule, World}; use glam::{vec2, Vec2}; use parking_lot::Mutex; use winit::{ @@ -61,15 +61,22 @@ impl Widget for Canvas { pub struct AppBuilder { renderer_config: RendererConfig, + title: String, } impl AppBuilder { pub fn new() -> Self { Self { renderer_config: Default::default(), + title: "Violet".to_string(), } } + pub fn with_title(mut self, title: impl Into) -> Self { + self.title = title.into(); + self + } + /// Set the renderer config pub fn with_renderer_config(mut self, renderer_config: RendererConfig) -> Self { self.renderer_config = renderer_config; @@ -86,7 +93,9 @@ impl AppBuilder { let event_loop = EventLoopBuilder::new().build()?; #[allow(unused_mut)] - let mut builder = WindowBuilder::new().with_inner_size(PhysicalSize::new(1920, 1080)); + let mut builder = WindowBuilder::new() + .with_inner_size(PhysicalSize::new(1920, 1080)) + .with_title(self.title); #[cfg(target_arch = "wasm32")] { @@ -175,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, @@ -203,11 +212,19 @@ impl AppBuilder { instance.update(); if !instance.is_minimized() { - let report = instance.stats.report(); - window.set_title(&format!( - "Violet - {:>4.1?} {:>4.1?} {:>4.1?}", - report.min_frame_time, report.average_frame_time, report.max_frame_time, - )); + let archetypes = instance.frame.world.archetype_info(); + let pruned = instance.frame.world.prune_archetypes(); + let entity_count = Query::new(entity_ids()) + .borrow(&instance.frame.world) + .iter() + .count(); + tracing::info!(archetype_count = archetypes.len(), entity_count, pruned); + // let report = instance.?stats.report(); + + // window.set_title(&format!( + // "Violet - {:>4.1?} {:>4.1?} {:>4.1?}", + // report.min_frame_time, report.average_frame_time, report.max_frame_time, + // )); } ctl.set_control_flow(ControlFlow::Poll); @@ -389,10 +406,10 @@ impl AppStats { } fn record_frame(&mut self, frame_time: Duration) { - self.frames.push(AppFrame { frame_time }); - if self.frames.len() > self.max_frames { + if self.frames.len() >= self.max_frames { self.frames.remove(0); } + self.frames.push(AppFrame { frame_time }); } fn report(&self) -> StatsReport { From f2856f311df1b71cd44429f67d66f1f0b57cb6b7 Mon Sep 17 00:00:00 2001 From: Freja Roberts Date: Thu, 21 Mar 2024 19:08:05 +0100 Subject: [PATCH 12/16] feat: new color palette --- Cargo.lock | 10 + Cargo.toml | 1 + colors.js | 18 +- colors.json | 184 +++++++++ colors.save.json | 128 ++++++ examples/basic.rs | 10 +- examples/color.rs | 6 +- examples/flow.rs | 33 +- examples/row.rs | 29 +- examples/sizing.rs | 61 ++- flax | 2 +- violet-core/src/layout/cache.rs | 12 +- violet-core/src/layout/mod.rs | 6 +- violet-core/src/state/constant.rs | 2 - violet-core/src/state/memo.rs | 86 ++++ violet-core/src/state/mod.rs | 19 +- violet-core/src/style/colors.rs | 408 +++++++++++++++---- violet-core/src/style/mod.rs | 37 +- violet-core/src/widget/basic.rs | 4 +- violet-core/src/widget/container.rs | 2 +- violet-core/src/widget/interactive/button.rs | 21 +- violet-core/src/widget/interactive/input.rs | 6 +- violet-core/src/widget/interactive/slider.rs | 4 +- violet-demo/Cargo.toml | 3 +- violet-demo/index.html | 19 +- violet-demo/src/editor.rs | 172 ++++++++ violet-demo/src/lib.rs | 369 +++++------------ violet-demo/src/menu.rs | 170 ++++++++ violet-wgpu/src/app.rs | 34 +- violet-wgpu/src/renderer/debug_renderer.rs | 112 +++-- 30 files changed, 1387 insertions(+), 581 deletions(-) create mode 100644 colors.json create mode 100644 colors.save.json create mode 100644 violet-core/src/state/memo.rs create mode 100644 violet-demo/src/editor.rs create mode 100644 violet-demo/src/menu.rs 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/basic.rs b/examples/basic.rs index fdfd9f9..cb09064 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -20,7 +20,7 @@ use violet::core::{ }; use violet_core::{ style::{ - colors::{DARK_CYAN_DEFAULT, JADE_DEFAULT, LION_DEFAULT}, + colors::{AMBER_500, EMERALD_500, TEAL_500}, danger_item, primary_background, secondary_background, spacing_medium, spacing_small, Background, SizeExt, ValueOrRef, }, @@ -73,12 +73,12 @@ impl Widget for MainApp { TextSegment::new(" also show off the different font loadings: \n"), TextSegment::new("Monospace:") .with_family(FontFamily::named("JetBrainsMono Nerd Font")) - .with_color(DARK_CYAN_DEFAULT), + .with_color(TEAL_500), TextSegment::new("\n\nfn main() { \n println!(") .with_family(FontFamily::named("JetBrainsMono Nerd Font")), TextSegment::new("\"Hello, world!\"") .with_family(FontFamily::named("JetBrainsMono Nerd Font")) - .with_color(LION_DEFAULT) + .with_color(AMBER_500) .with_style(Style::Italic), TextSegment::new("); \n}") .with_family(FontFamily::named("JetBrainsMono Nerd Font")), @@ -153,7 +153,7 @@ struct StackTest {} impl Widget for StackTest { fn mount(self, scope: &mut Scope<'_>) { - Stack::new((Text::new("This is an overlaid text").with_color(JADE_DEFAULT),)) + Stack::new((Text::new("This is an overlaid text").with_color(EMERALD_500),)) .with_style(ContainerStyle { background: Some(Background::new(secondary_background())), }) @@ -202,7 +202,7 @@ impl Widget for LayoutTest { TextSegment::new("This is "), TextSegment::new("sparta") .with_style(Style::Italic) - .with_color(LION_DEFAULT), + .with_color(AMBER_500), ]) .with_font_size(16.0) .with_wrap(Wrap::None), 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..a440601 100644 --- a/examples/flow.rs +++ b/examples/flow.rs @@ -15,13 +15,9 @@ use violet::core::{ Scope, Widget, }; use violet_core::{ - style::{ - self, - colors::{EERIE_BLACK_600, EERIE_BLACK_DEFAULT}, - spacing_small, Background, SizeExt, - }, + style::{self, primary_background, secondary_background, 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 +55,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(), @@ -79,9 +75,10 @@ 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(( + Rectangle::new(secondary_background()) + .with_size(Unit::rel2(1.0, 0.0) + Unit::px2(0.0, 1.0)), + card(col(( + col(( row(( Text::new("Size"), SliderWithLabel::new(value, 20.0, 200.0).editable(true), @@ -93,8 +90,7 @@ impl Widget for MainApp { )), SignalWidget::new(item_list), ))), - column( - [ + col([ // EERIE_BLACK_DEFAULT, // PLATINUM_DEFAULT, // JADE_DEFAULT, @@ -103,12 +99,11 @@ impl Widget for MainApp { // LION_DEFAULT, // REDWOOD_DEFAULT, ] - .into_iter() - .map(|color| Tints { color }) - .collect_vec(), - ), + .into_iter() + .map(|color| Tints { color }) + .collect_vec()), )) - .with_background(Background::new(EERIE_BLACK_DEFAULT)) + .with_background(Background::new(primary_background())) .contain_margins(true) .mount(scope) } @@ -130,7 +125,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..86643a4 100644 --- a/examples/row.rs +++ b/examples/row.rs @@ -1,29 +1,12 @@ -use std::iter::repeat; - use glam::{vec2, Vec2}; use itertools::Itertools; -use palette::named::DARKCYAN; use tracing_subscriber::{layer::SubscriberExt, registry, util::SubscriberInitExt, EnvFilter}; use tracing_tree::HierarchicalLayer; -use violet::core::{ - style::{ - colors::{EERIE_BLACK_DEFAULT, REDWOOD_DEFAULT}, - Background, - }, - unit::Unit, - widget::Rectangle, - Scope, Widget, -}; +use violet::core::{style::Background, unit::Unit, widget::Rectangle, Scope, Widget}; use violet_core::{ - style::{ - colors::{ - DARK_CYAN_DEFAULT, JADE_400, JADE_DEFAULT, LION_DEFAULT, REDWOOD_100, - ULTRA_VIOLET_DEFAULT, - }, - spacing_medium, spacing_small, SizeExt, - }, - widget::{card, centered, column, label, row, Image, Stack}, + style::{accent_item, primary_background, spacing_small, SizeExt}, + widget::{centered, col, row, Image, Stack}, }; use violet_wgpu::renderer::RendererConfig; @@ -49,7 +32,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(( @@ -74,7 +57,7 @@ impl Widget for MainApp { row((0..4) .map(|_| Box::new(Stack::new(Item)) as Box) .chain([Box::new( - centered((Rectangle::new(JADE_DEFAULT) + centered((Rectangle::new(accent_item()) .with_maximize(vec2(1.0, 0.0)) .with_size(Unit::px2(0.0, 50.0)) .with_max_size(Unit::px2(1000.0, 100.0)),)) @@ -86,7 +69,7 @@ impl Widget for MainApp { // .with_padding(spacing_medium()) .contain_margins(true), ) - .with_background(Background::new(EERIE_BLACK_DEFAULT)) + .with_background(Background::new(primary_background())) .mount(scope) } } diff --git a/examples/sizing.rs b/examples/sizing.rs index 2a59923..de34b85 100644 --- a/examples/sizing.rs +++ b/examples/sizing.rs @@ -8,22 +8,16 @@ use tracing_tree::HierarchicalLayer; use violet::core::{ components, - style::{ - colors::{ - EERIE_BLACK_400, EERIE_BLACK_DEFAULT, JADE_100, JADE_DEFAULT, LION_DEFAULT, - REDWOOD_DEFAULT, - }, - Background, - }, + state::MapRef, + style::{Background, SizeExt}, + text::Wrap, unit::Unit, - widget::{Rectangle, SignalWidget, Stack, Text, WidgetExt}, + widget::{card, centered, col, label, row, Rectangle, SignalWidget, Slider, Text, WidgetExt}, Edges, Scope, Widget, }; -use violet_core::{ - state::MapRef, - style::{colors::DARK_CYAN_DEFAULT, SizeExt}, - text::Wrap, - widget::{card, centered, column, label, row, Slider}, +use violet_core::style::{ + colors::{AMBER_500, EMERALD_500, EMERALD_800, REDWOOD_500, TEAL_500}, + primary_background, }; use violet_wgpu::renderer::RendererConfig; @@ -69,7 +63,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 +76,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}")))), ))), @@ -92,7 +86,7 @@ impl Widget for MainApp { // AnimatedSize, )) .contain_margins(true) - .with_background(Background::new(EERIE_BLACK_DEFAULT)) + .with_background(Background::new(primary_background())) .mount(scope) } } @@ -103,38 +97,35 @@ struct FlowSizing { impl Widget for FlowSizing { fn mount(self, scope: &mut Scope<'_>) { - let bg = Background::new(JADE_100); + let bg = Background::new(EMERALD_800); let content = ( - SizedBox::new(JADE_DEFAULT, Unit::px(self.size)).with_name("EMERALD"), - SizedBox::new(REDWOOD_DEFAULT, Unit::px2(50.0, 40.0)).with_name("REDWOOD"), - SizedBox::new( - DARK_CYAN_DEFAULT, - Unit::rel2(0.0, 0.0) + Unit::px2(10.0, 50.0), - ) - .with_name("DARK_CYAN"), + SizedBox::new(EMERALD_500, Unit::px(self.size)).with_name("EMERALD"), + SizedBox::new(REDWOOD_500, Unit::px2(50.0, 40.0)).with_name("REDWOOD"), + SizedBox::new(TEAL_500, Unit::rel2(0.0, 0.0) + Unit::px2(10.0, 50.0)) + .with_name("DARK_CYAN"), 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 +134,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) @@ -214,7 +205,7 @@ impl Widget for AnimatedSize { }), ); - Rectangle::new(LION_DEFAULT) + Rectangle::new(AMBER_500) .with_size(Default::default()) .mount(scope) } 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/layout/cache.rs b/violet-core/src/layout/cache.rs index d626ca3..9cf8f68 100644 --- a/violet-core/src/layout/cache.rs +++ b/violet-core/src/layout/cache.rs @@ -30,7 +30,7 @@ pub enum LayoutUpdate { } pub struct LayoutCache { - pub(crate) query: [Option>; 2], + pub(crate) query: [Vec>; 2], pub(crate) query_row: Option>, pub(crate) layout: Option>, on_invalidated: Option>, @@ -60,7 +60,13 @@ impl LayoutCache { pub(crate) fn insert_query(&mut self, direction: Direction, value: CachedValue) { self.hints = value.value.hints; - self.query[direction as usize] = Some(value); + let v = &mut self.query[direction as usize]; + if v.len() >= 16 { + v.pop(); + } + + v.insert(0, value); + if let Some(f) = self.on_invalidated.as_ref() { f(LayoutUpdate::SizeQueryUpdate) } @@ -84,7 +90,7 @@ impl LayoutCache { self.layout.as_ref() } - pub fn get_query(&self, direction: Direction) -> Option<&CachedValue> { + pub fn get_query(&self, direction: Direction) -> &[CachedValue] { self.query[direction as usize].as_ref() } diff --git a/violet-core/src/layout/mod.rs b/violet-core/src/layout/mod.rs index 4012f50..d3e80d2 100644 --- a/violet-core/src/layout/mod.rs +++ b/violet-core/src/layout/mod.rs @@ -275,9 +275,9 @@ pub(crate) fn query_size(world: &World, entity: &EntityRef, args: QueryArgs) -> }; // Check if cache is valid - if let Some(cache) = cache.get_query(args.direction) { - if validate_cached_query(cache, limits, args.content_area) { - return cache.value; + for cached in cache.get_query(args.direction) { + if validate_cached_query(cached, limits, args.content_area) { + return cached.value; } } 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..889db34 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,22 @@ 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_passive(), ZINC_800) + .set(interactive_hover(), EMERALD_800) + .set(interactive_pressed(), EMERALD_500) + .set(interactive_inactive(), ZINC_700) // spacing .set(spacing_small(), 4.0.into()) .set(spacing_medium(), 8.0.into()) @@ -321,6 +319,7 @@ flax::component! { /// Used for the main parts of interactive elements pub interactive_active: Srgba, + pub interactive_passive: Srgba, pub interactive_inactive: Srgba, pub interactive_hover: Srgba, pub interactive_pressed: Srgba, 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..56b37b7 100644 --- a/violet-core/src/widget/interactive/button.rs +++ b/violet-core/src/widget/interactive/button.rs @@ -8,12 +8,13 @@ use crate::{ scope::ScopeRef, state::{StateDuplex, StateStream, WatchState}, style::{ - danger_item, interactive_inactive, interactive_pressed, spacing_medium, success_item, - warning_item, Background, SizeExt, StyleExt, ValueOrRef, WidgetSize, + danger_item, interactive_inactive, interactive_passive, interactive_pressed, + spacing_medium, success_item, warning_item, Background, SizeExt, StyleExt, ValueOrRef, + WidgetSize, }, unit::Unit, widget::{ContainerStyle, Stack, Text}, - Frame, Scope, Widget, + Scope, Widget, WidgetCollection, }; type ButtonCallback = Box, winit::event::MouseButton)>; @@ -27,7 +28,7 @@ pub struct ButtonStyle { impl Default for ButtonStyle { fn default() -> Self { Self { - normal_color: interactive_inactive().into(), + normal_color: interactive_passive().into(), pressed_color: interactive_pressed().into(), } } @@ -190,14 +191,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 +207,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 +235,7 @@ impl Widget for Radio { } }); - Stack::new(()) + Stack::new(self.label) .with_style(ContainerStyle { background: Some(Background::new(normal_color)), }) diff --git a/violet-core/src/widget/interactive/input.rs b/violet-core/src/widget/interactive/input.rs index b721330..3d377a1 100644 --- a/violet-core/src/widget/interactive/input.rs +++ b/violet-core/src/widget/interactive/input.rs @@ -22,8 +22,8 @@ use crate::{ io, state::{State, StateDuplex, StateSink, StateStream}, style::{ - interactive_active, interactive_hover, interactive_inactive, spacing_small, Background, - SizeExt, StyleExt, ValueOrRef, WidgetSize, + interactive_active, interactive_hover, interactive_inactive, interactive_passive, + spacing_small, Background, SizeExt, StyleExt, ValueOrRef, WidgetSize, }, text::{CursorLocation, LayoutGlyphs, TextSegment}, time::sleep, @@ -48,7 +48,7 @@ impl Default for TextInputStyle { Self { cursor_color: interactive_active().into(), selection_color: interactive_hover().into(), - background: Background::new(interactive_inactive()), + background: Background::new(interactive_passive()), font_size: 16.0, } } diff --git a/violet-core/src/widget/interactive/slider.rs b/violet-core/src/widget/interactive/slider.rs index c15827d..5ce72c6 100644 --- a/violet-core/src/widget/interactive/slider.rs +++ b/violet-core/src/widget/interactive/slider.rs @@ -12,7 +12,7 @@ use crate::{ input::{focusable, on_cursor_move, on_mouse_input, CursorMove}, layout::Alignment, state::{State, StateDuplex, StateStream}, - style::{interactive_active, interactive_inactive, spacing_small, SizeExt, StyleExt}, + style::{interactive_active, interactive_passive, spacing_small, SizeExt, StyleExt}, to_owned, unit::Unit, utils::zip_latest, @@ -33,7 +33,7 @@ pub struct SliderStyle { impl Default for SliderStyle { fn default() -> Self { Self { - track_color: interactive_inactive(), + track_color: interactive_passive(), handle_color: interactive_active(), track_size: Unit::px2(256.0, 4.0), handle_size: Unit::px2(4.0, 16.0), 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/index.html b/violet-demo/index.html index afa5406..3b87311 100644 --- a/violet-demo/index.html +++ b/violet-demo/index.html @@ -7,21 +7,12 @@ @@ -37,6 +28,6 @@ start(); -
+ 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..04fe861 100644 --- a/violet-demo/src/lib.rs +++ b/violet-demo/src/lib.rs @@ -1,36 +1,35 @@ -use std::sync::Arc; +mod editor; +mod menu; -use anyhow::Context; -use flume::Sender; -use futures::{Future, Stream, StreamExt}; +use editor::palette_editor; +use futures::{Stream, StreamExt}; use glam::Vec2; -use heck::ToKebabCase; -use indexmap::IndexMap; use itertools::Itertools; -use rfd::AsyncFileDialog; +use menu::menu_bar; +use ordered_float::OrderedFloat; use serde::{Deserialize, Serialize}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use violet::{ core::{ declare_atom, layout::Alignment, - state::{DynStateDuplex, State, StateMut, StateStream, StateStreamRef}, + state::{State, StateMut, 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::{throttle, zip_latest_ref}, widget::{ - card, centered, column, label, row, Button, Radio, Rectangle, SliderWithLabel, Stack, - StreamWidget, Text, TextInput, WidgetExt, + card, col, label, row, Button, Radio, Rectangle, Stack, StreamWidget, Text, TextInput, + WidgetExt, }, Edges, Scope, Widget, }, futures_signals::signal::Mutable, - palette::{FromColor, IntoColor, OklabHue, Oklch, Srgb}, + palette::{IntoColor, Oklch, Srgb}, web_time::Duration, wgpu::{app::App, renderer::RendererConfig}, }; @@ -123,15 +122,16 @@ fn tints(color: impl StateStream) -> impl Widget { row(TINTS .iter() .map(move |&i| { - let color = color.stream().map(move |v| { - let f = (i as f32) / 1000.0; - let color = v.tint(f); + let color = + throttle(color.stream(), || sleep(Duration::from_millis(200))).map(move |v| { + let f = (i as f32) / 1000.0; + let color = v.tint(f); - Rectangle::new(ValueOrRef::value(color.into_color())) - .with_size(Unit::px2(120.0, 80.0)) - }); + Rectangle::new(ValueOrRef::value(color.into_color())) + .with_min_size(Unit::px2(80.0, 60.0)) + }); - Stack::new(column(StreamWidget(color))) + Stack::new(col(StreamWidget(color))) .with_margin(Edges::even(4.0)) .with_name("Tint") }) @@ -160,8 +160,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 +173,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 +218,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 +231,8 @@ impl Widget for Palettes { card(row(( checkbox, + move_down(i), + move_up(i), discard(i), palette_color_view(item.clone()), ))) @@ -210,26 +240,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())), )) @@ -278,7 +326,7 @@ where }) .collect_vec(); - column(items) + col(items) }); scope.spawn(async move { @@ -317,7 +365,7 @@ fn local_dir() -> std::path::PathBuf { } #[cfg(target_arch = "wasm32")] { - PathBuf::from(".") + std::path::PathBuf::from(".") } } @@ -332,158 +380,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 +393,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 +410,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 +438,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..10c63cd 100644 --- a/violet-wgpu/src/app.rs +++ b/violet-wgpu/src/app.rs @@ -93,9 +93,7 @@ impl AppBuilder { let event_loop = EventLoopBuilder::new().build()?; #[allow(unused_mut)] - let mut builder = WindowBuilder::new() - .with_inner_size(PhysicalSize::new(1920, 1080)) - .with_title(self.title); + let mut builder = WindowBuilder::new().with_title(self.title); #[cfg(target_arch = "wasm32")] { @@ -111,25 +109,21 @@ impl AppBuilder { .dyn_into::() .unwrap(); builder = builder.with_canvas(Some(canvas)); - // use winit::dpi::PhysicalSize; - // window.request_inner_size(PhysicalSize::new(450, 400)); - - // let node = web_sys::window() - // .unwrap() - // .document() - // .unwrap() - // .get_element_by_id("canvas-container") - // .unwrap(); - - // use winit::platform::web::WindowExtWebSys; - // node.append_child(&web_sys::Element::from( - // window.canvas().ok_or_else(|| anyhow!("No canvas"))?, - // )) - // .expect("Failed to add child"); } let window = builder.build(&event_loop)?; + #[cfg(target_arch = "wasm32")] + { + use winit::platform::web::WindowExtWebSys; + let canvas = window.canvas().expect("Missing window canvas"); + let (w, h) = (canvas.client_width(), canvas.client_height()); + + canvas.set_width(w.try_into().unwrap()); + canvas.set_height(h.try_into().unwrap()); + window.request_inner_size(winit::dpi::PhysicalSize::new(w, h)); + } + let mut input_state = InputState::new(Vec2::ZERO); let stylesheet = setup_stylesheet().spawn(frame.world_mut()); @@ -184,8 +178,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, diff --git a/violet-wgpu/src/renderer/debug_renderer.rs b/violet-wgpu/src/renderer/debug_renderer.rs index 2528fb4..9cb7dfc 100644 --- a/violet-wgpu/src/renderer/debug_renderer.rs +++ b/violet-wgpu/src/renderer/debug_renderer.rs @@ -137,41 +137,41 @@ impl DebugRenderer { let mut query = Query::new((entity_refs(), layout_cache())); let mut query = query.borrow(&frame.world); - let clamped_indicators = query.iter().filter_map(|(entity, v)| { - let can_grow_vert = if v - .get_query(Direction::Vertical) - .as_ref() - .is_some_and(|v| v.value.hints.can_grow.any()) - { - vec3(0.5, 0.0, 0.0) - } else { - Vec3::ZERO - }; + // let clamped_indicators = query.iter().filter_map(|(entity, v)| { + // let can_grow_vert = if v + // .get_query(Direction::Vertical) + // .as_ref() + // .is_some_and(|v| v.value.hints.can_grow.any()) + // { + // vec3(0.5, 0.0, 0.0) + // } else { + // Vec3::ZERO + // }; - let can_grow_hor = if v - .get_query(Direction::Horizontal) - .as_ref() - .is_some_and(|v| v.value.hints.can_grow.any()) - { - vec3(0.0, 0.5, 0.0) - } else { - Vec3::ZERO - }; + // let can_grow_hor = if v + // .get_query(Direction::Horizontal) + // .as_ref() + // .is_some_and(|v| v.value.hints.can_grow.any()) + // { + // vec3(0.0, 0.5, 0.0) + // } else { + // Vec3::ZERO + // }; - let can_grow = if v.layout().is_some_and(|v| v.value.can_grow.any()) { - vec3(0.0, 0.0, 0.5) - } else { - Vec3::ZERO - }; + // let can_grow = if v.layout().is_some_and(|v| v.value.can_grow.any()) { + // vec3(0.0, 0.0, 0.5) + // } else { + // Vec3::ZERO + // }; - let color: Vec3 = [can_grow_vert, can_grow_hor, can_grow].into_iter().sum(); + // let color: Vec3 = [can_grow_vert, can_grow_hor, can_grow].into_iter().sum(); - if color == Vec3::ZERO { - None - } else { - Some((entity, &self.corner_shader, color.extend(1.0))) - } - }); + // if color == Vec3::ZERO { + // None + // } else { + // Some((entity, &self.corner_shader, color.extend(1.0))) + // } + // }); let mut query = Query::new((entity_refs(), layout_cache())); let mut query = query.borrow(&frame.world); @@ -200,32 +200,30 @@ impl DebugRenderer { Some((entity, &self.border_shader, color)) }); - let objects = clamped_indicators - .chain(objects) - .filter_map(|(entity, shader, color)| { - let screen_rect = entity.get(screen_rect()).ok()?.align_to_grid(); - - let model_matrix = Mat4::from_scale_rotation_translation( - screen_rect.size().extend(1.0), - Quat::IDENTITY, - screen_rect.pos().extend(0.2), - ); - - let object_data = ObjectData { - model_matrix, - color, - }; - - Some(( - DrawCommand { - shader: shader.clone(), - bind_group: self.bind_group.clone(), - mesh: self.mesh.clone(), - index_count: 6, - }, - object_data, - )) - }); + let objects = objects.filter_map(|(entity, shader, color)| { + let screen_rect = entity.get(screen_rect()).ok()?.align_to_grid(); + + let model_matrix = Mat4::from_scale_rotation_translation( + screen_rect.size().extend(1.0), + Quat::IDENTITY, + screen_rect.pos().extend(0.2), + ); + + let object_data = ObjectData { + model_matrix, + color, + }; + + Some(( + DrawCommand { + shader: shader.clone(), + bind_group: self.bind_group.clone(), + mesh: self.mesh.clone(), + index_count: 6, + }, + object_data, + )) + }); self.objects.clear(); self.objects.extend(objects); From 62c7d94798cfe6f9e3ab792da910411877f04237 Mon Sep 17 00:00:00 2001 From: Tei Leelo Roberts Date: Fri, 22 Mar 2024 21:24:42 +0100 Subject: [PATCH 13/16] chore: update README --- README.md | 58 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 0d28f0c..bc7bddb 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,57 @@ -# Violet +## Violet +Retained mode GUI library focused on reactive and composable UI -Retained mode GUI library focusing on reactivity +Violet aims to be a simple library of minimal parts that can be composed to create complex UIs. -The UI is composed of `Widgets` which are inert. +State and reactivity is managed locally using async Streams, such as signals or MPSC channels and `map` methods. This +allows composing a declarative reactive UI where data flows naturally from source to destination without re-renders or +useState hooks. -However, just like a seed, they are full of potential. +## Features +- Declarative Widgets and reactive state +- Flexible layout system for responsive layouts +- Composable widgets +- Async widgets +- Thread local `!Send` + `!Sync` state and futures +- Signal based state management +- Wasm integration +- State decomposition and composition +- Renderer agnostic allowing embedding into other applications +- ECS based widget and property system (with change detection, async subscriptions, hierarchies, and more) -## Layout Example +## State Management -![Layout Example](https://github.com/ten3roberts/violet/assets/25723553/588adcf3-6045-4937-8939-bd6d040608bc) +State is primarily managed through [`futures-signals`](https://github.com/Pauan/rust-signals). + +State can be decomposed into smaller parts, composed into larger parts, or be mapped to different types using the built-in state morphism (https://docs.rs/violet/0.1.0/violet/state/). + +This allows mapping state from a struct to a string field for use in a text input widget, or mapping a larger user state to a different type to render reactively in a stream. + +These state morphisms and bidirectional which means it allows mapping to another type of state and back, supporting both +read and write operations. + +This makes sliders and text inputs targeting individual fields or even fallible operations such as parsing a string into a number trivial. + +## Reactivity + +Reactivity goes hand in hand with the state management. + +State can be converted into an async stream of changes using [StateStream](https://docs.rs/violet/0.1.0/violet/state/trait.StateStream.html) and then mapped, filtered and combined using Rust's conventional stream combinators into a widget, such as a text display or color preview. + +Most notable is that state and reactivity is managed locally, meaning that each widget can have its own state and reactivity without affecting the rest of the application. + +## Layout System + +Violet uses a custom layout system that allows for a flexible layouts that respond to different sizes. + +Each widgets has a preferred size and a minimum size. The layout system uses these sizes to determine how to distribute +the available space between widgets. + +### Layouts +- Flow - Works similar to a flexbox and distributes widgets in a row or column based on the available space and each + widgets minimum and preferred size. +- Stack - Stacks widgets on top of each other. Can be used to create overlays or centering or aligning widgets. +- Float - Floats widgets on top of each other. Can be used to create tooltips or floating popups. + +## Contributing +Contributions are always welcome! Feel free to open an issue or a PR. From 62fd9ab22b196478632a5162e0301be9ba18796b Mon Sep 17 00:00:00 2001 From: Freja Roberts Date: Fri, 22 Mar 2024 21:44:19 +0100 Subject: [PATCH 14/16] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bc7bddb..c551076 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ ## Violet -Retained mode GUI library focused on reactive and composable UI +A retained mode GUI library focused on reactive and composable UI Violet aims to be a simple library of minimal parts that can be composed to create complex UIs. @@ -7,6 +7,8 @@ State and reactivity is managed locally using async Streams, such as signals or allows composing a declarative reactive UI where data flows naturally from source to destination without re-renders or useState hooks. +![image](https://github.com/ten3roberts/violet/assets/25723553/e057405d-0acf-4f88-a86d-9106e4e912a5) + ## Features - Declarative Widgets and reactive state - Flexible layout system for responsive layouts From c35e9a4a881f6023ff36b3aea47eb2a02f42607e Mon Sep 17 00:00:00 2001 From: Tei Leelo Roberts Date: Fri, 22 Mar 2024 22:33:28 +0100 Subject: [PATCH 15/16] ci: deploy wasm example --- .github/workflows/deploy.yml | 41 ++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..4480515 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,41 @@ +name: deploy + +on: + push: + branches: [main] + +env: + CARGO_TERM_COLOR: always + +jobs: + demo: + runs-on: ubuntu-latest + if: github.event_name == 'push' + steps: + - uses: actions/checkout@v2 + - uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + - run: cargo build --release --package violet-demo --target wasm32-unknown-unknown + - run: mv ./target/wasm32-unknown-unknown/release/violet_demo.wasm ./violet-demo/public + - name: Upload Artefact + uses: actions/upload-artifact@v2 + with: + name: demo + path: demo/public + deploy: + runs-on: ubuntu-latest + needs: [asteroids, guide] + if: github.event_name == 'push' && ${{ github.ref == 'refs/heads/main' }} + steps: + - name: Download demo + uses: actions/download-artifact@v2 + with: + name: demo + path: dist/demo + + - name: Deploy + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./dist From 7091fee4f5ee0177597e4a38d6bebe7c7c385902 Mon Sep 17 00:00:00 2001 From: Tei Leelo Roberts Date: Fri, 22 Mar 2024 22:45:02 +0100 Subject: [PATCH 16/16] feat: readme example --- README.md | 47 ++- colors.json | 306 ++++++++++---------- examples/basic.rs | 289 ++++-------------- violet-core/src/state/feedback.rs | 3 +- violet-core/src/state/mod.rs | 18 +- violet-core/src/style/colors.rs | 306 ++++++++++---------- violet-core/src/text.rs | 2 +- violet-core/src/widget/interactive/input.rs | 1 + violet-demo/index.html | 33 --- violet-demo/src/lib.rs | 2 +- violet-wgpu/src/app.rs | 11 +- 11 files changed, 429 insertions(+), 589 deletions(-) delete mode 100644 violet-demo/index.html diff --git a/README.md b/README.md index c551076..7f1bd9c 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,52 @@ State and reactivity is managed locally using async Streams, such as signals or allows composing a declarative reactive UI where data flows naturally from source to destination without re-renders or useState hooks. -![image](https://github.com/ten3roberts/violet/assets/25723553/e057405d-0acf-4f88-a86d-9106e4e912a5) +## Example +```rust +let name = Mutable::new("".to_string()); +let color = Mutable::new(Srgba::new(0.0, 0.61, 0.388, 1.0)); + +// Map a `Mutable` into a `StateDuplex` for each field +let r = color.clone().map_ref(|v| &v.red, |v| &mut v.red); +let g = color.clone().map_ref(|v| &v.green, |v| &mut v.green); +let b = color.clone().map_ref(|v| &v.blue, |v| &mut v.blue); + +let speed = Mutable::new(None as Option); + +col(( + card(row((label("What is your name?"), TextInput::new(name)))), + card(col(( + label("What is your favorite color?"), + SliderWithLabel::new(r, 0.0, 1.0).round(0.01), + SliderWithLabel::new(g, 0.0, 1.0).round(0.01), + SliderWithLabel::new(b, 0.0, 1.0).round(0.01), + StreamWidget(color.stream().map(|v| { + Rectangle::new(v) + .with_maximize(Vec2::X) + .with_min_size(Unit::px2(100.0, 100.0)) + })), + ))), + card(row(( + label("What is the airspeed velocity of an unladen swallow?"), + // Fallibly parse and fill in the None at the same time using the `State` trait + // combinators + TextInput::new(speed.clone().prevent_feedback().filter_map( + |v| v.map(|v| v.to_string()), + |v| Some(v.parse::().ok()), + )), + StreamWidget(speed.stream().map(|v| { + match v { + Some(v) => pill(Text::new(format!("{v} m/s"))), + None => pill(Text::new("×".to_string())) + .with_background(Background::new(danger_background())), + } + })), + ))), +)) +.contain_margins(true) +``` + +![image](https://github.com/ten3roberts/violet/assets/25723553/cedecfb5-f76b-4a32-ac32-2abf94193acd) ## Features - Declarative Widgets and reactive state diff --git a/colors.json b/colors.json index 758308d..35697ce 100644 --- a/colors.json +++ b/colors.json @@ -1,184 +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" + "50": "#e8e8e8", + "100": "#dbdbdb", + "200": "#c1c1c1", + "300": "#a8a8a8", + "400": "#8f8f8f", + "500": "#777777", + "600": "#606060", + "700": "#4a4a4a", + "800": "#353535", + "900": "#222222", + "950": "#181818" }, "platinum": { - "50": "#e8e7e6", - "100": "#dbd9d8", - "200": "#c1bebc", - "300": "#a8a4a1", - "400": "#908a86", - "500": "#77726d", - "600": "#5f5a56", - "700": "#474441", - "800": "#312f2d", - "900": "#1c1b1a", - "950": "#131211" + "50": "#e9e7e6", + "100": "#dcdad9", + "200": "#c3c0be", + "300": "#aba7a4", + "400": "#938e8a", + "500": "#7c7671", + "600": "#645f5b", + "700": "#4d4a47", + "800": "#373533", + "900": "#232120", + "950": "#191817" }, "zinc": { - "50": "#e6e7ea", - "100": "#d8dadd", - "200": "#bdbfc4", - "300": "#a2a5ac", - "400": "#888b95", - "500": "#6f737e", - "600": "#585b64", - "700": "#42454b", - "800": "#2e2f33", - "900": "#1a1b1e", - "950": "#111214" + "50": "#e7e8eb", + "100": "#d9dbde", + "200": "#bfc1c6", + "300": "#a5a7af", + "400": "#8b8f99", + "500": "#737782", + "600": "#5d606a", + "700": "#484a51", + "800": "#343539", + "900": "#212224", + "950": "#17181a" }, "cherry": { - "50": "#f7e1e4", - "100": "#ecd2d6", - "200": "#d8b4ba", - "300": "#c8959e", - "400": "#b97582", - "500": "#a45969", - "600": "#844553", - "700": "#61363f", - "800": "#42262c", - "900": "#271519", - "950": "#1b0d10" + "50": "#f8e1e5", + "100": "#edd3d7", + "200": "#dab6bc", + "300": "#cb98a1", + "400": "#bd7986", + "500": "#a95d6d", + "600": "#8a4a58", + "700": "#673c44", + "800": "#482c32", + "900": "#2e1c1f", + "950": "#221316" }, "copper": { - "50": "#f6e3d9", - "100": "#ead5c8", - "200": "#d6b8a6", - "300": "#c59b81", - "400": "#b57d59", - "500": "#a06137", - "600": "#814c29", - "700": "#5f3b23", - "800": "#40291a", - "900": "#26170e", - "950": "#1b0f07" + "50": "#f6e4d9", + "100": "#ebd6c9", + "200": "#d8baa8", + "300": "#c89d83", + "400": "#b9805c", + "500": "#a5663b", + "600": "#87512e", + "700": "#654029", + "800": "#472f20", + "900": "#2d1e14", + "950": "#21150d" }, "redwood": { "50": "#ffded9", - "100": "#f9cec9", - "200": "#e9aea7", - "300": "#de8b82", - "400": "#d5645a", - "500": "#c1413a", - "600": "#9c312b", - "700": "#722a25", - "800": "#4d1f1b", - "900": "#2e110f", - "950": "#210a08" + "100": "#facfca", + "200": "#ebb0a9", + "300": "#e18e84", + "400": "#d9685e", + "500": "#c6463e", + "600": "#a23730", + "700": "#79302a", + "800": "#542521", + "900": "#361815", + "950": "#28100e" }, "amber": { - "50": "#f4e6c5", - "100": "#e9d8b1", - "200": "#d4bd84", - "300": "#c3a046", - "400": "#b48300", - "500": "#9f6700", - "600": "#805100", - "700": "#5e3e00", - "800": "#402c00", - "900": "#261900", - "950": "#1a1000" + "50": "#f5e7c6", + "100": "#ead9b2", + "200": "#d6bf86", + "300": "#c6a349", + "400": "#b88700", + "500": "#a46c00", + "600": "#865600", + "700": "#644400", + "800": "#463200", + "900": "#2c2000", + "950": "#211700" }, "rose": { - "50": "#fcdeea", - "100": "#f2cedd", - "200": "#e0afc4", - "300": "#d28cac", - "400": "#c66895", - "500": "#b1477d", - "600": "#8f3664", - "700": "#692d4b", - "800": "#472133", - "900": "#2a121e", - "950": "#1e0a14" + "50": "#fddeeb", + "100": "#f3cfde", + "200": "#e2b1c6", + "300": "#d58faf", + "400": "#ca6b99", + "500": "#b64c82", + "600": "#953c69", + "700": "#6f3250", + "800": "#4e2739", + "900": "#311824", + "950": "#25111a" }, "forest": { - "50": "#dbefc9", - "100": "#cbe3b5", - "200": "#abcc8a", - "300": "#87b651", - "400": "#62a100", - "500": "#428900", - "600": "#326d00", - "700": "#2a5100", - "800": "#1e3800", - "900": "#102100", - "950": "#091600" + "50": "#dcf0c9", + "100": "#cce4b6", + "200": "#adce8c", + "300": "#8ab954", + "400": "#65a400", + "500": "#478d00", + "600": "#377300", + "700": "#2f5700", + "800": "#243e00", + "900": "#172701", + "950": "#0f1d00" }, "emerald": { - "50": "#baf7e3", - "100": "#a2ebd5", - "200": "#64d7b8", - "300": "#00c59b", - "400": "#00b47e", - "500": "#009d63", - "600": "#007f4e", - "700": "#005e3c", - "800": "#00402a", - "900": "#002618", - "950": "#001a0f" + "50": "#baf7e4", + "100": "#a3ecd6", + "200": "#66d9ba", + "300": "#00c89d", + "400": "#00b881", + "500": "#00a267", + "600": "#008453", + "700": "#006441", + "800": "#004630", + "900": "#002d1e", + "950": "#002115" }, "teal": { - "50": "#b3f6f4", - "100": "#9aebe9", - "200": "#50d6d4", - "300": "#00c4c3", - "400": "#00b1b4", - "500": "#009aa0", - "600": "#007c81", - "700": "#005c5f", - "800": "#003f40", - "900": "#002526", - "950": "#001a1a" + "50": "#b3f7f5", + "100": "#9becea", + "200": "#53d8d6", + "300": "#00c7c6", + "400": "#00b5b8", + "500": "#009fa5", + "600": "#008187", + "700": "#006265", + "800": "#004546", + "900": "#002c2d", + "950": "#002121" }, "ocean": { - "50": "#cfe8ff", - "100": "#bddbff", - "200": "#96bfff", - "300": "#68a2ff", - "400": "#327fff", - "500": "#005bff", - "600": "#0046dc", - "700": "#063a9d", - "800": "#0b2a68", - "900": "#05183f", - "950": "#020f2f" + "50": "#d0e9ff", + "100": "#bedcff", + "200": "#98c1ff", + "300": "#6ba4ff", + "400": "#3683ff", + "500": "#0060ff", + "600": "#004de2", + "700": "#0d40a4", + "800": "#11316f", + "900": "#0b1f47", + "950": "#061636" }, "violet": { - "50": "#ecdfff", - "100": "#dfd0ff", - "200": "#c7b0fb", - "300": "#b28df9", - "400": "#9f63fb", - "500": "#8a38ee", - "600": "#6f29c2", - "700": "#51278b", - "800": "#371e5d", - "900": "#201138", - "950": "#160929" + "50": "#ece0ff", + "100": "#e0d1ff", + "200": "#c9b2fd", + "300": "#b490fc", + "400": "#a267ff", + "500": "#8e3ef3", + "600": "#7430c8", + "700": "#572e92", + "800": "#3d2564", + "900": "#261840", + "950": "#1c1030" }, "amethyst": { - "50": "#f4ddff", - "100": "#e9cefb", - "200": "#d4aded", - "300": "#c389e5", - "400": "#b45ee1", - "500": "#9f35d1", - "600": "#8026aa", - "700": "#5e257b", - "800": "#401d52", - "900": "#261031", - "950": "#1a0924" + "50": "#f5deff", + "100": "#eacefc", + "200": "#d6afef", + "300": "#c68be8", + "400": "#b762e5", + "500": "#a43bd6", + "600": "#852daf", + "700": "#642b81", + "800": "#462359", + "900": "#2c1639", + "950": "#210f2b" } } \ No newline at end of file diff --git a/examples/basic.rs b/examples/basic.rs index cb09064..8a4b2f7 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,8 +1,9 @@ use flax::{components::name, FetchExt, Query}; +use futures::StreamExt; use futures_signals::signal::Mutable; use glam::{vec2, Vec2}; use itertools::Itertools; -use palette::{Hsva, IntoColor}; +use palette::{Hsva, IntoColor, Srgba}; use std::time::Duration; use tracing_subscriber::{ prelude::__tracing_subscriber_SubscriberExt, registry, util::SubscriberInitExt, EnvFilter, @@ -19,243 +20,17 @@ use violet::core::{ Scope, StreamEffect, Widget, }; use violet_core::{ + state::{State, StateStream}, style::{ colors::{AMBER_500, EMERALD_500, TEAL_500}, - danger_item, primary_background, secondary_background, spacing_medium, spacing_small, - Background, SizeExt, ValueOrRef, + danger_background, danger_item, primary_background, secondary_background, spacing_medium, + spacing_small, Background, SizeExt, ValueOrRef, + }, + widget::{ + card, col, label, pill, row, ContainerStyle, SliderWithLabel, StreamWidget, TextInput, }, - widget::ContainerStyle, }; -struct MainApp; - -impl Widget for MainApp { - fn mount(self, scope: &mut Scope) { - scope - .set(name(), "MainApp".into()) - .set(size(), Unit::rel(vec2(1.0, 1.0))); - - List::new(( - LayoutFlexTest, - LayoutTest { - contain_margins: true, - } - .with_name("LayoutText 3"), - LayoutTest { - contain_margins: false, - } - .with_name("LayoutText 2"), - List::new( - (1..=4) - .map(|i| { - let size = Vec2::splat(128.0 / i as f32); - Stack::new( - Image::new("./assets/images/statue.jpg") - .with_min_size(Unit::px(size)) - .with_aspect_ratio(1.0), - ) - .with_margin(spacing_medium()) - }) - .collect_vec(), - ) - .with_name("Images"), - Stack::new((Text::rich([ - TextSegment::new("Violet"), - TextSegment::new(" now has support for "), - TextSegment::new("rich ").with_style(Style::Italic), - TextSegment::new("text. I wanted to "), - TextSegment::new("emphasize").with_style(Style::Italic), - TextSegment::new(" that, "), - TextSegment::new("(and put something in bold)") - .with_family("Inter") - .with_weight(Weight::BOLD), - TextSegment::new(", and").with_style(Style::Italic), - TextSegment::new(" also show off the different font loadings: \n"), - TextSegment::new("Monospace:") - .with_family(FontFamily::named("JetBrainsMono Nerd Font")) - .with_color(TEAL_500), - TextSegment::new("\n\nfn main() { \n println!(") - .with_family(FontFamily::named("JetBrainsMono Nerd Font")), - TextSegment::new("\"Hello, world!\"") - .with_family(FontFamily::named("JetBrainsMono Nerd Font")) - .with_color(AMBER_500) - .with_style(Style::Italic), - TextSegment::new("); \n}") - .with_family(FontFamily::named("JetBrainsMono Nerd Font")), - ]) - .with_font_size(18.0),)) - .with_margin(spacing_small()) - .with_margin(spacing_small()) - .with_background(Background::new(primary_background())), - Stack::new( - Text::rich([ - TextSegment::new("The quick brown fox 🦊 jumps over the lazy dog 🐕") - .with_style(Style::Italic), - ]) - .with_wrap(Wrap::Word) - // .with_family("Inter") - .with_font_size(18.0), - ) - .with_margin(spacing_small()) - .with_padding(spacing_small()) - .with_background(Background::new(primary_background())), - Stack::new(( - Rectangle::new(danger_item()) - .with_min_size(Unit::px(vec2(100.0, 30.0))) - .with_size(Unit::px(vec2(50.0, 30.0))), - Rectangle::new(danger_item()) - .with_min_size(Unit::px(vec2(200.0, 10.0))) - .with_size(Unit::px(vec2(50.0, 10.0))), - Text::new("This is some text").with_font_size(16.0), - )) - .with_vertical_alignment(Alignment::Center) - .with_horizontal_alignment(Alignment::Center) - .with_background(Background::new(secondary_background())) - .with_padding(spacing_small()) - .with_margin(spacing_small()), - )) - .with_background(Background::new(secondary_background())) - .contain_margins(true) - .with_direction(Direction::Vertical) - .mount(scope); - } -} - -struct DisplayWorld; - -impl Widget for DisplayWorld { - fn mount(self, scope: &mut Scope<'_>) { - scope.spawn_effect(StreamEffect::new( - interval(Duration::from_secs(1)), - |scope: &mut Scope<'_>, _| { - let world = &scope.frame().world; - let s = Query::new((components::color(), rect().opt())) - .borrow(world) - .iter() - .map(|v| format!("{v:?}")) - .join("\n"); - - scope.set( - text(), - vec![TextSegment::new(s).with_family(FontFamily::Monospace)], - ); - }, - )); - - Text::new("") - .with_font_size(12.0) - // .with_margin(MARGIN) - .mount(scope); - } -} - -struct StackTest {} - -impl Widget for StackTest { - fn mount(self, scope: &mut Scope<'_>) { - Stack::new((Text::new("This is an overlaid text").with_color(EMERALD_500),)) - .with_style(ContainerStyle { - background: Some(Background::new(secondary_background())), - }) - .with_margin(spacing_small()) - .with_padding(spacing_small()) - .mount(scope) - } -} - -struct LayoutFlexTest; - -impl Widget for LayoutFlexTest { - fn mount(self, scope: &mut Scope<'_>) { - List::new( - (0..8) - .map(|i| { - let size = vec2(50.0, 20.0); - - Stack::new( - Rectangle::new(ValueOrRef::value( - Hsva::new(i as f32 * 30.0, 1.0, 1.0, 1.0).into_color(), - )) - .with_min_size(Unit::px(size)) - .with_maximize(Vec2::X * i as f32), - ) - .with_margin(spacing_small()) - }) - .collect_vec(), - ) - .mount(scope) - } -} - -struct LayoutTest { - contain_margins: bool, -} - -impl Widget for LayoutTest { - fn mount(self, scope: &mut Scope<'_>) { - let click_count = Mutable::new(0); - - let row_1 = List::new(( - Button::new(List::new( - Stack::new( - Text::rich([ - TextSegment::new("This is "), - TextSegment::new("sparta") - .with_style(Style::Italic) - .with_color(AMBER_500), - ]) - .with_font_size(16.0) - .with_wrap(Wrap::None), - ) - .with_margin(spacing_small()) - .with_style(ContainerStyle { - background: Some(Background::new(primary_background())), - }), - )) - .on_press({ - let click_count = click_count.clone(); - move |_, _| { - *click_count.lock_mut() += 1; - } - }), - // row_2, - StackTest {}, - // Button::new(Text::new("Nope, don't you dare").with_color(CHILI_RED)).on_press({ - // let click_count = click_count.clone(); - // move |_, _| { - // *click_count.lock_mut() -= 1; - // } - // }), - // Text::new("Inline text, wrapping to fit"), - // BoxSized::new(Rectangle::new(EMERALD)) - // .with_margin(MARGIN) - // .with_size(Unit::px(vec2(10.0, 80.0))), - // Signal( - // click_count - // .signal() - // .map(|v| Text::new(format!("Clicked {} times", v))), - // ), - )) - .contain_margins(self.contain_margins) - .with_cross_align(Alignment::Center) - .with_margin(spacing_small()) - .with_padding(spacing_small()) - .with_style(ContainerStyle { - background: Some(Background::new(primary_background())), - }); - // row_1.mount(scope); - - List::new((row_1,)) - .contain_margins(self.contain_margins) - .with_margin(spacing_small()) - .with_padding(spacing_small()) - .with_style(ContainerStyle { - background: Some(Background::new(secondary_background())), - }) - .mount(scope); - } -} - pub fn main() -> anyhow::Result<()> { registry() .with( @@ -267,5 +42,51 @@ pub fn main() -> anyhow::Result<()> { .with(EnvFilter::from_default_env()) .init(); - violet_wgpu::AppBuilder::new().run(MainApp) + violet_wgpu::AppBuilder::new().run(app()) +} + +fn app() -> impl Widget { + let name = Mutable::new("".to_string()); + let quest = Mutable::new("".to_string()); + let color = Mutable::new(Srgba::new(0.0, 0.61, 0.388, 1.0)); + + // Map a `Mutable` into a `StateDuplex` for each field + let r = color.clone().map_ref(|v| &v.red, |v| &mut v.red); + let g = color.clone().map_ref(|v| &v.green, |v| &mut v.green); + let b = color.clone().map_ref(|v| &v.blue, |v| &mut v.blue); + + let speed = Mutable::new(None as Option); + + col(( + card(row((label("What is your name?"), TextInput::new(name)))), + card(row((label("What is your quest?"), TextInput::new(quest)))), + card(col(( + label("What is your favorite colour?"), + SliderWithLabel::new(r, 0.0, 1.0).round(0.01), + SliderWithLabel::new(g, 0.0, 1.0).round(0.01), + SliderWithLabel::new(b, 0.0, 1.0).round(0.01), + StreamWidget(color.stream().map(|v| { + Rectangle::new(v) + .with_maximize(Vec2::X) + .with_min_size(Unit::px2(100.0, 100.0)) + })), + ))), + card(row(( + label("What is the airspeed velocity of an unladen swallow?"), + // Fallibly parse and fill in the None at the same time using the `State` trait + // combinators + TextInput::new(speed.clone().prevent_feedback().filter_map( + |v| v.map(|v| v.to_string()), + |v| Some(v.parse::().ok()), + )), + StreamWidget(speed.stream().map(|v| { + match v { + Some(v) => pill(Text::new(format!("{v} m/s"))), + None => pill(Text::new("×".to_string())) + .with_background(Background::new(danger_background())), + } + })), + ))), + )) + .contain_margins(true) } diff --git a/violet-core/src/state/feedback.rs b/violet-core/src/state/feedback.rs index efeed9d..38c9cef 100644 --- a/violet-core/src/state/feedback.rs +++ b/violet-core/src/state/feedback.rs @@ -61,7 +61,8 @@ where self.inner .stream() .filter_map(move |v| { - let last_sent = last_sent.select_next_some().now_or_never().flatten(); + let last_sent = last_sent.next().now_or_never().flatten().flatten(); + if last_sent.as_ref() != Some(&v) { ready(Some(v)) } else { diff --git a/violet-core/src/state/mod.rs b/violet-core/src/state/mod.rs index 62f09da..c1d0816 100644 --- a/violet-core/src/state/mod.rs +++ b/violet-core/src/state/mod.rs @@ -30,38 +30,38 @@ pub trait State { /// This an be used to target a specific field of a struct or item in an array to transform. fn map_ref &U, G: Fn(&mut Self::Item) -> &mut U, U>( self, - f: F, - g: G, + conv_to: F, + conv_from: G, ) -> MapRef where Self: StateRef, Self: Sized, { - MapRef::new(self, f, g) + MapRef::new(self, conv_to, conv_from) } /// Map a state from one type to another fn map U, G: Fn(U) -> Self::Item, U>( self, - f: F, - g: G, + conv_to: F, + conv_from: G, ) -> Map where Self: Sized, { - Map::new(self, f, g) + Map::new(self, conv_to, conv_from) } /// Map a state from one type to another through fallible conversion fn filter_map Option, G: Fn(U) -> Option, U>( self, - f: F, - g: G, + conv_to: F, + conv_from: G, ) -> FilterMap where Self: Sized, { - FilterMap::new(self, f, g) + FilterMap::new(self, conv_to, conv_from) } fn dedup(self) -> Dedup diff --git a/violet-core/src/style/colors.rs b/violet-core/src/style/colors.rs index 313e198..0dfad56 100644 --- a/violet-core/src/style/colors.rs +++ b/violet-core/src/style/colors.rs @@ -1,160 +1,160 @@ use palette::Srgba; use crate::srgba; -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 STONE_50: Srgba = srgba!("#e8e8e8"); +pub const STONE_100: Srgba = srgba!("#dbdbdb"); +pub const STONE_200: Srgba = srgba!("#c1c1c1"); +pub const STONE_300: Srgba = srgba!("#a8a8a8"); +pub const STONE_400: Srgba = srgba!("#8f8f8f"); +pub const STONE_500: Srgba = srgba!("#777777"); +pub const STONE_600: Srgba = srgba!("#606060"); +pub const STONE_700: Srgba = srgba!("#4a4a4a"); +pub const STONE_800: Srgba = srgba!("#353535"); +pub const STONE_900: Srgba = srgba!("#222222"); +pub const STONE_950: Srgba = srgba!("#181818"); +pub const PLATINUM_50: Srgba = srgba!("#e9e7e6"); +pub const PLATINUM_100: Srgba = srgba!("#dcdad9"); +pub const PLATINUM_200: Srgba = srgba!("#c3c0be"); +pub const PLATINUM_300: Srgba = srgba!("#aba7a4"); +pub const PLATINUM_400: Srgba = srgba!("#938e8a"); +pub const PLATINUM_500: Srgba = srgba!("#7c7671"); +pub const PLATINUM_600: Srgba = srgba!("#645f5b"); +pub const PLATINUM_700: Srgba = srgba!("#4d4a47"); +pub const PLATINUM_800: Srgba = srgba!("#373533"); +pub const PLATINUM_900: Srgba = srgba!("#232120"); +pub const PLATINUM_950: Srgba = srgba!("#191817"); +pub const ZINC_50: Srgba = srgba!("#e7e8eb"); +pub const ZINC_100: Srgba = srgba!("#d9dbde"); +pub const ZINC_200: Srgba = srgba!("#bfc1c6"); +pub const ZINC_300: Srgba = srgba!("#a5a7af"); +pub const ZINC_400: Srgba = srgba!("#8b8f99"); +pub const ZINC_500: Srgba = srgba!("#737782"); +pub const ZINC_600: Srgba = srgba!("#5d606a"); +pub const ZINC_700: Srgba = srgba!("#484a51"); +pub const ZINC_800: Srgba = srgba!("#343539"); +pub const ZINC_900: Srgba = srgba!("#212224"); +pub const ZINC_950: Srgba = srgba!("#17181a"); +pub const CHERRY_50: Srgba = srgba!("#f8e1e5"); +pub const CHERRY_100: Srgba = srgba!("#edd3d7"); +pub const CHERRY_200: Srgba = srgba!("#dab6bc"); +pub const CHERRY_300: Srgba = srgba!("#cb98a1"); +pub const CHERRY_400: Srgba = srgba!("#bd7986"); +pub const CHERRY_500: Srgba = srgba!("#a95d6d"); +pub const CHERRY_600: Srgba = srgba!("#8a4a58"); +pub const CHERRY_700: Srgba = srgba!("#673c44"); +pub const CHERRY_800: Srgba = srgba!("#482c32"); +pub const CHERRY_900: Srgba = srgba!("#2e1c1f"); +pub const CHERRY_950: Srgba = srgba!("#221316"); +pub const COPPER_50: Srgba = srgba!("#f6e4d9"); +pub const COPPER_100: Srgba = srgba!("#ebd6c9"); +pub const COPPER_200: Srgba = srgba!("#d8baa8"); +pub const COPPER_300: Srgba = srgba!("#c89d83"); +pub const COPPER_400: Srgba = srgba!("#b9805c"); +pub const COPPER_500: Srgba = srgba!("#a5663b"); +pub const COPPER_600: Srgba = srgba!("#87512e"); +pub const COPPER_700: Srgba = srgba!("#654029"); +pub const COPPER_800: Srgba = srgba!("#472f20"); +pub const COPPER_900: Srgba = srgba!("#2d1e14"); +pub const COPPER_950: Srgba = srgba!("#21150d"); 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 REDWOOD_100: Srgba = srgba!("#facfca"); +pub const REDWOOD_200: Srgba = srgba!("#ebb0a9"); +pub const REDWOOD_300: Srgba = srgba!("#e18e84"); +pub const REDWOOD_400: Srgba = srgba!("#d9685e"); +pub const REDWOOD_500: Srgba = srgba!("#c6463e"); +pub const REDWOOD_600: Srgba = srgba!("#a23730"); +pub const REDWOOD_700: Srgba = srgba!("#79302a"); +pub const REDWOOD_800: Srgba = srgba!("#542521"); +pub const REDWOOD_900: Srgba = srgba!("#361815"); +pub const REDWOOD_950: Srgba = srgba!("#28100e"); +pub const AMBER_50: Srgba = srgba!("#f5e7c6"); +pub const AMBER_100: Srgba = srgba!("#ead9b2"); +pub const AMBER_200: Srgba = srgba!("#d6bf86"); +pub const AMBER_300: Srgba = srgba!("#c6a349"); +pub const AMBER_400: Srgba = srgba!("#b88700"); +pub const AMBER_500: Srgba = srgba!("#a46c00"); +pub const AMBER_600: Srgba = srgba!("#865600"); +pub const AMBER_700: Srgba = srgba!("#644400"); +pub const AMBER_800: Srgba = srgba!("#463200"); +pub const AMBER_900: Srgba = srgba!("#2c2000"); +pub const AMBER_950: Srgba = srgba!("#211700"); +pub const ROSE_50: Srgba = srgba!("#fddeeb"); +pub const ROSE_100: Srgba = srgba!("#f3cfde"); +pub const ROSE_200: Srgba = srgba!("#e2b1c6"); +pub const ROSE_300: Srgba = srgba!("#d58faf"); +pub const ROSE_400: Srgba = srgba!("#ca6b99"); +pub const ROSE_500: Srgba = srgba!("#b64c82"); +pub const ROSE_600: Srgba = srgba!("#953c69"); +pub const ROSE_700: Srgba = srgba!("#6f3250"); +pub const ROSE_800: Srgba = srgba!("#4e2739"); +pub const ROSE_900: Srgba = srgba!("#311824"); +pub const ROSE_950: Srgba = srgba!("#25111a"); +pub const FOREST_50: Srgba = srgba!("#dcf0c9"); +pub const FOREST_100: Srgba = srgba!("#cce4b6"); +pub const FOREST_200: Srgba = srgba!("#adce8c"); +pub const FOREST_300: Srgba = srgba!("#8ab954"); +pub const FOREST_400: Srgba = srgba!("#65a400"); +pub const FOREST_500: Srgba = srgba!("#478d00"); +pub const FOREST_600: Srgba = srgba!("#377300"); +pub const FOREST_700: Srgba = srgba!("#2f5700"); +pub const FOREST_800: Srgba = srgba!("#243e00"); +pub const FOREST_900: Srgba = srgba!("#172701"); +pub const FOREST_950: Srgba = srgba!("#0f1d00"); +pub const EMERALD_50: Srgba = srgba!("#baf7e4"); +pub const EMERALD_100: Srgba = srgba!("#a3ecd6"); +pub const EMERALD_200: Srgba = srgba!("#66d9ba"); +pub const EMERALD_300: Srgba = srgba!("#00c89d"); +pub const EMERALD_400: Srgba = srgba!("#00b881"); +pub const EMERALD_500: Srgba = srgba!("#00a267"); +pub const EMERALD_600: Srgba = srgba!("#008453"); +pub const EMERALD_700: Srgba = srgba!("#006441"); +pub const EMERALD_800: Srgba = srgba!("#004630"); +pub const EMERALD_900: Srgba = srgba!("#002d1e"); +pub const EMERALD_950: Srgba = srgba!("#002115"); +pub const TEAL_50: Srgba = srgba!("#b3f7f5"); +pub const TEAL_100: Srgba = srgba!("#9becea"); +pub const TEAL_200: Srgba = srgba!("#53d8d6"); +pub const TEAL_300: Srgba = srgba!("#00c7c6"); +pub const TEAL_400: Srgba = srgba!("#00b5b8"); +pub const TEAL_500: Srgba = srgba!("#009fa5"); +pub const TEAL_600: Srgba = srgba!("#008187"); +pub const TEAL_700: Srgba = srgba!("#006265"); +pub const TEAL_800: Srgba = srgba!("#004546"); +pub const TEAL_900: Srgba = srgba!("#002c2d"); +pub const TEAL_950: Srgba = srgba!("#002121"); +pub const OCEAN_50: Srgba = srgba!("#d0e9ff"); +pub const OCEAN_100: Srgba = srgba!("#bedcff"); +pub const OCEAN_200: Srgba = srgba!("#98c1ff"); +pub const OCEAN_300: Srgba = srgba!("#6ba4ff"); +pub const OCEAN_400: Srgba = srgba!("#3683ff"); +pub const OCEAN_500: Srgba = srgba!("#0060ff"); +pub const OCEAN_600: Srgba = srgba!("#004de2"); +pub const OCEAN_700: Srgba = srgba!("#0d40a4"); +pub const OCEAN_800: Srgba = srgba!("#11316f"); +pub const OCEAN_900: Srgba = srgba!("#0b1f47"); +pub const OCEAN_950: Srgba = srgba!("#061636"); +pub const VIOLET_50: Srgba = srgba!("#ece0ff"); +pub const VIOLET_100: Srgba = srgba!("#e0d1ff"); +pub const VIOLET_200: Srgba = srgba!("#c9b2fd"); +pub const VIOLET_300: Srgba = srgba!("#b490fc"); +pub const VIOLET_400: Srgba = srgba!("#a267ff"); +pub const VIOLET_500: Srgba = srgba!("#8e3ef3"); +pub const VIOLET_600: Srgba = srgba!("#7430c8"); +pub const VIOLET_700: Srgba = srgba!("#572e92"); +pub const VIOLET_800: Srgba = srgba!("#3d2564"); +pub const VIOLET_900: Srgba = srgba!("#261840"); +pub const VIOLET_950: Srgba = srgba!("#1c1030"); +pub const AMETHYST_50: Srgba = srgba!("#f5deff"); +pub const AMETHYST_100: Srgba = srgba!("#eacefc"); +pub const AMETHYST_200: Srgba = srgba!("#d6afef"); +pub const AMETHYST_300: Srgba = srgba!("#c68be8"); +pub const AMETHYST_400: Srgba = srgba!("#b762e5"); +pub const AMETHYST_500: Srgba = srgba!("#a43bd6"); +pub const AMETHYST_600: Srgba = srgba!("#852daf"); +pub const AMETHYST_700: Srgba = srgba!("#642b81"); +pub const AMETHYST_800: Srgba = srgba!("#462359"); +pub const AMETHYST_900: Srgba = srgba!("#2c1639"); +pub const AMETHYST_950: Srgba = srgba!("#210f2b"); pub const STONE_TINTS: [Srgba; 11] = [ STONE_50, STONE_100, diff --git a/violet-core/src/text.rs b/violet-core/src/text.rs index 3dc1993..45cecf6 100644 --- a/violet-core/src/text.rs +++ b/violet-core/src/text.rs @@ -77,7 +77,7 @@ impl TextSegment { pub fn new(text: impl Into) -> Self { Self { text: text.into(), - family: FontFamily::Serif, + family: FontFamily::SansSerif, style: Style::Normal, weight: Weight::NORMAL, color: Srgba::new(1.0, 1.0, 1.0, 1.0), diff --git a/violet-core/src/widget/interactive/input.rs b/violet-core/src/widget/interactive/input.rs index 3d377a1..48b880a 100644 --- a/violet-core/src/widget/interactive/input.rs +++ b/violet-core/src/widget/interactive/input.rs @@ -67,6 +67,7 @@ impl TextInput { content: Arc::new(content), style: Default::default(), size: WidgetSize::default() + .with_min_size(Unit::px2(16.0, 16.0)) .with_margin(spacing_small()) .with_padding(spacing_small()), } diff --git a/violet-demo/index.html b/violet-demo/index.html deleted file mode 100644 index 3b87311..0000000 --- a/violet-demo/index.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - diff --git a/violet-demo/src/lib.rs b/violet-demo/src/lib.rs index 04fe861..fe425da 100644 --- a/violet-demo/src/lib.rs +++ b/violet-demo/src/lib.rs @@ -440,5 +440,5 @@ 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_MIN: f32 = 0.17; static TINT_MAX: f32 = 0.97; diff --git a/violet-wgpu/src/app.rs b/violet-wgpu/src/app.rs index 10c63cd..a1935b2 100644 --- a/violet-wgpu/src/app.rs +++ b/violet-wgpu/src/app.rs @@ -19,12 +19,14 @@ use violet_core::{ executor::Executor, input::InputState, io::{self, Clipboard}, - style::{setup_stylesheet, stylesheet}, + style::{primary_background, setup_stylesheet, stylesheet, Background}, systems::{ hydrate_text, invalidate_cached_layout_system, layout_system, templating_system, transform_system, }, - to_owned, Frame, FutureEffect, Rect, Scope, Widget, + to_owned, + widget::col, + Frame, FutureEffect, Rect, Scope, Widget, }; use crate::{ @@ -55,7 +57,10 @@ impl Widget for Canvas { .set_default(screen_position()) .set_default(local_position()); - scope.attach(self.root); + col(self.root) + .contain_margins(true) + .with_background(Background::new(primary_background())) + .mount(scope); } }