From 9d8e7a286ac226e380101b8f99b97aa1dca5b3f8 Mon Sep 17 00:00:00 2001 From: Freja Roberts Date: Fri, 8 Mar 2024 14:08:53 +0100 Subject: [PATCH 1/7] feat: generic container and signal projection --- Cargo.lock | 1 + Cargo.toml | 1 + examples/color.rs | 27 ++ violet-core/Cargo.toml | 3 + violet-core/src/input.rs | 4 +- violet-core/src/lib.rs | 2 + violet-core/src/project.rs | 294 ++++++++++++++++++++ violet-core/src/sink.rs | 41 +++ violet-core/src/view.rs | 1 + violet-core/src/widget/interactive/input.rs | 4 + 10 files changed, 377 insertions(+), 1 deletion(-) create mode 100644 examples/color.rs create mode 100644 violet-core/src/project.rs create mode 100644 violet-core/src/sink.rs create mode 100644 violet-core/src/view.rs diff --git a/Cargo.lock b/Cargo.lock index 66a98ba..ef75236 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2498,6 +2498,7 @@ dependencies = [ "serde", "slab", "slotmap", + "tokio", "tracing", "tynm", "unicode-segmentation", diff --git a/Cargo.toml b/Cargo.toml index 6c5622c..0ce1238 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ anyhow = "1.0" once_cell = "1.18" slab = "0.4" tynm ="0.1" +tokio = { version = "1.0", default-features = false, features = ["macros", "rt"] } bytemuck = { version = "1.13", features = ["derive"] } winit = "0.29" diff --git a/examples/color.rs b/examples/color.rs new file mode 100644 index 0000000..1b40f12 --- /dev/null +++ b/examples/color.rs @@ -0,0 +1,27 @@ +use tracing_subscriber::{layer::SubscriberExt, registry, util::SubscriberInitExt, EnvFilter}; +use tracing_tree::HierarchicalLayer; +use violet_core::Widget; +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: false }) + .run(MainApp) +} + +struct MainApp; + +impl Widget for MainApp { + fn mount(scope: &mut Scope<'_>) {} +} diff --git a/violet-core/Cargo.toml b/violet-core/Cargo.toml index cb2d53c..0a16a8f 100644 --- a/violet-core/Cargo.toml +++ b/violet-core/Cargo.toml @@ -44,6 +44,9 @@ unicode-segmentation.workspace = true puffin.workspace = true web-time.workspace = true +[dev-dependencies] +tokio.workspace = true + [target.'cfg(target_arch = "wasm32")'.dependencies] gloo-timers.workspace = true wasm-bindgen-futures.workspace = true diff --git a/violet-core/src/input.rs b/violet-core/src/input.rs index e5a93b5..9ea6702 100644 --- a/violet-core/src/input.rs +++ b/violet-core/src/input.rs @@ -1,6 +1,8 @@ +use std::str::FromStr; + use flax::{ component, components::child_of, entity_ids, fetch::Satisfied, filter::All, Component, Entity, - EntityIds, EntityRef, Fetch, FetchExt, Query, Topo, + EntityIds, EntityRef, Fetch, FetchExt, Mutable, Query, Topo, }; use glam::Vec2; diff --git a/violet-core/src/lib.rs b/violet-core/src/lib.rs index 92b6427..8d50023 100644 --- a/violet-core/src/lib.rs +++ b/violet-core/src/lib.rs @@ -9,8 +9,10 @@ pub mod executor; mod frame; pub mod input; pub mod layout; +mod project; mod scope; pub mod shape; +mod sink; pub mod stored; pub mod style; pub mod systems; diff --git a/violet-core/src/project.rs b/violet-core/src/project.rs new file mode 100644 index 0000000..5beeb4d --- /dev/null +++ b/violet-core/src/project.rs @@ -0,0 +1,294 @@ +use std::{marker::PhantomData, sync::Arc}; + +use futures::{stream::BoxStream, Stream, StreamExt}; +use futures_signals::signal::{Mutable, MutableSignalRef, SignalExt, SignalStream}; + +/// A trait to project an arbitrary type into a type `U`. +/// +/// This can be used to "map" signals to other signals and composing and decomposing larger state +/// into smaller parts for reactivity +pub trait Project { + fn project V>(&self, f: F) -> V; +} + +pub trait ProjectDyn { + fn project_copy(&self) -> U + where + U: Copy; + + fn project_cloned(&self) -> U + where + U: Clone; +} + +impl ProjectDyn for T +where + T: Project, +{ + fn project_copy(&self) -> U + where + U: Copy, + { + self.project(|v| *v) + } + + fn project_cloned(&self) -> U + where + U: Clone, + { + self.project(|v| v.clone()) + } +} + +pub trait ProjectMut: Project { + fn project_mut V>(&self, f: F) -> V + where + Self: Sized; +} + +pub trait ProjectMutDyn: ProjectDyn { + fn replace(&self, value: U); +} + +impl ProjectMutDyn for T +where + T: Sized + ProjectMut, +{ + fn replace(&self, value: U) { + self.project_mut(|v| *v = value); + } +} + +/// A trait to project an arbitrary type into a stream of type `U`. +pub trait ProjectStream: Project { + type Stream V, V>: Stream + Send + Sync; + fn project_stream V, V: 'static>( + &self, + func: F, + ) -> Self::Stream; +} + +pub trait ProjectStreamDyn: ProjectDyn { + fn project_stream_copy(&self) -> BoxStream + where + U: 'static + Copy; + + fn project_stream_clone(&self) -> BoxStream + where + U: 'static + Clone; +} + +impl ProjectStreamDyn for T +where + T: Send + Sync + ProjectStream, +{ + fn project_stream_copy(&self) -> BoxStream + where + U: 'static + Copy, + { + Box::pin(self.project_stream(|v: &U| *v)) + } + + fn project_stream_clone(&self) -> BoxStream + where + U: 'static + Clone, + { + Box::pin(self.project_stream(|v| v.clone())) + } +} + +pub trait ProjectStreamDynMut: ProjectMutDyn + ProjectStreamDyn {} + +impl ProjectStreamDynMut for T where T: ProjectMutDyn + ProjectStreamDyn {} + +impl Project for Mutable { + fn project V>(&self, f: F) -> V { + f(&self.lock_ref()) + } +} + +impl ProjectMut for Mutable { + fn project_mut V>(&self, f: F) -> V { + f(&mut self.lock_mut()) + } +} + +impl ProjectStream for Mutable +where + T: Send + Sync, +{ + type Stream V, V> = SignalStream>; + + fn project_stream V, V>(&self, func: F) -> Self::Stream { + self.signal_ref(func).to_stream() + } +} + +/// A [`Mutable`](futures_signals::signal::Mutable) that is mapped to project a container of `T` to +/// a `U`. +/// +/// In a way, this acts as a duplex Sink, that allowes sending a U, and a stream for receiving a U. +/// +/// Please, for your own sanity, don't name this type yourself. It's a mouthful, compose and box it +/// like an iterator or stream. +pub struct Mapped { + inner: C, + project: Arc, + project_mut: G, + _marker: std::marker::PhantomData<(T, U)>, +} + +impl, T, U, F: Fn(&T) -> &U, G: Fn(&mut T) -> &mut U> Mapped { + pub fn new(inner: C, project: F, project_mut: G) -> Self { + Self { + inner, + project: Arc::new(project), + project_mut, + _marker: PhantomData, + } + } + + pub fn get(&self) -> U + where + U: Copy, + { + self.project(|v| *v) + } + + pub fn get_cloned(&self) -> U + where + U: Copy, + { + self.project(|v| v.clone()) + } +} + +impl, T, U, F: Fn(&T) -> &U, G: Fn(&mut T) -> &mut U> Project + for Mapped +{ + fn project V>(&self, f: H) -> V { + self.inner.project(|v| f((self.project)(v))) + } +} + +impl, T, U, F: Fn(&T) -> &U, G: Fn(&mut T) -> &mut U> ProjectMut + for Mapped +{ + fn project_mut V>(&self, f: H) -> V { + self.inner.project_mut(|v| f((self.project_mut)(v))) + } +} +impl, T, U, F: Fn(&T) -> &U, G: Fn(&mut T) -> &mut U> ProjectStream + for Mapped +where + T: 'static + Send + Sync, + U: 'static + Copy + Send + Sync, + F: 'static + Send + Sync + Fn(&T) -> &U, +{ + type Stream V, V> = + C::Stream V>, V>; + + fn project_stream V, V: 'static>( + &self, + mut func: H, + ) -> Self::Stream { + let p = self.project.clone(); + + let func = + Box::new(move |v: &T| -> V { func(p(v)) }) as Box V>; + + self.inner.project_stream(func) + } +} + +// type MappedMutableStream = +// SignalStream V>>>; +// pub struct MappedStream { +// inner: MappedMutableStream, +// } + +// impl Stream for MappedStream { +// type Item = V; + +// fn poll_next( +// mut self: std::pin::Pin<&mut Self>, +// cx: &mut std::task::Context<'_>, +// ) -> std::task::Poll> { +// self.inner.poll_next_unpin(cx) +// } +// } + +impl Project for Arc +where + T: Project, +{ + fn project V>(&self, f: F) -> V { + (**self).project(f) + } +} + +impl ProjectMut for Arc +where + T: ProjectMut, +{ + fn project_mut V>(&self, f: F) -> V { + (**self).project_mut(f) + } +} + +impl ProjectStream for Arc +where + T: ProjectStream, +{ + type Stream V, V> = T::Stream; + + fn project_stream V, V: 'static>( + &self, + func: F, + ) -> Self::Stream { + (**self).project_stream(func) + } +} + +#[cfg(test)] +mod tests { + use futures::StreamExt; + + use super::*; + + #[tokio::test] + async fn mapped_mutable() { + let state = Mutable::new((1, 2)); + + let a = Mapped::new(state.clone(), |v| &v.0, |v| &mut v.0); + + assert_eq!(a.get(), 1); + + let mut stream1 = a.project_stream(|v| *v); + let mut stream2 = a.project_stream(|v| *v); + + assert_eq!(stream1.next().await, Some(1)); + a.project_mut(|v| *v = 2); + assert_eq!(stream1.next().await, Some(2)); + assert_eq!(stream2.next().await, Some(2)); + } + + #[tokio::test] + async fn mapped_mutable_dyn() { + let state = Mutable::new((1, 2)); + + let a = Mapped::new(state.clone(), |v| &v.0, |v| &mut v.0); + + let a = Box::new(a) as Box>; + + assert_eq!(a.project_copy(), 1); + + let mut stream1 = a.project_stream_copy(); + let mut stream2 = a.project_stream_clone(); + + assert_eq!(stream1.next().await, Some(1)); + a.replace(2); + assert_eq!(stream1.next().await, Some(2)); + assert_eq!(stream2.next().await, Some(2)); + } +} diff --git a/violet-core/src/sink.rs b/violet-core/src/sink.rs new file mode 100644 index 0000000..0295f16 --- /dev/null +++ b/violet-core/src/sink.rs @@ -0,0 +1,41 @@ +// use futures::Stream; +// use futures_signals::signal::Mutable; + +// struct Closed; + +// /// A sink is a type which allows sending values. +// /// +// /// Values are sent synchronously. +// pub trait Sink { +// fn send(&self, value: T) -> Result<(), Closed>; +// } + +// pub trait DuplexSink { +// type Sink: Sink; +// type Stream: Stream; + +// fn sink(&self) -> Self::Sink; +// fn stream(&self) -> Self::Stream; +// } + +// pub struct DuplexMutable { +// inner: Mutable, + +// to_value: Box T>, +// from_value: Box U>, +// } + +// pub struct MappedSink { +// inner: S, +// func: F, +// } + +// impl Sink for MappedSink +// where +// S: Sink, +// F: Fn(U) -> U, +// { +// fn send(&self, value: U) -> Result<(), Closed> { +// self.inner.send((self.func)(value)) +// } +// } diff --git a/violet-core/src/view.rs b/violet-core/src/view.rs new file mode 100644 index 0000000..db88843 --- /dev/null +++ b/violet-core/src/view.rs @@ -0,0 +1 @@ +///! This module provides utility functions for signals for projection. diff --git a/violet-core/src/widget/interactive/input.rs b/violet-core/src/widget/interactive/input.rs index 789df9a..2091ef8 100644 --- a/violet-core/src/widget/interactive/input.rs +++ b/violet-core/src/widget/interactive/input.rs @@ -231,3 +231,7 @@ fn handle_input(input: KeyboardInput) -> Option { None } + +struct InputField { + value: Mutable, +} From 26837b0bb506b40d102bb7a2482d0ed25b149ab0 Mon Sep 17 00:00:00 2001 From: Freja Roberts Date: Fri, 8 Mar 2024 19:37:14 +0100 Subject: [PATCH 2/7] feat: generic state projection --- Cargo.lock | 73 +++++-- Cargo.toml | 3 +- examples/sizing.rs | 20 +- violet-core/Cargo.toml | 1 + violet-core/src/lib.rs | 2 +- violet-core/src/project.rs | 199 +++++++++++-------- violet-core/src/utils.rs | 77 +++++++ violet-core/src/widget/interactive/slider.rs | 49 +++-- violet-core/src/widget/mod.rs | 4 +- 9 files changed, 283 insertions(+), 145 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ef75236..c9ab882 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -208,6 +208,18 @@ version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "block" version = "0.1.6" @@ -519,17 +531,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" -[[package]] -name = "d3d12" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e3d747f100290a1ca24b752186f61f6637e1deffe3bf6320de6fcb29510a307" -dependencies = [ - "bitflags 2.4.2", - "libloading 0.8.1", - "winapi", -] - [[package]] name = "dashmap" version = "5.5.3" @@ -724,6 +725,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.30" @@ -749,6 +756,19 @@ dependencies = [ "futures-sink", ] +[[package]] +name = "futures-concurrency" +version = "7.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef6712e11cdeed5c8cf21ea0b90fec40fbe64afc9bbf2339356197eeca829fc3" +dependencies = [ + "bitvec", + "futures-core", + "pin-project", + "slab", + "smallvec", +] + [[package]] name = "futures-core" version = "0.3.30" @@ -1732,6 +1752,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.5" @@ -1747,12 +1773,6 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -[[package]] -name = "range-alloc" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8a99fddc9f0ba0a85884b8d14e3592853e787d581ca1816c91349b10e4eeab" - [[package]] name = "rangemap" version = "1.4.0" @@ -2147,6 +2167,12 @@ dependencies = [ "web-sys", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "termcolor" version = "1.4.1" @@ -2484,6 +2510,7 @@ dependencies = [ "flax", "flume", "futures", + "futures-concurrency", "futures-signals", "glam", "gloo-timers", @@ -2829,11 +2856,9 @@ dependencies = [ "android_system_properties", "arrayvec", "ash", - "bit-set", "bitflags 2.4.2", "cfg_aliases", "core-graphics-types", - "d3d12", "glow", "glutin_wgl_sys", "gpu-alloc", @@ -2851,7 +2876,6 @@ dependencies = [ "once_cell", "parking_lot", "profiling", - "range-alloc", "raw-window-handle", "renderdoc-sys", "rustc-hash", @@ -3185,6 +3209,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x11-dl" version = "2.21.0" diff --git a/Cargo.toml b/Cargo.toml index 0ce1238..15bf917 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ futures-signals = "0.3" itertools = "0.12" glam = { version = "0.24", features = ["bytemuck"] } futures = "0.3" +futures-concurrency = "7.0" flume = "0.11" parking_lot = "0.12" slotmap = "1.0" @@ -44,7 +45,7 @@ tokio = { version = "1.0", default-features = false, features = ["macros", "rt"] bytemuck = { version = "1.13", features = ["derive"] } winit = "0.29" -wgpu = { version = "0.19", default-features = false, features = ["dx12", "fragile-send-sync-non-atomic-wasm", "webgl", "wgsl"] } +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"] } diff --git a/examples/sizing.rs b/examples/sizing.rs index 4a2cb39..8c72f96 100644 --- a/examples/sizing.rs +++ b/examples/sizing.rs @@ -20,14 +20,13 @@ use violet::core::{ Edges, Scope, Widget, }; use violet_core::{ + project::MappedState, style::{colors::DARK_CYAN_DEFAULT, SizeExt}, text::Wrap, - to_owned, widget::{card, centered, column, row, Slider}, }; use violet_wgpu::renderer::RendererConfig; -const MARGIN: Edges = Edges::even(8.0); const MARGIN_SM: Edges = Edges::even(4.0); fn label(text: impl Into) -> Stack { @@ -74,21 +73,8 @@ impl Widget for Vec2Editor { fn mount(self, scope: &mut Scope<'_>) { let value = self.value; - let x = Mutable::new(value.get().x); - let y = Mutable::new(value.get().y); - - scope.spawn(x.signal().for_each({ - to_owned![value]; - move |x| { - value.lock_mut().x = x.round(); - async {} - } - })); - - scope.spawn(y.signal().for_each(move |y| { - value.lock_mut().y = y.round(); - async {} - })); + let x = MappedState::new(value.clone(), |v| &v.x, |v| &mut v.x); + let y = MappedState::new(value.clone(), |v| &v.y, |v| &mut v.y); column(( row((label(self.x_label), Slider::new(x, 0.0, 200.0))), diff --git a/violet-core/Cargo.toml b/violet-core/Cargo.toml index 0a16a8f..9354172 100644 --- a/violet-core/Cargo.toml +++ b/violet-core/Cargo.toml @@ -18,6 +18,7 @@ futures-signals.workspace = true itertools.workspace = true glam.workspace = true futures.workspace = true +futures-concurrency.workspace = true flume.workspace = true parking_lot.workspace = true slotmap.workspace = true diff --git a/violet-core/src/lib.rs b/violet-core/src/lib.rs index 8d50023..c39f283 100644 --- a/violet-core/src/lib.rs +++ b/violet-core/src/lib.rs @@ -9,7 +9,7 @@ pub mod executor; mod frame; pub mod input; pub mod layout; -mod project; +pub mod project; mod scope; pub mod shape; mod sink; diff --git a/violet-core/src/project.rs b/violet-core/src/project.rs index 5beeb4d..43162eb 100644 --- a/violet-core/src/project.rs +++ b/violet-core/src/project.rs @@ -1,4 +1,4 @@ -use std::{marker::PhantomData, sync::Arc}; +use std::{marker::PhantomData, rc::Rc, sync::Arc}; use futures::{stream::BoxStream, Stream, StreamExt}; use futures_signals::signal::{Mutable, MutableSignalRef, SignalExt, SignalStream}; @@ -7,11 +7,11 @@ use futures_signals::signal::{Mutable, MutableSignalRef, SignalExt, SignalStream /// /// This can be used to "map" signals to other signals and composing and decomposing larger state /// into smaller parts for reactivity -pub trait Project { +pub trait ProjectRef { fn project V>(&self, f: F) -> V; } -pub trait ProjectDyn { +pub trait ProjectOwned { fn project_copy(&self) -> U where U: Copy; @@ -21,9 +21,9 @@ pub trait ProjectDyn { U: Clone; } -impl ProjectDyn for T +impl ProjectOwned for T where - T: Project, + T: ProjectRef, { fn project_copy(&self) -> U where @@ -40,56 +40,57 @@ where } } -pub trait ProjectMut: Project { +/// Ability to project a mutable reference to a type `U`. +pub trait ProjectMut: ProjectRef { fn project_mut V>(&self, f: F) -> V where Self: Sized; } -pub trait ProjectMutDyn: ProjectDyn { - fn replace(&self, value: U); +/// Ability to receive a type of `U` +pub trait ProjectSink { + fn project_send(&self, value: U); } -impl ProjectMutDyn for T -where - T: Sized + ProjectMut, -{ - fn replace(&self, value: U) { - self.project_mut(|v| *v = value); - } -} +// impl ProjectSink for T +// where +// T: Sized + ProjectMut, +// { +// fn project_send(&self, value: U) { +// self.project_mut(|v| *v = value); +// } +// } -/// A trait to project an arbitrary type into a stream of type `U`. -pub trait ProjectStream: Project { - type Stream V, V>: Stream + Send + Sync; - fn project_stream V, V: 'static>( +/// A trait to produce a stream projection of a type `U`. +pub trait ProjectStream { + fn project_stream V, V: 'static>( &self, func: F, - ) -> Self::Stream; + ) -> impl 'static + Send + Stream; } -pub trait ProjectStreamDyn: ProjectDyn { - fn project_stream_copy(&self) -> BoxStream +pub trait ProjectStreamOwned { + fn project_stream_copy(&self) -> BoxStream<'static, U> where U: 'static + Copy; - fn project_stream_clone(&self) -> BoxStream + fn project_stream_clone(&self) -> BoxStream<'static, U> where U: 'static + Clone; } -impl ProjectStreamDyn for T +impl ProjectStreamOwned for T where - T: Send + Sync + ProjectStream, + T: Send + ProjectStream, { - fn project_stream_copy(&self) -> BoxStream + fn project_stream_copy(&self) -> BoxStream<'static, U> where U: 'static + Copy, { Box::pin(self.project_stream(|v: &U| *v)) } - fn project_stream_clone(&self) -> BoxStream + fn project_stream_clone(&self) -> BoxStream<'static, U> where U: 'static + Clone, { @@ -97,11 +98,17 @@ where } } -pub trait ProjectStreamDynMut: ProjectMutDyn + ProjectStreamDyn {} +/// Supertrait for types that support both sending and receiving a type `U`. +pub trait ProjectDuplex: ProjectSink + ProjectStreamOwned {} + +/// Supertrait which support mutable and reference projection and streaming of a type `U`. +/// +/// This is the most general trait, and is useful for composing and decomposing state. +pub trait ProjectState: ProjectMut + ProjectStream {} -impl ProjectStreamDynMut for T where T: ProjectMutDyn + ProjectStreamDyn {} +impl ProjectDuplex for T where T: ProjectSink + ProjectStreamOwned {} -impl Project for Mutable { +impl ProjectRef for Mutable { fn project V>(&self, f: F) -> V { f(&self.lock_ref()) } @@ -113,13 +120,20 @@ impl ProjectMut for Mutable { } } +impl ProjectSink for Mutable { + fn project_send(&self, value: T) { + self.set(value); + } +} + impl ProjectStream for Mutable where - T: Send + Sync, + T: 'static + Send + Sync, { - type Stream V, V> = SignalStream>; - - fn project_stream V, V>(&self, func: F) -> Self::Stream { + fn project_stream V, V: 'static>( + &self, + func: F, + ) -> impl 'static + Send + Stream { self.signal_ref(func).to_stream() } } @@ -131,14 +145,14 @@ where /// /// Please, for your own sanity, don't name this type yourself. It's a mouthful, compose and box it /// like an iterator or stream. -pub struct Mapped { +pub struct MappedState { inner: C, project: Arc, project_mut: G, _marker: std::marker::PhantomData<(T, U)>, } -impl, T, U, F: Fn(&T) -> &U, G: Fn(&mut T) -> &mut U> Mapped { +impl, T, U, F: Fn(&T) -> &U, G: Fn(&mut T) -> &mut U> MappedState { pub fn new(inner: C, project: F, project_mut: G) -> Self { Self { inner, @@ -157,14 +171,14 @@ impl, T, U, F: Fn(&T) -> &U, G: Fn(&mut T) -> &mut U> Mapped U where - U: Copy, + U: Clone, { self.project(|v| v.clone()) } } -impl, T, U, F: Fn(&T) -> &U, G: Fn(&mut T) -> &mut U> Project - for Mapped +impl, T, U, F: Fn(&T) -> &U, G: Fn(&mut T) -> &mut U> ProjectRef + for MappedState { fn project V>(&self, f: H) -> V { self.inner.project(|v| f((self.project)(v))) @@ -172,30 +186,35 @@ impl, T, U, F: Fn(&T) -> &U, G: Fn(&mut T) -> &mut U> Project } impl, T, U, F: Fn(&T) -> &U, G: Fn(&mut T) -> &mut U> ProjectMut - for Mapped + for MappedState { fn project_mut V>(&self, f: H) -> V { self.inner.project_mut(|v| f((self.project_mut)(v))) } } + +impl, T, U, F: Fn(&T) -> &U, G: Fn(&mut T) -> &mut U> ProjectSink + for MappedState +{ + fn project_send(&self, value: U) { + self.project_mut(|v| *v = value); + } +} + impl, T, U, F: Fn(&T) -> &U, G: Fn(&mut T) -> &mut U> ProjectStream - for Mapped + for MappedState where T: 'static + Send + Sync, - U: 'static + Copy + Send + Sync, + U: 'static + Copy + Send, F: 'static + Send + Sync + Fn(&T) -> &U, { - type Stream V, V> = - C::Stream V>, V>; - - fn project_stream V, V: 'static>( + fn project_stream V, V: 'static>( &self, mut func: H, - ) -> Self::Stream { + ) -> impl Stream + 'static { let p = self.project.clone(); - let func = - Box::new(move |v: &T| -> V { func(p(v)) }) as Box V>; + let func = Box::new(move |v: &T| -> V { func(p(v)) }) as Box V>; self.inner.project_stream(func) } @@ -218,35 +237,59 @@ where // } // } -impl Project for Arc -where - T: Project, -{ - fn project V>(&self, f: F) -> V { - (**self).project(f) - } +macro_rules! impl_container { + ($ty: ident) => { + impl ProjectRef for $ty + where + T: ProjectRef, + { + fn project V>(&self, f: F) -> V { + (**self).project(f) + } + } + + impl ProjectMut for $ty + where + T: ProjectMut, + { + fn project_mut V>(&self, f: F) -> V { + (**self).project_mut(f) + } + } + + impl ProjectStream for $ty + where + T: ProjectStream, + { + fn project_stream V, V: 'static>( + &self, + func: F, + ) -> impl Stream + 'static { + (**self).project_stream(func) + } + } + }; } -impl ProjectMut for Arc +impl_container!(Box); +impl_container!(Arc); +impl_container!(Rc); + +impl ProjectStream for flume::Receiver where - T: ProjectMut, + T: 'static + Send + Sync, { - fn project_mut V>(&self, f: F) -> V { - (**self).project_mut(f) + fn project_stream V, V: 'static>( + &self, + mut func: F, + ) -> impl 'static + Send + Stream { + self.clone().into_stream().map(move |v| func(&v)) } } -impl ProjectStream for Arc -where - T: ProjectStream, -{ - type Stream V, V> = T::Stream; - - fn project_stream V, V: 'static>( - &self, - func: F, - ) -> Self::Stream { - (**self).project_stream(func) +impl ProjectSink for flume::Sender { + fn project_send(&self, value: T) { + self.send(value).unwrap(); } } @@ -260,7 +303,7 @@ mod tests { async fn mapped_mutable() { let state = Mutable::new((1, 2)); - let a = Mapped::new(state.clone(), |v| &v.0, |v| &mut v.0); + let a = MappedState::new(state.clone(), |v| &v.0, |v| &mut v.0); assert_eq!(a.get(), 1); @@ -274,20 +317,18 @@ mod tests { } #[tokio::test] - async fn mapped_mutable_dyn() { + async fn project_duplex() { let state = Mutable::new((1, 2)); - let a = Mapped::new(state.clone(), |v| &v.0, |v| &mut v.0); - - let a = Box::new(a) as Box>; + let a = MappedState::new(state.clone(), |v| &v.0, |v| &mut v.0); - assert_eq!(a.project_copy(), 1); + let a = Box::new(a) as Box>; let mut stream1 = a.project_stream_copy(); let mut stream2 = a.project_stream_clone(); assert_eq!(stream1.next().await, Some(1)); - a.replace(2); + a.project_send(2); assert_eq!(stream1.next().await, Some(2)); assert_eq!(stream2.next().await, Some(2)); } diff --git a/violet-core/src/utils.rs b/violet-core/src/utils.rs index e6b8610..eb9f5ec 100644 --- a/violet-core/src/utils.rs +++ b/violet-core/src/utils.rs @@ -1,6 +1,83 @@ +use std::task::Poll; + +use futures::Stream; + #[macro_export] macro_rules! to_owned { ($($ident: ident),*) => ( $(let $ident = $ident.to_owned();)* ) } + +/// Combines two streams yielding the latest value from each stream +pub fn zip_latest(a: A, b: B, func: F) -> ZipLatest { + ZipLatest::new(a, b, func) +} + +/// Combines two streams yielding the latest value from each stream +pub fn zip_latest_clone( + a: A, + b: B, +) -> ZipLatest (A::Item, B::Item)> +where + A: Stream, + B: Stream, + A::Item: Clone, + B::Item: Clone, +{ + ZipLatest::new(a, b, |a: &A::Item, b: &B::Item| (a.clone(), b.clone())) +} +#[pin_project::pin_project] +pub struct ZipLatest { + #[pin] + a: A, + #[pin] + b: B, + b_item: Option, + a_item: Option, + func: F, +} + +impl ZipLatest { + pub fn new(a: A, b: B, func: F) -> Self { + Self { + a, + b, + a_item: None, + b_item: None, + func, + } + } +} + +impl Stream for ZipLatest +where + A: Stream, + B: Stream, + F: FnMut(&A::Item, &B::Item) -> V, +{ + type Item = V; + + fn poll_next( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + let p = self.project(); + let mut ready = false; + + if let Poll::Ready(value) = p.a.poll_next(cx) { + *p.a_item = value; + ready = true; + } + + if let Poll::Ready(value) = p.b.poll_next(cx) { + *p.b_item = value; + ready = true; + } + + match (&p.a_item, &p.b_item) { + (Some(a), Some(b)) if ready => Poll::Ready(Some((p.func)(a, b))), + _ => Poll::Pending, + } + } +} diff --git a/violet-core/src/widget/interactive/slider.rs b/violet-core/src/widget/interactive/slider.rs index c143401..758eae8 100644 --- a/violet-core/src/widget/interactive/slider.rs +++ b/violet-core/src/widget/interactive/slider.rs @@ -1,9 +1,9 @@ +use std::sync::Arc; + use cosmic_text::Wrap; use flax::{Component, Entity, EntityRef}; -use futures_signals::{ - map_ref, - signal::{Mutable, MutableSignal, SignalExt}, -}; +use futures::{stream::BoxStream, StreamExt}; +use futures_signals::signal::Mutable; use glam::{IVec2, Vec2}; use palette::Srgba; use winit::event::ElementState; @@ -12,10 +12,12 @@ use crate::{ components::{offset, rect}, input::{focusable, on_cursor_move, on_mouse_input, CursorMove}, layout::Alignment, + project::{ProjectDuplex, ProjectStreamOwned}, style::{get_stylesheet, interactive_active, interactive_inactive, spacing, StyleExt}, text::TextSegment, unit::Unit, - widget::{row, BoxSized, ContainerStyle, Positioned, Rectangle, SignalWidget, Stack, Text}, + utils::zip_latest_clone, + widget::{row, BoxSized, ContainerStyle, Positioned, Rectangle, Stack, StreamWidget, Text}, Edges, Scope, StreamEffect, Widget, }; @@ -40,19 +42,19 @@ impl Default for SliderStyle { pub struct Slider { style: SliderStyle, - value: Mutable, + value: Arc>, min: V, max: V, label: bool, } impl Slider { - pub fn new(value: Mutable, min: V, max: V) -> Self + pub fn new(value: impl 'static + Send + Sync + ProjectDuplex, min: V, max: V) -> Self where V: Copy, { Self { - value, + value: Arc::new(value), min, max, style: Default::default(), @@ -99,15 +101,15 @@ impl Widget for Slider { input: CursorMove, min: f32, max: f32, - dst: &Mutable, + dst: &dyn ProjectDuplex, ) { let rect = entity.get_copy(rect()).unwrap(); let value = (input.local_pos.x / rect.size().x).clamp(0.0, 1.0) * (max - min) + min; - dst.set(V::from_progress(value)); + dst.project_send(V::from_progress(value)); } let handle = SliderHandle { - value: self.value.signal(), + value: self.value.project_stream_copy(), min, max, rect_id: track, @@ -121,13 +123,13 @@ impl Widget for Slider { let value = self.value.clone(); move |_, entity, input| { if input.state == ElementState::Pressed { - update(entity, input.cursor, min, max, &value); + update(entity, input.cursor, min, max, &*value); } } }) .on_event(on_cursor_move(), { let value = self.value.clone(); - move |_, entity, input| update(entity, input, min, max, &value) + move |_, entity, input| update(entity, input, min, max, &*value) }); let slider = Stack::new(handle) @@ -140,7 +142,7 @@ impl Widget for Slider { if self.label { row(( slider, - SignalWidget(self.value.signal().map(|v| { + StreamWidget(self.value.project_stream_copy().map(|v| { Text::rich([TextSegment::new(format!("{:>4.2}", v))]).with_wrap(Wrap::None) })), )) @@ -152,7 +154,7 @@ impl Widget for Slider { } struct SliderHandle { - value: MutableSignal, + value: BoxStream<'static, V>, handle_color: Srgba, handle_size: Unit, min: f32, @@ -164,20 +166,17 @@ impl Widget for SliderHandle { fn mount(self, scope: &mut Scope<'_>) { let rect_size = Mutable::new(None); - let update_signal = map_ref! { - let value = self.value, - let size = rect_size.signal() => - (value.to_progress(), *size) - }; + let update = zip_latest_clone(self.value, rect_size.project_stream_copy()); scope.frame_mut().monitor(self.rect_id, rect(), move |v| { rect_size.set(v.map(|v| v.size())); }); - scope.spawn_effect(StreamEffect::new(update_signal.to_stream(), { - move |scope: &mut Scope<'_>, (value, size): (f32, Option)| { + scope.spawn_effect(StreamEffect::new(update, { + move |scope: &mut Scope<'_>, (value, size): (V, Option)| { + tracing::info!(value = value.to_progress(), ?size, "update"); if let Some(size) = size { - let pos = (value - self.min) * size.x / (self.max - self.min); + let pos = (value.to_progress() - self.min) * size.x / (self.max - self.min); scope.entity().update_dedup(offset(), Unit::px2(pos, 0.0)); } @@ -238,7 +237,7 @@ pub struct SliderWithLabel { } impl SliderWithLabel { - pub fn new(value: Mutable, min: V, max: V) -> Self + pub fn new(value: impl 'static + Send + Sync + ProjectDuplex, min: V, max: V) -> Self where V: Copy, { @@ -257,7 +256,7 @@ impl SliderWithLabel { impl Widget for SliderWithLabel { fn mount(self, scope: &mut Scope<'_>) { let label = - SignalWidget(self.slider.value.signal().map(|v| { + StreamWidget(self.slider.value.project_stream_copy().map(|v| { Text::rich([TextSegment::new(format!("{:>4.2}", v))]).with_wrap(Wrap::None) })); diff --git a/violet-core/src/widget/mod.rs b/violet-core/src/widget/mod.rs index d52e129..89b8cb2 100644 --- a/violet-core/src/widget/mod.rs +++ b/violet-core/src/widget/mod.rs @@ -11,9 +11,9 @@ pub use future::{SignalWidget, StreamWidget}; use futures_signals::signal::Mutable; pub use interactive::{button::*, input::*, slider::*}; -/// Represents a widget in the UI tree which can mount itself into the frame. +/// A widget is a description of a part of the Ui with the capability to mount itself into the world. /// -/// Is inert before mounting +/// This trait rarely required Send nor Sync, or a static lifetime as it does remain in the world after it is mounted. pub trait Widget: BoxedWidget { /// Mount the widget into the world, returning a handle to refer to it fn mount(self, scope: &mut Scope<'_>); From bf363c0eec0ec1cf3634d575701305967206858f Mon Sep 17 00:00:00 2001 From: Freja Roberts Date: Sat, 9 Mar 2024 01:51:48 +0100 Subject: [PATCH 3/7] feat: basic input fields --- examples/color.rs | 37 ++- examples/sizing.rs | 6 +- violet-core/src/input.rs | 2 +- violet-core/src/layout/mod.rs | 4 +- violet-core/src/project.rs | 248 +++++++++++++------ violet-core/src/widget/interactive/input.rs | 92 +++++-- violet-core/src/widget/interactive/slider.rs | 68 ++++- violet-wgpu/src/renderer/debug_renderer.rs | 118 +++++---- 8 files changed, 409 insertions(+), 166 deletions(-) diff --git a/examples/color.rs b/examples/color.rs index 1b40f12..27deefa 100644 --- a/examples/color.rs +++ b/examples/color.rs @@ -1,6 +1,15 @@ +use futures_signals::signal::{Mutable, SignalExt}; +use glam::Vec3; +use palette::{IntoColor, Oklch}; use tracing_subscriber::{layer::SubscriberExt, registry, util::SubscriberInitExt, EnvFilter}; use tracing_tree::HierarchicalLayer; -use violet_core::Widget; +use violet_core::{ + project::{MappedDuplex, MappedState}, + style::SizeExt, + unit::Unit, + widget::{card, column, row, InputField, Rectangle, SignalWidget, SliderWithInput, Text}, + Edges, Scope, Widget, +}; use violet_wgpu::renderer::RendererConfig; pub fn main() -> anyhow::Result<()> { @@ -23,5 +32,29 @@ pub fn main() -> anyhow::Result<()> { struct MainApp; impl Widget for MainApp { - fn mount(scope: &mut Scope<'_>) {} + fn mount(self, scope: &mut Scope<'_>) { + let color = Mutable::new(Vec3::new(0.0, 0.0, 0.0)); + + let lightness = MappedState::new(color.clone(), |v| &v.x, |v| &mut v.x); + let chroma = MappedState::new(color.clone(), |v| &v.y, |v| &mut v.y); + let hue = MappedState::new(color.clone(), |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(color).with_min_size(Unit::px2(200.0, 100.0)) + }); + + column(( + row(( + Text::new("Lightness"), + SliderWithInput::new(lightness, 0.0, 1.0), + )), + row((Text::new("Chroma"), SliderWithInput::new(chroma, 0.0, 0.37))), + row((Text::new("Hue"), SliderWithInput::new(hue, 0.0, 360.0))), + SignalWidget(color.signal().map(|v| Text::new(format!("{}", v)))), + card(SignalWidget(color_rect)), + )) + .with_margin(Edges::even(4.0)) + .mount(scope); + } } diff --git a/examples/sizing.rs b/examples/sizing.rs index 8c72f96..8e6a94b 100644 --- a/examples/sizing.rs +++ b/examples/sizing.rs @@ -120,7 +120,7 @@ impl Widget for FlowSizing { Unit::rel2(0.0, 0.0) + Unit::px2(10.0, 50.0), ) .with_name("DARK_CYAN"), - // AnimatedSize, + AnimatedSize, ); column(( @@ -221,6 +221,8 @@ impl Widget for AnimatedSize { }), ); - Rectangle::new(LION_DEFAULT).mount(scope) + Rectangle::new(LION_DEFAULT) + .with_size(Default::default()) + .mount(scope) } } diff --git a/violet-core/src/input.rs b/violet-core/src/input.rs index 9ea6702..64e69e2 100644 --- a/violet-core/src/input.rs +++ b/violet-core/src/input.rs @@ -147,7 +147,7 @@ impl InputState { pub fn on_keyboard_input(&mut self, frame: &mut Frame, event: KeyEvent) { if let Some(cur) = &self.focused { - tracing::info!(?cur, "sending keyboard input event"); + tracing::debug!(?cur, "sending keyboard input event"); let entity = frame.world.entity(cur.id).unwrap(); if let Ok(mut on_input) = entity.get_mut(on_keyboard_input()) { diff --git a/violet-core/src/layout/mod.rs b/violet-core/src/layout/mod.rs index 60f4fcf..7286750 100644 --- a/violet-core/src/layout/mod.rs +++ b/violet-core/src/layout/mod.rs @@ -310,7 +310,7 @@ pub(crate) fn query_size( let _span = tracing::trace_span!("cached").entered(); // validate_sizing(entity, &cache.value, limits); tracing::debug!(%entity, "found valid cached query"); - // return cache.value; + return cache.value; // } } } @@ -453,7 +453,7 @@ pub(crate) fn update_subtree( 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); - // return value.value; + return value.value; } } diff --git a/violet-core/src/project.rs b/violet-core/src/project.rs index 43162eb..eeb3446 100644 --- a/violet-core/src/project.rs +++ b/violet-core/src/project.rs @@ -1,7 +1,7 @@ use std::{marker::PhantomData, rc::Rc, sync::Arc}; use futures::{stream::BoxStream, Stream, StreamExt}; -use futures_signals::signal::{Mutable, MutableSignalRef, SignalExt, SignalStream}; +use futures_signals::signal::{Mutable, SignalExt}; /// A trait to project an arbitrary type into a type `U`. /// @@ -11,37 +11,25 @@ pub trait ProjectRef { fn project V>(&self, f: F) -> V; } -pub trait ProjectOwned { - fn project_copy(&self) -> U - where - U: Copy; - - fn project_cloned(&self) -> U - where - U: Clone; +pub trait ProjectOwned: ProjectRef { + fn project_owned(&self) -> U; } -impl ProjectOwned for T -where - T: ProjectRef, -{ - fn project_copy(&self) -> U - where - U: Copy, - { - self.project(|v| *v) - } - - fn project_cloned(&self) -> U - where - U: Clone, - { - self.project(|v| v.clone()) - } -} +// impl ProjectOwned for T +// where +// T: ProjectRef, +// U: Clone, +// { +// fn project_owned(&self) -> U +// where +// U: Clone, +// { +// self.project(|v| v.clone()) +// } +// } /// Ability to project a mutable reference to a type `U`. -pub trait ProjectMut: ProjectRef { +pub trait ProjectMut { fn project_mut V>(&self, f: F) -> V where Self: Sized; @@ -62,40 +50,17 @@ pub trait ProjectSink { // } /// A trait to produce a stream projection of a type `U`. -pub trait ProjectStream { +pub trait ProjectStreamRef { fn project_stream V, V: 'static>( &self, func: F, - ) -> impl 'static + Send + Stream; -} - -pub trait ProjectStreamOwned { - fn project_stream_copy(&self) -> BoxStream<'static, U> + ) -> impl 'static + Send + Stream where - U: 'static + Copy; - - fn project_stream_clone(&self) -> BoxStream<'static, U> - where - U: 'static + Clone; + Self: Sized; } -impl ProjectStreamOwned for T -where - T: Send + ProjectStream, -{ - fn project_stream_copy(&self) -> BoxStream<'static, U> - where - U: 'static + Copy, - { - Box::pin(self.project_stream(|v: &U| *v)) - } - - fn project_stream_clone(&self) -> BoxStream<'static, U> - where - U: 'static + Clone, - { - Box::pin(self.project_stream(|v| v.clone())) - } +pub trait ProjectStreamOwned: ProjectStreamRef { + fn project_stream_owned(&self) -> BoxStream<'static, U>; } /// Supertrait for types that support both sending and receiving a type `U`. @@ -104,7 +69,7 @@ pub trait ProjectDuplex: ProjectSink + ProjectStreamOwned {} /// Supertrait which support mutable and reference projection and streaming of a type `U`. /// /// This is the most general trait, and is useful for composing and decomposing state. -pub trait ProjectState: ProjectMut + ProjectStream {} +pub trait ProjectState: ProjectMut + ProjectStreamRef {} impl ProjectDuplex for T where T: ProjectSink + ProjectStreamOwned {} @@ -114,6 +79,12 @@ impl ProjectRef for Mutable { } } +impl ProjectOwned for Mutable { + fn project_owned(&self) -> T { + self.get_cloned() + } +} + impl ProjectMut for Mutable { fn project_mut V>(&self, f: F) -> V { f(&mut self.lock_mut()) @@ -126,7 +97,7 @@ impl ProjectSink for Mutable { } } -impl ProjectStream for Mutable +impl ProjectStreamRef for Mutable where T: 'static + Send + Sync, { @@ -138,13 +109,19 @@ where } } +impl ProjectStreamOwned for Mutable +where + T: 'static + Send + Sync + Clone, +{ + fn project_stream_owned(&self) -> BoxStream<'static, T> + where + T: 'static + Clone, + { + self.signal_cloned().to_stream().boxed() + } +} /// A [`Mutable`](futures_signals::signal::Mutable) that is mapped to project a container of `T` to /// a `U`. -/// -/// In a way, this acts as a duplex Sink, that allowes sending a U, and a stream for receiving a U. -/// -/// Please, for your own sanity, don't name this type yourself. It's a mouthful, compose and box it -/// like an iterator or stream. pub struct MappedState { inner: C, project: Arc, @@ -161,10 +138,13 @@ impl, T, U, F: Fn(&T) -> &U, G: Fn(&mut T) -> &mut U> MappedSta _marker: PhantomData, } } +} +impl, T, U, F, G> MappedState { pub fn get(&self) -> U where U: Copy, + F: Fn(&T) -> &U, { self.project(|v| *v) } @@ -172,20 +152,27 @@ impl, T, U, F: Fn(&T) -> &U, G: Fn(&mut T) -> &mut U> MappedSta pub fn get_cloned(&self) -> U where U: Clone, + F: Fn(&T) -> &U, { self.project(|v| v.clone()) } } -impl, T, U, F: Fn(&T) -> &U, G: Fn(&mut T) -> &mut U> ProjectRef - for MappedState -{ +impl, T, U, F: Fn(&T) -> &U, G> ProjectRef for MappedState { fn project V>(&self, f: H) -> V { self.inner.project(|v| f((self.project)(v))) } } -impl, T, U, F: Fn(&T) -> &U, G: Fn(&mut T) -> &mut U> ProjectMut +impl, T, U: Clone, F: Fn(&T) -> &U, G> ProjectOwned + for MappedState +{ + fn project_owned(&self) -> U { + self.project(|v| v.clone()) + } +} + +impl, T, U, F, G: Fn(&mut T) -> &mut U> ProjectMut for MappedState { fn project_mut V>(&self, f: H) -> V { @@ -193,7 +180,7 @@ impl, T, U, F: Fn(&T) -> &U, G: Fn(&mut T) -> &mut U> ProjectMu } } -impl, T, U, F: Fn(&T) -> &U, G: Fn(&mut T) -> &mut U> ProjectSink +impl, T, U, F, G: Fn(&mut T) -> &mut U> ProjectSink for MappedState { fn project_send(&self, value: U) { @@ -201,11 +188,11 @@ impl, T, U, F: Fn(&T) -> &U, G: Fn(&mut T) -> &mut U> ProjectSi } } -impl, T, U, F: Fn(&T) -> &U, G: Fn(&mut T) -> &mut U> ProjectStream +impl, T, U, F: Fn(&T) -> &U, G> ProjectStreamRef for MappedState where T: 'static + Send + Sync, - U: 'static + Copy + Send, + U: 'static + Send, F: 'static + Send + Sync + Fn(&T) -> &U, { fn project_stream V, V: 'static>( @@ -220,6 +207,96 @@ where } } +impl, T, U, F: Fn(&T) -> &U, G> ProjectStreamOwned + for MappedState +where + T: 'static + Send + Sync, + U: 'static + Clone + Send, + F: 'static + Send + Sync + Fn(&T) -> &U, +{ + fn project_stream_owned(&self) -> BoxStream<'static, U> { + let f = self.project.clone(); + self.inner.project_stream(move |v| f(v).clone()).boxed() + } +} + +/// A [`Mutable`](futures_signals::signal::Mutable) that is mapped to project a container of `T` to +/// owned instances of `U`. +/// +/// In a way, this acts as a duplex Sink, that allowes sending a U, and a stream for receiving a U. +pub struct MappedDuplex { + inner: C, + into: Arc, + from: G, + _marker: std::marker::PhantomData<(T, U)>, +} + +impl U, G: Fn(U) -> T> MappedDuplex { + pub fn new(inner: C, into: F, from: G) -> Self { + Self { + inner, + into: Arc::new(into), + from, + _marker: PhantomData, + } + } +} + +impl, T, U: Clone, F: Fn(&T) -> U, G> ProjectRef + for MappedDuplex +{ + fn project V>(&self, f: H) -> V { + f(&self.project_owned()) + } +} +impl, T, U: Clone, F: Fn(&T) -> U, G> ProjectOwned + for MappedDuplex +{ + fn project_owned(&self) -> U { + self.inner.project(|v| (self.into)(v)) + } +} + +impl, T, U, F: Fn(&T) -> U, G: Fn(U) -> T> ProjectSink + for MappedDuplex +{ + fn project_send(&self, value: U) { + self.inner.project_send((self.from)(value)); + } +} + +impl, T, U, F: Fn(&T) -> U, G: Fn(U) -> T> ProjectStreamRef + for MappedDuplex +where + T: 'static + Send + Sync, + U: 'static + Send, + F: 'static + Send + Sync + Fn(&T) -> U, +{ + fn project_stream V, V: 'static>( + &self, + mut func: H, + ) -> impl 'static + Send + Stream { + let f = self.into.clone(); + self.inner.project_stream(move |v| func(&f(v))).boxed() + } +} + +impl, T, U, F: Fn(&T) -> U, G: Fn(U) -> T> ProjectStreamOwned + for MappedDuplex +where + T: 'static + Send + Sync, + U: 'static + Send, + F: 'static + Send + Sync + Fn(&T) -> U, +{ + fn project_stream_owned(&self) -> BoxStream<'static, U> + where + U: 'static, + { + let f = self.into.clone(); + self.inner.project_stream(move |v| f(v)).boxed() + } +} + // type MappedMutableStream = // SignalStream V>>>; // pub struct MappedStream { @@ -241,7 +318,7 @@ macro_rules! impl_container { ($ty: ident) => { impl ProjectRef for $ty where - T: ProjectRef, + T: ?Sized + ProjectRef, { fn project V>(&self, f: F) -> V { (**self).project(f) @@ -257,9 +334,9 @@ macro_rules! impl_container { } } - impl ProjectStream for $ty + impl ProjectStreamRef for $ty where - T: ProjectStream, + T: ProjectStreamRef, { fn project_stream V, V: 'static>( &self, @@ -268,6 +345,24 @@ macro_rules! impl_container { (**self).project_stream(func) } } + + impl ProjectStreamOwned for $ty + where + T: ProjectStreamOwned, + { + fn project_stream_owned(&self) -> BoxStream<'static, U> { + (**self).project_stream_owned() + } + } + + impl ProjectSink for $ty + where + T: ?Sized + ProjectSink, + { + fn project_send(&self, value: U) { + (**self).project_send(value); + } + } }; } @@ -275,7 +370,7 @@ impl_container!(Box); impl_container!(Arc); impl_container!(Rc); -impl ProjectStream for flume::Receiver +impl ProjectStreamRef for flume::Receiver where T: 'static + Send + Sync, { @@ -295,7 +390,6 @@ impl ProjectSink for flume::Sender { #[cfg(test)] mod tests { - use futures::StreamExt; use super::*; @@ -324,8 +418,8 @@ mod tests { let a = Box::new(a) as Box>; - let mut stream1 = a.project_stream_copy(); - let mut stream2 = a.project_stream_clone(); + let mut stream1 = a.project_stream_owned(); + let mut stream2 = a.project_stream_owned(); assert_eq!(stream1.next().await, Some(1)); a.project_send(2); diff --git a/violet-core/src/widget/interactive/input.rs b/violet-core/src/widget/interactive/input.rs index 2091ef8..790d63f 100644 --- a/violet-core/src/widget/interactive/input.rs +++ b/violet-core/src/widget/interactive/input.rs @@ -1,9 +1,13 @@ +use core::panic; +use std::{fmt::Display, future::ready, str::FromStr, sync::Arc}; + use flax::Component; use futures::{FutureExt, StreamExt}; use futures_signals::signal::{self, Mutable, SignalExt}; use glam::{vec2, Vec2}; use itertools::Itertools; use palette::Srgba; +use tracing::info; use winit::{ event::ElementState, keyboard::{Key, NamedKey}, @@ -13,6 +17,7 @@ use crate::{ components::{self, screen_rect}, editor::{CursorMove, EditAction, EditorAction, TextEditor}, input::{focus_sticky, focusable, on_keyboard_input, on_mouse_input, KeyboardInput}, + project::{ProjectDuplex, ProjectOwned, ProjectSink, ProjectStreamOwned}, style::{ colors::EERIE_BLACK_300, get_stylesheet, interactive_active, spacing, Background, SizeExt, StyleExt, WidgetSize, @@ -20,7 +25,9 @@ use crate::{ text::{LayoutGlyphs, TextSegment}, to_owned, unit::Unit, - widget::{NoOp, Positioned, Rectangle, SignalWidget, Stack, Text, WidgetExt}, + widget::{ + row, NoOp, Positioned, Rectangle, SignalWidget, Stack, StreamWidget, Text, WidgetExt, + }, Rect, Scope, Widget, }; @@ -42,14 +49,14 @@ impl Default for TextInputStyle { pub struct TextInput { style: TextInputStyle, - content: Mutable, + content: Arc>, size: WidgetSize, } impl TextInput { - pub fn new(content: Mutable) -> Self { + pub fn new(content: impl 'static + Send + Sync + ProjectDuplex) -> Self { Self { - content, + content: Arc::new(content), style: Default::default(), size: Default::default(), } @@ -88,10 +95,11 @@ 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_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.clone(); scope.spawn({ let mut layout_glyphs = layout_glyphs.signal_cloned().to_stream(); @@ -102,25 +110,26 @@ impl Widget for TextInput { let mut cursor_pos = Vec2::ZERO; + let mut new_text = content.project_stream_owned(); + loop { futures::select! { + new_text = new_text.next().fuse() => { + if let Some(new_text) = new_text { + editor.set_text(new_text.split('\n')); + } + } action = rx.next().fuse() => { if let Some(action) = action { editor.apply_action(action); - let mut c = content.lock_mut(); - c.clear(); - for line in editor.lines() { - c.push_str(line.text()); - c.push('\n'); - } + content.project_send(editor.lines().iter().map(|v| v.text()).join("\n")); } } new_glyphs = layout_glyphs.next().fuse() => { if let Some(Some(new_glyphs)) = new_glyphs { glyphs = new_glyphs; - tracing::info!("{:?}", glyphs.lines().iter().map(|v| v.glyphs.len()).collect_vec()); if let Some(loc) = glyphs.to_glyph_boundary(editor.cursor()) { cursor_pos = loc; @@ -186,7 +195,7 @@ impl Widget for TextInput { }); Stack::new(( - SignalWidget(self.content.signal_cloned().map(move |v| { + StreamWidget(self.content.clone().project_stream_owned().map(move |v| { to_owned![text_bounds]; Text::rich([TextSegment::new(v)]) .with_font_size(self.style.font_size) @@ -232,6 +241,59 @@ fn handle_input(input: KeyboardInput) -> Option { None } -struct InputField { - value: Mutable, +pub struct InputField { + label: String, + value: Arc>, +} + +impl InputField { + pub fn new( + label: impl Into, + value: impl 'static + Send + Sync + ProjectDuplex, + ) -> Self { + Self { + label: label.into(), + value: Arc::new(value), + } + } +} + +impl Widget for InputField { + fn mount(self, scope: &mut Scope<'_>) { + let text_value = Mutable::new(String::new()); + let value = self.value.clone(); + + scope.spawn( + text_value + .signal_cloned() + .dedupe_cloned() + .to_stream() + .filter_map(|v| { + tracing::info!(?v, "Parsing"); + ready(v.trim().parse().ok()) + }) + .for_each(move |v| { + tracing::info!("Parsed: {}", v); + value.project_send(v); + async {} + }), + ); + + scope.spawn( + self.value + .project_stream_owned() + .map(|v| v.to_string()) + .for_each({ + to_owned![text_value]; + move |v| { + text_value.set(v); + async {} + } + }), + ); + + let editor = TextInput::new(text_value); + + row((Text::new(self.label), editor)).mount(scope); + } } diff --git a/violet-core/src/widget/interactive/slider.rs b/violet-core/src/widget/interactive/slider.rs index 758eae8..b1081f9 100644 --- a/violet-core/src/widget/interactive/slider.rs +++ b/violet-core/src/widget/interactive/slider.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::{fmt::Display, str::FromStr, sync::Arc}; use cosmic_text::Wrap; use flax::{Component, Entity, EntityRef}; @@ -10,9 +10,10 @@ use winit::event::ElementState; use crate::{ components::{offset, rect}, + editor::TextEditor, input::{focusable, on_cursor_move, on_mouse_input, CursorMove}, layout::Alignment, - project::{ProjectDuplex, ProjectStreamOwned}, + project::{MappedDuplex, ProjectDuplex, ProjectStreamOwned}, style::{get_stylesheet, interactive_active, interactive_inactive, spacing, StyleExt}, text::TextSegment, unit::Unit, @@ -21,6 +22,8 @@ use crate::{ Edges, Scope, StreamEffect, Widget, }; +use super::input::{InputField, TextInput}; + #[derive(Debug, Clone, Copy)] pub struct SliderStyle { pub track_color: Component, @@ -109,7 +112,7 @@ impl Widget for Slider { } let handle = SliderHandle { - value: self.value.project_stream_copy(), + value: self.value.project_stream_owned(), min, max, rect_id: track, @@ -142,7 +145,7 @@ impl Widget for Slider { if self.label { row(( slider, - StreamWidget(self.value.project_stream_copy().map(|v| { + StreamWidget(self.value.project_stream_owned().map(|v| { Text::rich([TextSegment::new(format!("{:>4.2}", v))]).with_wrap(Wrap::None) })), )) @@ -166,7 +169,7 @@ impl Widget for SliderHandle { fn mount(self, scope: &mut Scope<'_>) { let rect_size = Mutable::new(None); - let update = zip_latest_clone(self.value, rect_size.project_stream_copy()); + let update = zip_latest_clone(self.value, rect_size.project_stream_owned()); scope.frame_mut().monitor(self.rect_id, rect(), move |v| { rect_size.set(v.map(|v| v.size())); @@ -174,7 +177,6 @@ impl Widget for SliderHandle { scope.spawn_effect(StreamEffect::new(update, { move |scope: &mut Scope<'_>, (value, size): (V, Option)| { - tracing::info!(value = value.to_progress(), ?size, "update"); if let Some(size) = size { let pos = (value.to_progress() - self.min) * size.x / (self.max - self.min); @@ -256,10 +258,62 @@ impl SliderWithLabel { impl Widget for SliderWithLabel { fn mount(self, scope: &mut Scope<'_>) { let label = - StreamWidget(self.slider.value.project_stream_copy().map(|v| { + StreamWidget(self.slider.value.project_stream_owned().map(|v| { Text::rich([TextSegment::new(format!("{:>4.2}", v))]).with_wrap(Wrap::None) })); crate::widget::List::new((self.slider, label)).mount(scope) } } + +/// A slider with label displaying the value +pub struct SliderWithInput { + text_value: TextInput, + slider: Slider, +} + +impl SliderWithInput { + pub fn new(value: impl 'static + Send + Sync + ProjectDuplex, min: V, max: V) -> Self + where + V: Copy, + { + let value = Arc::new(value); + let text_value = TextInput::new(MappedDuplex::new( + value.clone(), + move |v| format!("{v}"), + move |v| { + let v = v.parse::().unwrap_or_default(); + if v < min { + min + } else if v > max { + max + } else { + v + } + }, + )); + + Self { + text_value, + slider: Slider { + style: Default::default(), + value, + min, + max, + label: false, + }, + } + } + + /// Set the style + pub fn with_style(mut self, style: SliderStyle) -> Self { + self.slider = self.slider.with_style(style); + self + } +} + +impl Widget for SliderWithInput { + fn mount(self, scope: &mut Scope<'_>) { + row((self.slider, self.text_value)).mount(scope) + } +} diff --git a/violet-wgpu/src/renderer/debug_renderer.rs b/violet-wgpu/src/renderer/debug_renderer.rs index adbbf76..0619e5f 100644 --- a/violet-wgpu/src/renderer/debug_renderer.rs +++ b/violet-wgpu/src/renderer/debug_renderer.rs @@ -122,41 +122,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 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.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 color: Vec3 = [ - clamped_query_vertical, - clamped_query_horizontal, - clamped_layout, - ] - .into_iter() - .sum(); - - if color == Vec3::ZERO { - None - } else { - Some((entity, color.extend(1.0))) - } - }); + // let color: Vec3 = [ + // clamped_query_vertical, + // clamped_query_horizontal, + // clamped_layout, + // ] + // .into_iter() + // .sum(); + + // if color == Vec3::ZERO { + // None + // } else { + // Some((entity, color.extend(1.0))) + // } + // }); let mut query = Query::new((entity_refs(), layout_cache())); let mut query = query.borrow(&frame.world); @@ -185,32 +185,30 @@ impl DebugRenderer { Some((entity, color)) }); - let objects = clamped_indicators - .chain(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 = 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, + )) + }); self.objects.clear(); self.objects.extend(objects); From 6ef79dd414a778c7c1d25b57dc0dfde3b8c940ba Mon Sep 17 00:00:00 2001 From: Freja Roberts Date: Sat, 9 Mar 2024 17:16:01 +0100 Subject: [PATCH 4/7] wip: refined state projection --- violet-core/src/project/duplex.rs | 82 +++++ .../src/{project.rs => project/mod.rs} | 322 ++++++++++++------ violet-core/src/widget/interactive/input.rs | 6 +- violet-core/src/widget/interactive/slider.rs | 53 +-- violet-web-example/src/lib.rs | 200 +---------- 5 files changed, 341 insertions(+), 322 deletions(-) create mode 100644 violet-core/src/project/duplex.rs rename violet-core/src/{project.rs => project/mod.rs} (50%) diff --git a/violet-core/src/project/duplex.rs b/violet-core/src/project/duplex.rs new file mode 100644 index 0000000..799d512 --- /dev/null +++ b/violet-core/src/project/duplex.rs @@ -0,0 +1,82 @@ +use std::{marker::PhantomData, sync::Arc}; + +use futures::{stream::BoxStream, Stream, StreamExt}; + +use super::{ProjectOwned, ProjectRef, ProjectSink, ProjectStreamOwned, ProjectStreamRef}; + +/// A [`Mutable`](futures_signals::signal::Mutable) that is mapped to project a container of `T` to +/// owned instances of `U`. +/// +/// In a way, this acts as a duplex Sink, that allowes sending a U, and a stream for receiving a U. +pub struct MappedDuplex { + inner: C, + into: Arc, + from: G, + _marker: std::marker::PhantomData<(T, U)>, +} + +impl U, G: Fn(U) -> T> MappedDuplex { + pub fn new(inner: C, into: F, from: G) -> Self { + Self { + inner, + into: Arc::new(into), + from, + _marker: PhantomData, + } + } +} + +impl, T, U: Clone, F: Fn(&T) -> U, G> ProjectRef + for MappedDuplex +{ + fn project V>(&self, f: H) -> V { + f(&self.project_owned()) + } +} +impl, T, U: Clone, F: Fn(&T) -> U, G> ProjectOwned + for MappedDuplex +{ + fn project_owned(&self) -> U { + self.inner.project(|v| (self.into)(v)) + } +} + +impl, T, U, F: Fn(&T) -> U, G: Fn(U) -> T> ProjectSink + for MappedDuplex +{ + fn project_send(&self, value: U) { + self.inner.project_send((self.from)(value)); + } +} + +impl, T, U, F: Fn(&T) -> U, G: Fn(U) -> T> ProjectStreamRef + for MappedDuplex +where + T: 'static + Send + Sync, + U: 'static + Send, + F: 'static + Send + Sync + Fn(&T) -> U, +{ + fn project_stream V, V: 'static>( + &self, + mut func: H, + ) -> impl 'static + Send + Stream { + let f = self.into.clone(); + self.inner.project_stream(move |v| func(&f(v))).boxed() + } +} + +impl, T, U, F: Fn(&T) -> U, G: Fn(U) -> T> ProjectStreamOwned + for MappedDuplex +where + T: 'static + Send + Sync, + U: 'static + Send, + F: 'static + Send + Sync + Fn(&T) -> U, +{ + fn project_stream_owned(&self) -> BoxStream<'static, U> + where + U: 'static, + { + let f = self.into.clone(); + self.inner.project_stream(move |v| f(v)).boxed() + } +} diff --git a/violet-core/src/project.rs b/violet-core/src/project/mod.rs similarity index 50% rename from violet-core/src/project.rs rename to violet-core/src/project/mod.rs index eeb3446..254539b 100644 --- a/violet-core/src/project.rs +++ b/violet-core/src/project/mod.rs @@ -3,6 +3,9 @@ use std::{marker::PhantomData, rc::Rc, sync::Arc}; use futures::{stream::BoxStream, Stream, StreamExt}; use futures_signals::signal::{Mutable, SignalExt}; +mod duplex; +pub use duplex::*; + /// A trait to project an arbitrary type into a type `U`. /// /// This can be used to "map" signals to other signals and composing and decomposing larger state @@ -120,16 +123,109 @@ where self.signal_cloned().to_stream().boxed() } } -/// A [`Mutable`](futures_signals::signal::Mutable) that is mapped to project a container of `T` to -/// a `U`. -pub struct MappedState { + +/// A trait to read a reference from a generic state +pub trait StateRef { + type Item; + fn read_ref V, V>(&self, f: F) -> V; +} + +/// Allows reading an owned value from a state +pub trait StateOwned: StateRef { + fn read(&self) -> Self::Item; +} + +/// A trait to read a mutable reference from a generic state. +/// +/// As opposed to [`StateSink`], this allows in place mutation or partial mutation of the state. +/// +/// Used as a building block for sinks to target e.g; specific fields of a struct. +pub trait StateMut: StateRef { + fn write_mut V, V>(&self, f: F) -> V; +} + +pub trait State { + type Item; +} + +/// A trait to read a stream from a generic state +pub trait StateStream: State { + /// Subscribe to a stream of the 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 V, V: 'static>( + &self, + func: F, + ) -> impl Stream + 'static + Send + where + Self: Sized; +} + +pub trait StateStreamOwned: StateStream { + fn stream_owned(&self) -> BoxStream<'static, Self::Item>; +} + +/// A trait to send a value to a generic state +pub trait StateSink: State { + /// Send a value to the state + fn send(&self, value: Self::Item); +} + +/// Allows sending and receiving a value to a state +pub trait StateDuplex: StateStreamOwned + StateSink {} + +impl StateDuplex for T where T: StateStreamOwned + StateSink {} + +impl State for Mutable { + type Item = T; +} + +impl StateRef for Mutable { + type Item = T; + fn read_ref V, V>(&self, f: F) -> V { + f(&self.lock_ref()) + } +} + +impl StateOwned for Mutable { + fn read(&self) -> Self::Item { + self.get_cloned() + } +} + +impl StateMut for Mutable { + fn write_mut V, V>(&self, f: F) -> V { + f(&mut self.lock_mut()) + } +} + +impl StateStream for Mutable +where + T: 'static + Send + Sync, +{ + fn stream V, V: 'static>( + &self, + func: F, + ) -> impl Stream + 'static + Send { + self.signal_ref(func).to_stream() + } +} + +/// Transforms one state of to another type through reference projection. +/// +/// This is used to lower a state `T` of a struct to a state `U` of a field of that struct. +/// +/// Can be used both to mutate and read the state, as well as to operate as a duplex sink and +/// stream. +pub struct MappedState { inner: C, project: Arc, project_mut: G, - _marker: std::marker::PhantomData<(T, U)>, + _marker: PhantomData, } -impl, T, U, F: Fn(&T) -> &U, G: Fn(&mut T) -> &mut U> MappedState { +impl &U, G: Fn(&mut C::Item) -> &mut U> MappedState { pub fn new(inner: C, project: F, project_mut: G) -> Self { Self { inner, @@ -140,160 +236,164 @@ impl, T, U, F: Fn(&T) -> &U, G: Fn(&mut T) -> &mut U> MappedSta } } -impl, T, U, F, G> MappedState { - pub fn get(&self) -> U - where - U: Copy, - F: Fn(&T) -> &U, - { - self.project(|v| *v) - } - - pub fn get_cloned(&self) -> U - where - U: Clone, - F: Fn(&T) -> &U, - { - self.project(|v| v.clone()) - } +impl State for MappedState { + type Item = U; } -impl, T, U, F: Fn(&T) -> &U, G> ProjectRef for MappedState { - fn project V>(&self, f: H) -> V { - self.inner.project(|v| f((self.project)(v))) +impl StateRef for MappedState +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))) } } -impl, T, U: Clone, F: Fn(&T) -> &U, G> ProjectOwned - for MappedState +impl StateOwned for MappedState +where + C: StateRef, + U: Clone, + F: Fn(&C::Item) -> &U, { - fn project_owned(&self) -> U { - self.project(|v| v.clone()) + fn read(&self) -> Self::Item { + self.read_ref(|v| v.clone()) } } -impl, T, U, F, G: Fn(&mut T) -> &mut U> ProjectMut - for MappedState +impl StateMut for MappedState +where + C: StateMut, + F: Fn(&C::Item) -> &U, + G: Fn(&mut C::Item) -> &mut U, { - fn project_mut V>(&self, f: H) -> V { - self.inner.project_mut(|v| f((self.project_mut)(v))) + fn write_mut V, V>(&self, f: H) -> V { + self.inner.write_mut(|v| f((self.project_mut)(v))) } } -impl, T, U, F, G: Fn(&mut T) -> &mut U> ProjectSink - for MappedState +impl StateStream for MappedState +where + C: StateStream, + F: 'static + Fn(&C::Item) -> &U + Sync + Send, { - fn project_send(&self, value: U) { - self.project_mut(|v| *v = value); + fn stream V, V: 'static>( + &self, + mut func: I, + ) -> impl Stream + 'static + Send { + let project = self.project.clone(); + self.inner.stream(move |v| func(project(v))) } } -impl, T, U, F: Fn(&T) -> &U, G> ProjectStreamRef - for MappedState +impl StateStreamOwned for MappedState where - T: 'static + Send + Sync, - U: 'static + Send, - F: 'static + Send + Sync + Fn(&T) -> &U, + C: StateStream, + U: 'static + Clone, + F: 'static + Fn(&C::Item) -> &U + Sync + Send, { - fn project_stream V, V: 'static>( - &self, - mut func: H, - ) -> impl Stream + 'static { - let p = self.project.clone(); - - let func = Box::new(move |v: &T| -> V { func(p(v)) }) as Box V>; - - self.inner.project_stream(func) + fn stream_owned(&self) -> BoxStream<'static, Self::Item> { + self.stream(|v| v.clone()).boxed() } } -impl, T, U, F: Fn(&T) -> &U, G> ProjectStreamOwned - for MappedState +/// Bridge update-by-reference to update-by-value +impl StateSink for MappedState where - T: 'static + Send + Sync, - U: 'static + Clone + Send, - F: 'static + Send + Sync + Fn(&T) -> &U, + C: StateMut, + F: Fn(&C::Item) -> &U, + G: Fn(&mut C::Item) -> &mut U, { - fn project_stream_owned(&self) -> BoxStream<'static, U> { - let f = self.project.clone(); - self.inner.project_stream(move |v| f(v).clone()).boxed() + fn send(&self, value: Self::Item) { + self.write_mut(|v| *v = value); } } -/// A [`Mutable`](futures_signals::signal::Mutable) that is mapped to project a container of `T` to -/// owned instances of `U`. +/// Transforms one state to another through type conversion +/// +/// +/// This allows deriving state from another where the derived state is not present in the original. /// -/// In a way, this acts as a duplex Sink, that allowes sending a U, and a stream for receiving a U. -pub struct MappedDuplex { +/// However, as this does not assume the derived state is contained withing the original state is +/// does not allow in-place mutation. +pub struct MappedDuplex { inner: C, - into: Arc, - from: G, - _marker: std::marker::PhantomData<(T, U)>, + project: Arc, + project_mut: G, + _marker: PhantomData, } -impl U, G: Fn(U) -> T> MappedDuplex { - pub fn new(inner: C, into: F, from: G) -> Self { +impl State for MappedDuplex { + type Item = U; +} + +impl U, G: Fn(&U) -> C::Item> MappedDuplex { + pub fn new(inner: C, project: F, project_mut: G) -> Self { Self { inner, - into: Arc::new(into), - from, + project: Arc::new(project), + project_mut, _marker: PhantomData, } } } -impl, T, U: Clone, F: Fn(&T) -> U, G> ProjectRef - for MappedDuplex +impl StateRef for MappedDuplex +where + C: StateRef, + F: Fn(&C::Item) -> U, { - fn project V>(&self, f: H) -> V { - f(&self.project_owned()) + type Item = U; + fn read_ref V, V>(&self, f: H) -> V { + f(&self.inner.read_ref(|v| (self.project)(v))) } } -impl, T, U: Clone, F: Fn(&T) -> U, G> ProjectOwned - for MappedDuplex + +impl StateOwned for MappedDuplex +where + C: StateRef, + F: Fn(&C::Item) -> U, { - fn project_owned(&self) -> U { - self.inner.project(|v| (self.into)(v)) + fn read(&self) -> Self::Item { + self.inner.read_ref(|v| (self.project)(v)) } } -impl, T, U, F: Fn(&T) -> U, G: Fn(U) -> T> ProjectSink - for MappedDuplex +impl StateStream for MappedDuplex +where + C: StateStream, + F: 'static + Fn(&C::Item) -> U + Sync + Send, { - fn project_send(&self, value: U) { - self.inner.project_send((self.from)(value)); + fn stream V, V: 'static>( + &self, + mut func: I, + ) -> impl Stream + 'static + Send { + let project = self.project.clone(); + self.inner.stream(move |v| func(&project(v))) } } -impl, T, U, F: Fn(&T) -> U, G: Fn(U) -> T> ProjectStreamRef - for MappedDuplex +impl StateStreamOwned for MappedDuplex where - T: 'static + Send + Sync, - U: 'static + Send, - F: 'static + Send + Sync + Fn(&T) -> U, + C: StateStream, + U: 'static + Clone, + F: 'static + Fn(&C::Item) -> U + Sync + Send, { - fn project_stream V, V: 'static>( - &self, - mut func: H, - ) -> impl 'static + Send + Stream { - let f = self.into.clone(); - self.inner.project_stream(move |v| func(&f(v))).boxed() + fn stream_owned(&self) -> BoxStream<'static, Self::Item> { + self.stream(|v| v.clone()).boxed() } } -impl, T, U, F: Fn(&T) -> U, G: Fn(U) -> T> ProjectStreamOwned - for MappedDuplex +/// Bridge update-by-reference to update-by-value +impl StateSink for MappedDuplex where - T: 'static + Send + Sync, - U: 'static + Send, - F: 'static + Send + Sync + Fn(&T) -> U, + C: StateSink, + F: Fn(C::Item) -> U, + G: Fn(U) -> C::Item, { - fn project_stream_owned(&self) -> BoxStream<'static, U> - where - U: 'static, - { - let f = self.into.clone(); - self.inner.project_stream(move |v| f(v)).boxed() + fn send(&self, value: Self::Item) { + self.inner.send((self.project_mut)(value)) } } @@ -399,13 +499,13 @@ mod tests { let a = MappedState::new(state.clone(), |v| &v.0, |v| &mut v.0); - assert_eq!(a.get(), 1); + assert_eq!(a.read(), 1); - let mut stream1 = a.project_stream(|v| *v); - let mut stream2 = a.project_stream(|v| *v); + let mut stream1 = a.stream(|v| *v); + let mut stream2 = a.stream(|v| *v); assert_eq!(stream1.next().await, Some(1)); - a.project_mut(|v| *v = 2); + a.write_mut(|v| *v = 2); assert_eq!(stream1.next().await, Some(2)); assert_eq!(stream2.next().await, Some(2)); } @@ -416,13 +516,13 @@ mod tests { let a = MappedState::new(state.clone(), |v| &v.0, |v| &mut v.0); - let a = Box::new(a) as Box>; + let a = Box::new(a) as Box>; - let mut stream1 = a.project_stream_owned(); - let mut stream2 = a.project_stream_owned(); + let mut stream1 = a.stream_owned(); + let mut stream2 = a.stream_owned(); assert_eq!(stream1.next().await, Some(1)); - a.project_send(2); + a.send(2); assert_eq!(stream1.next().await, Some(2)); assert_eq!(stream2.next().await, Some(2)); } diff --git a/violet-core/src/widget/interactive/input.rs b/violet-core/src/widget/interactive/input.rs index 790d63f..20ce426 100644 --- a/violet-core/src/widget/interactive/input.rs +++ b/violet-core/src/widget/interactive/input.rs @@ -7,7 +7,6 @@ use futures_signals::signal::{self, Mutable, SignalExt}; use glam::{vec2, Vec2}; use itertools::Itertools; use palette::Srgba; -use tracing::info; use winit::{ event::ElementState, keyboard::{Key, NamedKey}, @@ -90,6 +89,7 @@ impl Widget for TextInput { let content = self.content.clone(); + let mut text_content = Mutable::new(String::new()); let mut editor = TextEditor::new(); let layout_glyphs = Mutable::new(None); @@ -124,6 +124,10 @@ impl Widget for TextInput { editor.apply_action(action); + let mut text = text_content.lock_mut(); + #[allow(unstable_name_collisions)] + text.extend(editor.lines().iter().map(|v| v.text()).intersperse("\n")); + content.project_send(editor.lines().iter().map(|v| v.text()).join("\n")); } } diff --git a/violet-core/src/widget/interactive/slider.rs b/violet-core/src/widget/interactive/slider.rs index b1081f9..79fb207 100644 --- a/violet-core/src/widget/interactive/slider.rs +++ b/violet-core/src/widget/interactive/slider.rs @@ -277,32 +277,33 @@ impl SliderWithInput< where V: Copy, { - let value = Arc::new(value); - let text_value = TextInput::new(MappedDuplex::new( - value.clone(), - move |v| format!("{v}"), - move |v| { - let v = v.parse::().unwrap_or_default(); - if v < min { - min - } else if v > max { - max - } else { - v - } - }, - )); - - Self { - text_value, - slider: Slider { - style: Default::default(), - value, - min, - max, - label: false, - }, - } + todo!() + // let value = Arc::new(value); + // let text_value = TextInput::new(MappedDuplex::new( + // value.clone(), + // move |v| format!("{v}"), + // move |v| { + // let v = v.parse::().unwrap_or_default(); + // if v < min { + // min + // } else if v > max { + // max + // } else { + // v + // } + // }, + // )); + + // Self { + // text_value, + // slider: Slider { + // style: Default::default(), + // value, + // min, + // max, + // label: false, + // }, + // } } /// Set the style diff --git a/violet-web-example/src/lib.rs b/violet-web-example/src/lib.rs index ffa7e19..215ce11 100644 --- a/violet-web-example/src/lib.rs +++ b/violet-web-example/src/lib.rs @@ -46,200 +46,32 @@ pub async fn run() { violet::wgpu::App::new().run(MainApp).unwrap(); } -struct Vec2Editor { - value: Mutable, - x_label: String, - y_label: String, -} - -impl Vec2Editor { - fn new(value: Mutable, x_label: impl Into, y_label: impl Into) -> Self { - Self { - value, - x_label: x_label.into(), - y_label: y_label.into(), - } - } -} - -impl Widget for Vec2Editor { - fn mount(self, scope: &mut Scope<'_>) { - let value = self.value; - - column(( - row(( - label(self.x_label), - SliderWithLabel::new_with_transform( - value.clone(), - 0.0, - 200.0, - |v| v.x, - |v, x| v.x = x.round(), - ), - )), - row(( - label(self.y_label), - SliderWithLabel::new_with_transform( - value.clone(), - 0.0, - 200.0, - |v| v.y, - |v, y| v.y = y.round(), - ), - )), - )) - .mount(scope) - } -} struct MainApp; impl Widget for MainApp { fn mount(self, scope: &mut Scope<'_>) { - let size = Mutable::new(vec2(100.0, 100.0)); + let color = Mutable::new(Vec3::new(0.0, 0.0, 0.0)); - column(( - card(column(( - Vec2Editor::new(size.clone(), "width", "height"), - SignalWidget::new(size.signal().map(|size| label(format!("Rectangle size: {size}")))), - ))), - row((label("This is a row of longer text that is wrapped. When the text wraps it will take up more vertical space in the layout, and will as such increase the overall height"), card(Text::new(":P").with_wrap(Wrap::None)))), - SignalWidget::new(size.signal().map(|size| FlowSizing { size })), - // AnimatedSize, - )) - .contain_margins(true) - .with_background(Background::new(EERIE_BLACK_DEFAULT)) - .mount(scope) - } -} + let lightness = MappedState::new(color.clone(), |v| &v.x, |v| &mut v.x); + let chroma = MappedState::new(color.clone(), |v| &v.y, |v| &mut v.y); + let hue = MappedState::new(color.clone(), |v| &v.z, |v| &mut v.z); -struct FlowSizing { - size: Vec2, -} - -impl Widget for FlowSizing { - fn mount(self, scope: &mut Scope<'_>) { - let bg = Background::new(JADE_200); - - 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"), - AnimatedSize, - ); + let color_rect = color.signal().map(|v| { + let color = Oklch::new(v.x, v.y, v.z).into_color(); + Rectangle::new(color).with_min_size(Unit::px2(200.0, 100.0)) + }); column(( row(( - card(column(( - label("Unconstrained list"), - row(content.clone()).with_background(bg), - ))), - card(column(( - label("Constrained list with min size"), - row(content.clone()) - .with_background(bg) - .with_min_size(Unit::px2(100.0, 100.0)), - ))), - card(column(( - label("Constrained list with max size"), - row(content.clone()) - .with_background(bg) - .with_max_size(Unit::px2(100.0, 100.0)), - ))), - )), - row(( - card(column(( - label("Unconstrained list"), - centered(content.clone()).with_background(bg), - ))), - card(column(( - label("Constrained list with min size"), - centered(content.clone()) - .with_background(bg) - .with_min_size(Unit::px2(100.0, 100.0)), - ))), - card(column(( - label("Constrained list with max size"), - centered(content.clone()) - .with_background(bg) - .with_max_size(Unit::px2(100.0, 100.0)), - ))), + Text::new("Lightness"), + SliderWithInput::new(lightness, 0.0, 1.0), )), + row((Text::new("Chroma"), SliderWithInput::new(chroma, 0.0, 0.37))), + row((Text::new("Hue"), SliderWithInput::new(hue, 0.0, 360.0))), + SignalWidget(color.signal().map(|v| Text::new(format!("{}", v)))), + card(SignalWidget(color_rect)), )) - .mount(scope) + .with_margin(Edges::even(4.0)) + .mount(scope); } } - -#[derive(Debug, Clone)] -struct SizedBox { - color: Srgba, - size: Unit, -} - -impl SizedBox { - fn new(color: Srgba, size: Unit) -> Self { - Self { color, size } - } -} - -impl Widget for SizedBox { - fn mount(self, scope: &mut Scope<'_>) { - // Stack::new(( - Rectangle::new(self.color) - .with_size(self.size) - // column(( - // Text::new(format!("{}", self.size.px)), - // Text::new(format!("{}", self.size.rel)), - // )), - // )) - .mount(scope) - } -} - -#[derive(Debug, Clone)] -pub struct AnimatedSize; - -impl Widget for AnimatedSize { - fn mount(self, scope: &mut Scope<'_>) { - scope.set(name(), "AnimatedBox".into()); - scope.set( - components::on_animation_frame(), - Box::new(move |_, entity, t| { - let t = t.as_secs_f32(); - - let size = vec2(t.sin() * 50.0, (t * 2.5).cos() * 50.0) + vec2(100.0, 100.0); - entity.update_dedup(components::size(), Unit::px(size)); - }), - ); - - Rectangle::new(LION_DEFAULT).mount(scope) - } -} -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)) -} - -fn row(widgets: W) -> List { - List::new(widgets).with_direction(Direction::Horizontal) -} - -fn column(widgets: W) -> List { - List::new(widgets).with_direction(Direction::Vertical) -} - -fn centered(widget: W) -> Stack { - Stack::new(widget) - .with_horizontal_alignment(Alignment::Center) - .with_vertical_alignment(Alignment::Center) -} - -fn card(widget: W) -> Stack { - Stack::new(widget) - .with_background(Background::new(EERIE_BLACK_400)) - .with_padding(MARGIN) - .with_margin(MARGIN) -} - -const MARGIN: Edges = Edges::even(8.0); -const MARGIN_SM: Edges = Edges::even(4.0); From 910221702fae8d44f435b19b00b839b3488e5cde Mon Sep 17 00:00:00 2001 From: Freja Roberts Date: Sun, 10 Mar 2024 19:34:34 +0100 Subject: [PATCH 5/7] feat: even more state projection --- Cargo.lock | 4 +- Cargo.toml | 1 + examples/color.rs | 114 ++++++- examples/flow.rs | 42 +-- violet-core/src/editor.rs | 11 + violet-core/src/layout/mod.rs | 9 + violet-core/src/project/bridge.rs | 9 + violet-core/src/project/dedup.rs | 97 ++++++ violet-core/src/project/duplex.rs | 80 ----- violet-core/src/project/filter.rs | 59 ++++ violet-core/src/project/mod.rs | 340 +++++++------------ violet-core/src/widget/future.rs | 69 ++-- violet-core/src/widget/interactive/input.rs | 89 ++--- violet-core/src/widget/interactive/slider.rs | 174 +++++----- violet-web-example/src/lib.rs | 16 +- 15 files changed, 619 insertions(+), 495 deletions(-) create mode 100644 violet-core/src/project/bridge.rs create mode 100644 violet-core/src/project/dedup.rs create mode 100644 violet-core/src/project/filter.rs diff --git a/Cargo.lock b/Cargo.lock index c9ab882..781b185 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -573,9 +573,9 @@ checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" [[package]] name = "either" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" [[package]] name = "equivalent" diff --git a/Cargo.toml b/Cargo.toml index 15bf917..8add1ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ once_cell = "1.18" slab = "0.4" tynm ="0.1" tokio = { version = "1.0", default-features = false, features = ["macros", "rt"] } +either = "1.10" bytemuck = { version = "1.13", features = ["derive"] } winit = "0.29" diff --git a/examples/color.rs b/examples/color.rs index 27deefa..ab5ac4f 100644 --- a/examples/color.rs +++ b/examples/color.rs @@ -1,13 +1,19 @@ +use futures::StreamExt; use futures_signals::signal::{Mutable, SignalExt}; use glam::Vec3; -use palette::{IntoColor, Oklch}; +use itertools::Itertools; +use palette::{num::Powi, FromColor, IntoColor, Lighten, Oklch, Srgb}; +use tracing::info; use tracing_subscriber::{layer::SubscriberExt, registry, util::SubscriberInitExt, EnvFilter}; use tracing_tree::HierarchicalLayer; use violet_core::{ - project::{MappedDuplex, MappedState}, - style::SizeExt, + project::{Map, MappedState, StateStream, StateStreamRef}, + style::{colors::LION_500, SizeExt}, unit::Unit, - widget::{card, column, row, InputField, Rectangle, SignalWidget, SliderWithInput, Text}, + utils::zip_latest_clone, + widget::{ + card, column, row, Rectangle, SignalWidget, SliderWithLabel, Stack, StreamWidget, Text, + }, Edges, Scope, Widget, }; use violet_wgpu::renderer::RendererConfig; @@ -33,7 +39,12 @@ struct MainApp; impl Widget for MainApp { fn mount(self, scope: &mut Scope<'_>) { - let color = Mutable::new(Vec3::new(0.0, 0.0, 0.0)); + 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 = MappedState::new(color.clone(), |v| &v.x, |v| &mut v.x); let chroma = MappedState::new(color.clone(), |v| &v.y, |v| &mut v.y); @@ -44,17 +55,88 @@ impl Widget for MainApp { Rectangle::new(color).with_min_size(Unit::px2(200.0, 100.0)) }); - column(( - row(( - Text::new("Lightness"), - SliderWithInput::new(lightness, 0.0, 1.0), - )), - row((Text::new("Chroma"), SliderWithInput::new(chroma, 0.0, 0.37))), - row((Text::new("Hue"), SliderWithInput::new(hue, 0.0, 360.0))), - SignalWidget(color.signal().map(|v| Text::new(format!("{}", v)))), - card(SignalWidget(color_rect)), - )) - .with_margin(Edges::even(4.0)) + let falloff = Mutable::new(50.0); + + 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_clone(color_oklch.stream(), falloff.stream()) + .map(|(color, falloff)| Tints::new(color, falloff)), + ), + )) + .with_stretch(true) + .with_margin(Edges::even(4.0)), + ) + .with_size(Unit::rel2(1.0, 1.0)) .mount(scope); } } + +struct Tints { + base: Oklch, + falloff: f32, +} + +impl Tints { + fn new(base: Oklch, falloff: f32) -> Self { + Self { base, falloff } + } +} + +impl Widget for Tints { + fn mount(self, scope: &mut Scope<'_>) { + row((1..=9) + .map(|i| { + 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 = self.base.lighten(f); + let color = Oklch { + chroma, + l: f, + ..self.base + }; + + Stack::new(column(( + Rectangle::new(color.into_color()).with_min_size(Unit::px2(60.0, 60.0)), + Text::new(format!("{:.2}", f)), + ))) + .with_margin(Edges::even(4.0)) + }) + .collect_vec()) + .mount(scope) + } +} diff --git a/examples/flow.rs b/examples/flow.rs index a7e4aec..3315fd8 100644 --- a/examples/flow.rs +++ b/examples/flow.rs @@ -1,49 +1,31 @@ use std::usize; -use futures_signals::{ - map_ref, - signal::{self, Mutable, SignalExt}, -}; +use futures_signals::{map_ref, signal::Mutable}; -use glam::{vec2, Vec2}; use itertools::Itertools; -use palette::{num::Round, FromColor, Hsva, IntoColor, Oklcha, Srgba}; +use palette::{FromColor, Hsva, IntoColor, Oklcha, Srgba}; use tracing_subscriber::{layer::SubscriberExt, registry, util::SubscriberInitExt, EnvFilter}; use tracing_tree::HierarchicalLayer; -use futures::stream::StreamExt; use violet::core::{ - components::{self, screen_rect}, - editor::{self, EditAction, EditorAction, TextEditor}, - input::{focusable, on_keyboard_input, on_mouse_input}, layout::Alignment, style::StyleExt, - text::{LayoutGlyphs, TextSegment}, - to_owned, unit::Unit, - widget::{List, NoOp, Rectangle, SignalWidget, Stack, Text, WidgetExt}, + widget::{List, Rectangle, SignalWidget, Stack, Text}, Scope, Widget, }; use violet_core::{ - input::{ - event::ElementState, - focus_sticky, - keyboard::{Key, NamedKey}, - KeyboardInput, - }, style::{ self, - colors::{ - EERIE_BLACK_300, EERIE_BLACK_600, EERIE_BLACK_DEFAULT, JADE_DEFAULT, LION_DEFAULT, - }, + colors::{EERIE_BLACK_300, EERIE_BLACK_600, EERIE_BLACK_DEFAULT}, Background, SizeExt, }, text::Wrap, widget::{ - card, column, row, BoxSized, Button, ButtonStyle, ContainerStyle, Positioned, Slider, - SliderWithLabel, TextInput, + card, column, row, BoxSized, Button, ButtonStyle, ContainerStyle, SliderWithLabel, + TextInput, }, - Edges, Rect, + Edges, }; use violet_wgpu::renderer::RendererConfig; @@ -126,8 +108,14 @@ impl Widget for MainApp { .with_size(Unit::rel2(1.0, 0.0) + Unit::px2(0.0, 1.0)), card(column(( column(( - row((Text::new("Size"), SliderWithLabel::new(value, 20.0, 200.0))), - row((Text::new("Count"), SliderWithLabel::new(count, 1, 20))), + row(( + Text::new("Size"), + SliderWithLabel::new(value, 20.0, 200.0).editable(true), + )), + row(( + Text::new("Count"), + SliderWithLabel::new(count, 1, 2).editable(true), + )), )), SignalWidget::new(item_list), ))), diff --git a/violet-core/src/editor.rs b/violet-core/src/editor.rs index 52fde1c..941649e 100644 --- a/violet-core/src/editor.rs +++ b/violet-core/src/editor.rs @@ -302,11 +302,22 @@ impl TextEditor { } pub fn set_text<'a>(&mut self, text: impl IntoIterator) { + let at_end_col = self.cursor.col >= self.text[self.cursor.row].len(); + let at_end_row = self.cursor.row >= self.text.len() - 1; + self.text.clear(); self.text.extend(text.into_iter().map(EditorLine::new)); self.cursor.row = self.cursor.row.min(self.text.len() - 1); self.cursor.col = self.cursor.col.min(self.text[self.cursor.row].len()); + + if at_end_row { + self.cursor.row = self.text.len() - 1; + } + + if at_end_col { + self.cursor.col = self.text[self.cursor.row].len(); + } } pub fn set_cursor(&mut self, row: usize, col: usize) { diff --git a/violet-core/src/layout/mod.rs b/violet-core/src/layout/mod.rs index 7286750..d16adf9 100644 --- a/violet-core/src/layout/mod.rs +++ b/violet-core/src/layout/mod.rs @@ -352,6 +352,9 @@ pub(crate) fn query_size( preferred: sizing.preferred.pad(&padding), hints: sizing.hints.combine(hints), } + } else if let [child] = children { + let child = world.entity(*child).unwrap(); + query_size(world, &child, content_area, limits, direction) } else { let (instrisic_min_size, intrinsic_size, intrinsic_hints) = size_resolver .map(|v| v.query(entity, content_area, limits, direction)) @@ -490,6 +493,12 @@ pub(crate) fn update_subtree( block.margin = (block.margin - padding).max(margin); + block + } else if let [child] = children { + let child = world.entity(*child).unwrap(); + let block = update_subtree(world, &child, content_area, limits); + + child.update_dedup(components::rect(), block.rect); block } else { assert_eq!(children, [], "Widget with children must have a layout"); diff --git a/violet-core/src/project/bridge.rs b/violet-core/src/project/bridge.rs new file mode 100644 index 0000000..c0dc6ab --- /dev/null +++ b/violet-core/src/project/bridge.rs @@ -0,0 +1,9 @@ +/// Bridge two different states together. +/// +/// This is a bidirectional bridge, meaning that it can be used to bridge two states together +/// +/// State updates from `A` will be sent to `B` and vice versa. +pub struct Bridge { + a: A, + b: B, +} diff --git a/violet-core/src/project/dedup.rs b/violet-core/src/project/dedup.rs new file mode 100644 index 0000000..58f917b --- /dev/null +++ b/violet-core/src/project/dedup.rs @@ -0,0 +1,97 @@ +use std::{future::ready, sync::Arc}; + +use futures::{FutureExt, StreamExt}; +use futures_signals::signal::{Mutable, SignalExt}; +use parking_lot::Mutex; +use tracing::info; + +use super::{State, StateSink, StateStream, StateStreamRef}; + +/// Deduplicates a state updates for both sending and receiving halves. +/// +/// This means that if the same item is sent to the sink multiple times in a row, it will only be +/// sent once. +/// +/// Likewise, the stream will be filtered for duplicate items, to catch duplicates from external +/// sinks (as items can arrive from other sinks than the one that is being deduplicated). +pub struct Dedup { + last_sent: Mutable>, + inner: T, +} + +impl Dedup { + pub fn new(inner: T) -> Self { + Self { + inner, + last_sent: Default::default(), + } + } +} + +impl State for Dedup { + type Item = T::Item; +} + +impl StateStreamRef for Dedup +where + T: StateStreamRef, + T::Item: 'static + Send + Sync + Clone + PartialEq, +{ + fn stream_ref V, V: 'static + Send + Sync>( + &self, + mut func: F, + ) -> impl futures::prelude::Stream + 'static + Send + where + Self: Sized, + { + let mut last_seen = None; + let mut last_sent = self.last_sent.signal_cloned().to_stream(); + + self.inner + .stream_ref(move |item| { + let last_sent = last_sent.next().now_or_never().flatten().flatten(); + + if last_seen.as_ref() != Some(item) && last_sent.as_ref() != Some(item) { + last_seen = Some(item.clone()); + Some(func(item)) + } else { + None + } + }) + .filter_map(ready) + } +} + +impl StateStream for Dedup +where + T: StateStream, + T::Item: 'static + Send + Sync + PartialEq + Clone, +{ + fn stream(&self) -> futures::prelude::stream::BoxStream<'static, Self::Item> { + let mut last_seen = None; + let mut last_sent = self.last_sent.signal_cloned().to_stream(); + self.inner + .stream() + .filter_map(move |v| { + let last_sent = last_sent.next().now_or_never().flatten().flatten(); + if last_seen.as_ref() != Some(&v) && last_sent.as_ref() != Some(&v) { + last_seen = Some(v.clone()); + ready(Some(v)) + } else { + ready(None) + } + }) + .boxed() + } +} + +impl StateSink for Dedup +where + T: StateSink, + T::Item: 'static + Send + Sync + PartialEq + Clone, +{ + fn send(&self, item: Self::Item) { + self.last_sent.set(Some(item.clone())); + self.inner.send(item); + } +} diff --git a/violet-core/src/project/duplex.rs b/violet-core/src/project/duplex.rs index 799d512..139597f 100644 --- a/violet-core/src/project/duplex.rs +++ b/violet-core/src/project/duplex.rs @@ -1,82 +1,2 @@ -use std::{marker::PhantomData, sync::Arc}; -use futures::{stream::BoxStream, Stream, StreamExt}; -use super::{ProjectOwned, ProjectRef, ProjectSink, ProjectStreamOwned, ProjectStreamRef}; - -/// A [`Mutable`](futures_signals::signal::Mutable) that is mapped to project a container of `T` to -/// owned instances of `U`. -/// -/// In a way, this acts as a duplex Sink, that allowes sending a U, and a stream for receiving a U. -pub struct MappedDuplex { - inner: C, - into: Arc, - from: G, - _marker: std::marker::PhantomData<(T, U)>, -} - -impl U, G: Fn(U) -> T> MappedDuplex { - pub fn new(inner: C, into: F, from: G) -> Self { - Self { - inner, - into: Arc::new(into), - from, - _marker: PhantomData, - } - } -} - -impl, T, U: Clone, F: Fn(&T) -> U, G> ProjectRef - for MappedDuplex -{ - fn project V>(&self, f: H) -> V { - f(&self.project_owned()) - } -} -impl, T, U: Clone, F: Fn(&T) -> U, G> ProjectOwned - for MappedDuplex -{ - fn project_owned(&self) -> U { - self.inner.project(|v| (self.into)(v)) - } -} - -impl, T, U, F: Fn(&T) -> U, G: Fn(U) -> T> ProjectSink - for MappedDuplex -{ - fn project_send(&self, value: U) { - self.inner.project_send((self.from)(value)); - } -} - -impl, T, U, F: Fn(&T) -> U, G: Fn(U) -> T> ProjectStreamRef - for MappedDuplex -where - T: 'static + Send + Sync, - U: 'static + Send, - F: 'static + Send + Sync + Fn(&T) -> U, -{ - fn project_stream V, V: 'static>( - &self, - mut func: H, - ) -> impl 'static + Send + Stream { - let f = self.into.clone(); - self.inner.project_stream(move |v| func(&f(v))).boxed() - } -} - -impl, T, U, F: Fn(&T) -> U, G: Fn(U) -> T> ProjectStreamOwned - for MappedDuplex -where - T: 'static + Send + Sync, - U: 'static + Send, - F: 'static + Send + Sync + Fn(&T) -> U, -{ - fn project_stream_owned(&self) -> BoxStream<'static, U> - where - U: 'static, - { - let f = self.into.clone(); - self.inner.project_stream(move |v| f(v)).boxed() - } -} diff --git a/violet-core/src/project/filter.rs b/violet-core/src/project/filter.rs new file mode 100644 index 0000000..7a8971f --- /dev/null +++ b/violet-core/src/project/filter.rs @@ -0,0 +1,59 @@ +use std::{future::ready, marker::PhantomData, sync::Arc}; + +use futures::{stream::BoxStream, StreamExt}; + +use super::{State, StateSink, StateStream}; + +/// Transforms one type to another through fallible conversion. +pub struct FilterDuplex { + inner: C, + conv_to: Arc, + conv_from: G, + _marker: PhantomData, +} + +impl State for FilterDuplex { + type Item = U; +} + +impl Option, G: Fn(U) -> Option> + FilterDuplex +{ + pub fn new(inner: C, conv_to: F, conv_from: G) -> Self { + Self { + inner, + conv_to: Arc::new(conv_to), + conv_from, + _marker: PhantomData, + } + } +} + +impl StateStream for FilterDuplex +where + C: StateStream, + C::Item: 'static + Send, + U: 'static + Send + Sync + Clone, + F: 'static + Send + Sync + Fn(C::Item) -> Option, +{ + fn stream(&self) -> BoxStream<'static, Self::Item> { + let project = self.conv_to.clone(); + self.inner + .stream() + .filter_map(move |v| ready(project(v))) + .boxed() + } +} + +/// Bridge update-by-reference to update-by-value +impl StateSink for FilterDuplex +where + C: StateSink, + G: Fn(U) -> Option, +{ + fn send(&self, value: Self::Item) { + if let Some(v) = (self.conv_from)(value) { + self.inner.send(v) + } + } +} diff --git a/violet-core/src/project/mod.rs b/violet-core/src/project/mod.rs index 254539b..39ef9b0 100644 --- a/violet-core/src/project/mod.rs +++ b/violet-core/src/project/mod.rs @@ -1,127 +1,20 @@ +//! Module for state projection and transformation +//! +//! This simplifies the process of working with signals and mapping state from different types or +//! smaller parts of a larger state. use std::{marker::PhantomData, rc::Rc, sync::Arc}; use futures::{stream::BoxStream, Stream, StreamExt}; use futures_signals::signal::{Mutable, SignalExt}; -mod duplex; -pub use duplex::*; +mod dedup; +mod filter; -/// A trait to project an arbitrary type into a type `U`. -/// -/// This can be used to "map" signals to other signals and composing and decomposing larger state -/// into smaller parts for reactivity -pub trait ProjectRef { - fn project V>(&self, f: F) -> V; -} - -pub trait ProjectOwned: ProjectRef { - fn project_owned(&self) -> U; -} - -// impl ProjectOwned for T -// where -// T: ProjectRef, -// U: Clone, -// { -// fn project_owned(&self) -> U -// where -// U: Clone, -// { -// self.project(|v| v.clone()) -// } -// } - -/// Ability to project a mutable reference to a type `U`. -pub trait ProjectMut { - fn project_mut V>(&self, f: F) -> V - where - Self: Sized; -} - -/// Ability to receive a type of `U` -pub trait ProjectSink { - fn project_send(&self, value: U); -} - -// impl ProjectSink for T -// where -// T: Sized + ProjectMut, -// { -// fn project_send(&self, value: U) { -// self.project_mut(|v| *v = value); -// } -// } - -/// A trait to produce a stream projection of a type `U`. -pub trait ProjectStreamRef { - fn project_stream V, V: 'static>( - &self, - func: F, - ) -> impl 'static + Send + Stream - where - Self: Sized; -} - -pub trait ProjectStreamOwned: ProjectStreamRef { - fn project_stream_owned(&self) -> BoxStream<'static, U>; -} - -/// Supertrait for types that support both sending and receiving a type `U`. -pub trait ProjectDuplex: ProjectSink + ProjectStreamOwned {} - -/// Supertrait which support mutable and reference projection and streaming of a type `U`. -/// -/// This is the most general trait, and is useful for composing and decomposing state. -pub trait ProjectState: ProjectMut + ProjectStreamRef {} - -impl ProjectDuplex for T where T: ProjectSink + ProjectStreamOwned {} - -impl ProjectRef for Mutable { - fn project V>(&self, f: F) -> V { - f(&self.lock_ref()) - } -} - -impl ProjectOwned for Mutable { - fn project_owned(&self) -> T { - self.get_cloned() - } -} +pub use dedup::*; +pub use filter::*; -impl ProjectMut for Mutable { - fn project_mut V>(&self, f: F) -> V { - f(&mut self.lock_mut()) - } -} - -impl ProjectSink for Mutable { - fn project_send(&self, value: T) { - self.set(value); - } -} - -impl ProjectStreamRef for Mutable -where - T: 'static + Send + Sync, -{ - fn project_stream V, V: 'static>( - &self, - func: F, - ) -> impl 'static + Send + Stream { - self.signal_ref(func).to_stream() - } -} - -impl ProjectStreamOwned for Mutable -where - T: 'static + Send + Sync + Clone, -{ - fn project_stream_owned(&self) -> BoxStream<'static, T> - where - T: 'static + Clone, - { - self.signal_cloned().to_stream().boxed() - } +pub trait State { + type Item; } /// A trait to read a reference from a generic state @@ -131,7 +24,7 @@ pub trait StateRef { } /// Allows reading an owned value from a state -pub trait StateOwned: StateRef { +pub trait StateOwned: State { fn read(&self) -> Self::Item; } @@ -144,17 +37,13 @@ pub trait StateMut: StateRef { fn write_mut V, V>(&self, f: F) -> V; } -pub trait State { - type Item; -} - /// A trait to read a stream from a generic state -pub trait StateStream: State { +pub trait StateStreamRef: State { /// Subscribe to a stream of the 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 V, V: 'static>( + fn stream_ref V, V: 'static + Send + Sync>( &self, func: F, ) -> impl Stream + 'static + Send @@ -162,8 +51,8 @@ pub trait StateStream: State { Self: Sized; } -pub trait StateStreamOwned: StateStream { - fn stream_owned(&self) -> BoxStream<'static, Self::Item>; +pub trait StateStream: State { + fn stream(&self) -> BoxStream<'static, Self::Item>; } /// A trait to send a value to a generic state @@ -173,9 +62,9 @@ pub trait StateSink: State { } /// Allows sending and receiving a value to a state -pub trait StateDuplex: StateStreamOwned + StateSink {} +pub trait StateDuplex: StateStream + StateSink {} -impl StateDuplex for T where T: StateStreamOwned + StateSink {} +impl StateDuplex for T where T: StateStream + StateSink {} impl State for Mutable { type Item = T; @@ -200,11 +89,11 @@ impl StateMut for Mutable { } } -impl StateStream for Mutable +impl StateStreamRef for Mutable where T: 'static + Send + Sync, { - fn stream V, V: 'static>( + fn stream_ref V, V: 'static + Send + Sync>( &self, func: F, ) -> impl Stream + 'static + Send { @@ -212,6 +101,21 @@ where } } +impl StateStream for Mutable +where + T: 'static + Send + Sync + Clone, +{ + fn stream(&self) -> BoxStream<'static, Self::Item> { + self.signal_cloned().to_stream().boxed() + } +} + +impl StateSink for Mutable { + fn send(&self, value: Self::Item) { + self.set(value); + } +} + /// Transforms one state of to another type through reference projection. /// /// This is used to lower a state `T` of a struct to a state `U` of a field of that struct. @@ -273,28 +177,28 @@ where } } -impl StateStream for MappedState +impl StateStreamRef for MappedState where - C: StateStream, + C: StateStreamRef, F: 'static + Fn(&C::Item) -> &U + Sync + Send, { - fn stream V, V: 'static>( + fn stream_ref V, V: 'static + Send + Sync>( &self, mut func: I, ) -> impl Stream + 'static + Send { let project = self.project.clone(); - self.inner.stream(move |v| func(project(v))) + self.inner.stream_ref(move |v| func(project(v))) } } -impl StateStreamOwned for MappedState +impl StateStream for MappedState where - C: StateStream, - U: 'static + Clone, + C: StateStreamRef, + U: 'static + Send + Sync + Clone, F: 'static + Fn(&C::Item) -> &U + Sync + Send, { - fn stream_owned(&self) -> BoxStream<'static, Self::Item> { - self.stream(|v| v.clone()).boxed() + fn stream(&self) -> BoxStream<'static, Self::Item> { + self.stream_ref(|v| v.clone()).boxed() } } @@ -317,83 +221,60 @@ where /// /// However, as this does not assume the derived state is contained withing the original state is /// does not allow in-place mutation. -pub struct MappedDuplex { +pub struct Map { inner: C, - project: Arc, - project_mut: G, + conv_to: Arc, + conv_from: G, _marker: PhantomData, } -impl State for MappedDuplex { +impl State for Map { type Item = U; } -impl U, G: Fn(&U) -> C::Item> MappedDuplex { +impl U, G: Fn(U) -> C::Item> Map { pub fn new(inner: C, project: F, project_mut: G) -> Self { Self { inner, - project: Arc::new(project), - project_mut, + conv_to: Arc::new(project), + conv_from: project_mut, _marker: PhantomData, } } } -impl StateRef for MappedDuplex -where - C: StateRef, - F: Fn(&C::Item) -> U, -{ - type Item = U; - fn read_ref V, V>(&self, f: H) -> V { - f(&self.inner.read_ref(|v| (self.project)(v))) - } -} - -impl StateOwned for MappedDuplex +impl StateOwned for Map where - C: StateRef, - F: Fn(&C::Item) -> U, + C: StateOwned, + F: Fn(C::Item) -> U, { fn read(&self) -> Self::Item { - self.inner.read_ref(|v| (self.project)(v)) + (self.conv_to)(self.inner.read()) } } -impl StateStream for MappedDuplex +impl StateStream for Map where C: StateStream, - F: 'static + Fn(&C::Item) -> U + Sync + Send, + C::Item: 'static + Send, + U: 'static + Send + Sync, + F: 'static + Fn(C::Item) -> U + Sync + Send, { - fn stream V, V: 'static>( - &self, - mut func: I, - ) -> impl Stream + 'static + Send { - let project = self.project.clone(); - self.inner.stream(move |v| func(&project(v))) - } -} - -impl StateStreamOwned for MappedDuplex -where - C: StateStream, - U: 'static + Clone, - F: 'static + Fn(&C::Item) -> U + Sync + Send, -{ - fn stream_owned(&self) -> BoxStream<'static, Self::Item> { - self.stream(|v| v.clone()).boxed() + fn stream(&self) -> BoxStream<'static, Self::Item> { + let project = self.conv_to.clone(); + self.inner.stream().map(move |v| (project)(v)).boxed() } } /// Bridge update-by-reference to update-by-value -impl StateSink for MappedDuplex +impl StateSink for Map where C: StateSink, F: Fn(C::Item) -> U, G: Fn(U) -> C::Item, { fn send(&self, value: Self::Item) { - self.inner.send((self.project_mut)(value)) + self.inner.send((self.conv_from)(value)) } } @@ -416,51 +297,71 @@ where macro_rules! impl_container { ($ty: ident) => { - impl ProjectRef for $ty + impl State for $ty + where + T: ?Sized + State, + { + type Item = T::Item; + } + + impl StateRef for $ty where - T: ?Sized + ProjectRef, + T: StateRef, { - fn project V>(&self, f: F) -> V { - (**self).project(f) + type Item = T::Item; + fn read_ref V, V>(&self, f: F) -> V { + (**self).read_ref(f) } } - impl ProjectMut for $ty + impl StateOwned for $ty where - T: ProjectMut, + T: StateOwned, { - fn project_mut V>(&self, f: F) -> V { - (**self).project_mut(f) + fn read(&self) -> Self::Item { + (**self).read() } } - impl ProjectStreamRef for $ty + impl StateMut for $ty where - T: ProjectStreamRef, + T: StateMut, { - fn project_stream V, V: 'static>( + fn write_mut V, V>(&self, f: F) -> V { + (**self).write_mut(f) + } + } + + impl StateStreamRef for $ty + where + T: StateStreamRef, + { + fn stream_ref< + F: 'static + Send + Sync + FnMut(&Self::Item) -> V, + V: 'static + Send + Sync, + >( &self, func: F, - ) -> impl Stream + 'static { - (**self).project_stream(func) + ) -> impl Stream + 'static + Send { + (**self).stream_ref(func) } } - impl ProjectStreamOwned for $ty + impl StateStream for $ty where - T: ProjectStreamOwned, + T: ?Sized + StateStream, { - fn project_stream_owned(&self) -> BoxStream<'static, U> { - (**self).project_stream_owned() + fn stream(&self) -> BoxStream<'static, Self::Item> { + (**self).stream() } } - impl ProjectSink for $ty + impl StateSink for $ty where - T: ?Sized + ProjectSink, + T: ?Sized + StateSink, { - fn project_send(&self, value: U) { - (**self).project_send(value); + fn send(&self, value: Self::Item) { + (**self).send(value) } } }; @@ -470,11 +371,19 @@ impl_container!(Box); impl_container!(Arc); impl_container!(Rc); -impl ProjectStreamRef for flume::Receiver +impl State for flume::Receiver { + type Item = T; +} + +impl State for flume::Sender { + type Item = T; +} + +impl StateStreamRef for flume::Receiver where T: 'static + Send + Sync, { - fn project_stream V, V: 'static>( + fn stream_ref V, V: 'static + Send + Sync>( &self, mut func: F, ) -> impl 'static + Send + Stream { @@ -482,8 +391,17 @@ where } } -impl ProjectSink for flume::Sender { - fn project_send(&self, value: T) { +impl StateStream for flume::Receiver +where + T: 'static + Send + Sync, +{ + fn stream(&self) -> BoxStream<'static, Self::Item> { + self.clone().into_stream().boxed() + } +} + +impl StateSink for flume::Sender { + fn send(&self, value: T) { self.send(value).unwrap(); } } @@ -501,8 +419,8 @@ mod tests { assert_eq!(a.read(), 1); - let mut stream1 = a.stream(|v| *v); - let mut stream2 = a.stream(|v| *v); + let mut stream1 = a.stream_ref(|v| *v); + let mut stream2 = a.stream_ref(|v| *v); assert_eq!(stream1.next().await, Some(1)); a.write_mut(|v| *v = 2); @@ -518,8 +436,8 @@ mod tests { let a = Box::new(a) as Box>; - let mut stream1 = a.stream_owned(); - let mut stream2 = a.stream_owned(); + let mut stream1 = a.stream(); + let mut stream2 = a.stream(); assert_eq!(stream1.next().await, Some(1)); a.send(2); diff --git a/violet-core/src/widget/future.rs b/violet-core/src/widget/future.rs index 9e29971..afb72e1 100644 --- a/violet-core/src/widget/future.rs +++ b/violet-core/src/widget/future.rs @@ -1,7 +1,7 @@ -use futures::Stream; +use futures::{Future, Stream}; use futures_signals::signal::{self, SignalExt}; -use crate::{components::layout, layout::Layout, Scope, StreamEffect, Widget}; +use crate::{components::layout, layout::Layout, FutureEffect, Scope, StreamEffect, Widget}; pub struct SignalWidget(pub S); @@ -24,22 +24,23 @@ where let mut child = None; let stream = self.0.to_stream(); - scope - .set(layout(), Layout::Stack(Default::default())) - .spawn_effect(StreamEffect::new( - stream, - move |scope: &mut Scope<'_>, v| { - if let Some(child) = child { - scope.detach(child); - } + scope.spawn_effect(StreamEffect::new( + stream, + move |scope: &mut Scope<'_>, v| { + if let Some(child) = child { + scope.detach(child); + } - child = Some(scope.attach(v)); - }, - )); + child = Some(scope.attach(v)); + }, + )); } } -pub struct StreamWidget(pub S); +pub struct StreamWidget(pub S) +where + S: Stream, + S::Item: Widget; impl Widget for StreamWidget where @@ -49,17 +50,35 @@ where fn mount(self, scope: &mut crate::Scope<'_>) { let mut child = None; - scope - .set(layout(), Layout::Stack(Default::default())) - .spawn_effect(StreamEffect::new( - self.0, - move |scope: &mut Scope<'_>, v| { - if let Some(child) = child { - scope.detach(child); - } + scope.spawn_effect(StreamEffect::new( + self.0, + move |scope: &mut Scope<'_>, v| { + if let Some(child) = child { + scope.detach(child); + } + + child = Some(scope.attach(v)); + }, + )); + } +} + +pub struct FutureWidget(pub S) +where + S: Future, + S::Output: Widget; - child = Some(scope.attach(v)); - }, - )); +impl Widget for FutureWidget +where + S: 'static + Future, + W: Widget, +{ + fn mount(self, scope: &mut crate::Scope<'_>) { + scope.spawn_effect(FutureEffect::new( + self.0, + move |scope: &mut Scope<'_>, v| { + scope.attach(v); + }, + )); } } diff --git a/violet-core/src/widget/interactive/input.rs b/violet-core/src/widget/interactive/input.rs index 20ce426..72d4689 100644 --- a/violet-core/src/widget/interactive/input.rs +++ b/violet-core/src/widget/interactive/input.rs @@ -5,7 +5,7 @@ use flax::Component; use futures::{FutureExt, StreamExt}; use futures_signals::signal::{self, Mutable, SignalExt}; use glam::{vec2, Vec2}; -use itertools::Itertools; +use itertools::{Itertools, Position}; use palette::Srgba; use winit::{ event::ElementState, @@ -15,11 +15,11 @@ use winit::{ use crate::{ components::{self, screen_rect}, editor::{CursorMove, EditAction, EditorAction, TextEditor}, - input::{focus_sticky, focusable, on_keyboard_input, on_mouse_input, KeyboardInput}, - project::{ProjectDuplex, ProjectOwned, ProjectSink, ProjectStreamOwned}, + input::{focus_sticky, focusable, on_focus, on_keyboard_input, on_mouse_input, KeyboardInput}, + project::{StateDuplex, StateOwned, StateSink, StateStream}, style::{ - colors::EERIE_BLACK_300, get_stylesheet, interactive_active, spacing, Background, SizeExt, - StyleExt, WidgetSize, + colors::EERIE_BLACK_300, get_stylesheet, interactive_active, interactive_inactive, spacing, + Background, SizeExt, StyleExt, WidgetSize, }, text::{LayoutGlyphs, TextSegment}, to_owned, @@ -46,14 +46,15 @@ impl Default for TextInputStyle { } } +/// Text field allowing arbitrary user input pub struct TextInput { style: TextInputStyle, - content: Arc>, + content: Arc>, size: WidgetSize, } impl TextInput { - pub fn new(content: impl 'static + Send + Sync + ProjectDuplex) -> Self { + pub fn new(content: impl 'static + Send + Sync + StateDuplex) -> Self { Self { content: Arc::new(content), style: Default::default(), @@ -89,6 +90,10 @@ impl Widget for TextInput { let content = self.content.clone(); + let focused = Mutable::new(false); + + // Internal text to keep track of non-bijective text changes, such as incomplete numeric + // input let mut text_content = Mutable::new(String::new()); let mut editor = TextEditor::new(); @@ -103,33 +108,37 @@ impl Widget for TextInput { scope.spawn({ let mut layout_glyphs = layout_glyphs.signal_cloned().to_stream(); + let mut focused_signal = focused.stream().fuse(); + to_owned![text_content]; async move { - let mut rx = rx.into_stream(); + let mut rx = rx.into_stream().fuse(); let mut glyphs: LayoutGlyphs; let mut cursor_pos = Vec2::ZERO; - let mut new_text = content.project_stream_owned(); + let mut new_text = content.stream().fuse(); + let mut focused = false; loop { futures::select! { - new_text = new_text.next().fuse() => { - if let Some(new_text) = new_text { - editor.set_text(new_text.split('\n')); - } + focus = focused_signal.select_next_some() => { + focused = focus; } - action = rx.next().fuse() => { - if let Some(action) = action { - - editor.apply_action(action); + new_text = new_text.select_next_some() => { + editor.set_text(new_text.split('\n')); + text_content.send(new_text); + } + action = rx.select_next_some() => { + editor.apply_action(action); - let mut text = text_content.lock_mut(); - #[allow(unstable_name_collisions)] - text.extend(editor.lines().iter().map(|v| v.text()).intersperse("\n")); + let mut text = text_content.lock_mut(); + text.clear(); + #[allow(unstable_name_collisions)] + text.extend(editor.lines().iter().map(|v| v.text()).intersperse("\n")); - content.project_send(editor.lines().iter().map(|v| v.text()).join("\n")); - } + 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 { @@ -152,12 +161,12 @@ impl Widget for TextInput { } } - editor_props_tx + editor_props_tx .send(Box::new(Stack::new( ( - Positioned::new(Rectangle::new(cursor_color) + focused.then(|| Positioned::new(Rectangle::new(cursor_color) .with_min_size(Unit::px2(2.0, 18.0))) - .with_offset(Unit::px(cursor_pos)), + .with_offset(Unit::px(cursor_pos))), ) ))) .ok(); @@ -168,6 +177,9 @@ impl Widget for TextInput { scope .set(focusable(), ()) .set(focus_sticky(), ()) + .on_event(on_focus(), move |_, _, focus| { + focused.set(focus); + }) .on_event(on_mouse_input(), { to_owned![layout_glyphs, text_bounds, tx]; move |_, _, input| { @@ -199,7 +211,7 @@ impl Widget for TextInput { }); Stack::new(( - StreamWidget(self.content.clone().project_stream_owned().map(move |v| { + StreamWidget(text_content.stream().map(move |v| { to_owned![text_bounds]; Text::rich([TextSegment::new(v)]) .with_font_size(self.style.font_size) @@ -247,13 +259,13 @@ fn handle_input(input: KeyboardInput) -> Option { pub struct InputField { label: String, - value: Arc>, + value: Arc>, } impl InputField { pub fn new( label: impl Into, - value: impl 'static + Send + Sync + ProjectDuplex, + value: impl 'static + Send + Sync + StateDuplex, ) -> Self { Self { label: label.into(), @@ -278,23 +290,18 @@ impl Widget for InputField { }) .for_each(move |v| { tracing::info!("Parsed: {}", v); - value.project_send(v); + value.send(v); async {} }), ); - scope.spawn( - self.value - .project_stream_owned() - .map(|v| v.to_string()) - .for_each({ - to_owned![text_value]; - move |v| { - text_value.set(v); - async {} - } - }), - ); + scope.spawn(self.value.stream().map(|v| v.to_string()).for_each({ + to_owned![text_value]; + move |v| { + text_value.set(v); + async {} + } + })); let editor = TextInput::new(text_value); diff --git a/violet-core/src/widget/interactive/slider.rs b/violet-core/src/widget/interactive/slider.rs index 79fb207..e606e59 100644 --- a/violet-core/src/widget/interactive/slider.rs +++ b/violet-core/src/widget/interactive/slider.rs @@ -5,24 +5,24 @@ use flax::{Component, Entity, EntityRef}; use futures::{stream::BoxStream, StreamExt}; use futures_signals::signal::Mutable; use glam::{IVec2, Vec2}; -use palette::Srgba; +use palette::{num::Recip, Srgba}; use winit::event::ElementState; use crate::{ components::{offset, rect}, - editor::TextEditor, input::{focusable, on_cursor_move, on_mouse_input, CursorMove}, layout::Alignment, - project::{MappedDuplex, ProjectDuplex, ProjectStreamOwned}, + project::{Dedup, FilterDuplex, Map, StateDuplex, StateStream}, style::{get_stylesheet, interactive_active, interactive_inactive, spacing, StyleExt}, text::TextSegment, + to_owned, unit::Unit, utils::zip_latest_clone, widget::{row, BoxSized, ContainerStyle, Positioned, Rectangle, Stack, StreamWidget, Text}, Edges, Scope, StreamEffect, Widget, }; -use super::input::{InputField, TextInput}; +use super::input::TextInput; #[derive(Debug, Clone, Copy)] pub struct SliderStyle { @@ -45,14 +45,14 @@ impl Default for SliderStyle { pub struct Slider { style: SliderStyle, - value: Arc>, + value: Arc>, min: V, max: V, - label: bool, + transform: Option V>>, } impl Slider { - pub fn new(value: impl 'static + Send + Sync + ProjectDuplex, min: V, max: V) -> Self + pub fn new(value: impl 'static + Send + Sync + StateDuplex, min: V, max: V) -> Self where V: Copy, { @@ -61,21 +61,21 @@ impl Slider { min, max, style: Default::default(), - label: false, + transform: None, } } - /// Set the label visibility - pub fn with_label(mut self, label: bool) -> Self { - self.label = label; - self - } - /// Set the style pub fn with_style(mut self, style: SliderStyle) -> Self { self.style = style; self } + + /// Set the transform + pub fn with_transform(mut self, transform: impl 'static + Send + Sync + Fn(V) -> V) -> Self { + self.transform = Some(Box::new(transform)); + self + } } impl Widget for Slider { @@ -104,15 +104,15 @@ impl Widget for Slider { input: CursorMove, min: f32, max: f32, - dst: &dyn ProjectDuplex, + dst: &dyn StateDuplex, ) { let rect = entity.get_copy(rect()).unwrap(); let value = (input.local_pos.x / rect.size().x).clamp(0.0, 1.0) * (max - min) + min; - dst.project_send(V::from_progress(value)); + dst.send(V::from_progress(value)); } let handle = SliderHandle { - value: self.value.project_stream_owned(), + value: self.value.stream(), min, max, rect_id: track, @@ -120,10 +120,22 @@ impl Widget for Slider { handle_size, }; + let value = Arc::new(Map::new( + self.value, + |v| v, + move |v| { + if let Some(transform) = &self.transform { + transform(v) + } else { + v + } + }, + )); + scope .set(focusable(), ()) .on_event(on_mouse_input(), { - let value = self.value.clone(); + to_owned![value]; move |_, entity, input| { if input.state == ElementState::Pressed { update(entity, input.cursor, min, max, &*value); @@ -131,7 +143,7 @@ impl Widget for Slider { } }) .on_event(on_cursor_move(), { - let value = self.value.clone(); + to_owned![value]; move |_, entity, input| update(entity, input, min, max, &*value) }); @@ -142,17 +154,7 @@ impl Widget for Slider { ..Default::default() }); - if self.label { - row(( - slider, - StreamWidget(self.value.project_stream_owned().map(|v| { - Text::rich([TextSegment::new(format!("{:>4.2}", v))]).with_wrap(Wrap::None) - })), - )) - .mount(scope) - } else { - slider.mount(scope) - } + slider.mount(scope) } } @@ -169,7 +171,7 @@ impl Widget for SliderHandle { fn mount(self, scope: &mut Scope<'_>) { let rect_size = Mutable::new(None); - let update = zip_latest_clone(self.value, rect_size.project_stream_owned()); + let update = zip_latest_clone(self.value, rect_size.stream()); scope.frame_mut().monitor(self.rect_id, rect(), move |v| { rect_size.set(v.map(|v| v.size())); @@ -235,16 +237,46 @@ num_impl!(usize); /// A slider with label displaying the value pub struct SliderWithLabel { + text_value: Box>, slider: Slider, + editable: bool, } -impl SliderWithLabel { - pub fn new(value: impl 'static + Send + Sync + ProjectDuplex, min: V, max: V) -> Self +impl SliderWithLabel { + pub fn new(value: impl 'static + Send + Sync + StateDuplex, min: V, max: V) -> Self where V: Copy, { + // Wrap in dedup to prevent updating equal numeric values like `0` and `0.` etc when typing + let value = Arc::new(value); + + let text_value = Box::new(FilterDuplex::new( + Dedup::new(value.clone()), + |v| Some(format!("{v}")), + move |v: String| { + let v = v.parse::().ok()?; + let v = if v < min { + min + } else if v > max { + max + } else { + v + }; + + Some(v) + }, + )); + Self { - slider: Slider::new(value, min, max), + text_value, + slider: Slider { + style: Default::default(), + value, + min, + max, + transform: None, + }, + editable: false, } } @@ -253,68 +285,36 @@ impl SliderWithLabel { self.slider = self.slider.with_style(style); self } -} -impl Widget for SliderWithLabel { - fn mount(self, scope: &mut Scope<'_>) { - let label = - StreamWidget(self.slider.value.project_stream_owned().map(|v| { - Text::rich([TextSegment::new(format!("{:>4.2}", v))]).with_wrap(Wrap::None) - })); - - crate::widget::List::new((self.slider, label)).mount(scope) + pub fn with_transform(mut self, transform: impl 'static + Send + Sync + Fn(V) -> V) -> Self { + self.slider.transform = Some(Box::new(transform)); + self } -} - -/// A slider with label displaying the value -pub struct SliderWithInput { - text_value: TextInput, - slider: Slider, -} -impl SliderWithInput { - pub fn new(value: impl 'static + Send + Sync + ProjectDuplex, min: V, max: V) -> Self - where - V: Copy, - { - todo!() - // let value = Arc::new(value); - // let text_value = TextInput::new(MappedDuplex::new( - // value.clone(), - // move |v| format!("{v}"), - // move |v| { - // let v = v.parse::().unwrap_or_default(); - // if v < min { - // min - // } else if v > max { - // max - // } else { - // v - // } - // }, - // )); - - // Self { - // text_value, - // slider: Slider { - // style: Default::default(), - // value, - // min, - // max, - // label: false, - // }, - // } + pub fn editable(mut self, editable: bool) -> Self { + self.editable = editable; + self } +} - /// Set the style - pub fn with_style(mut self, style: SliderStyle) -> Self { - self.slider = self.slider.with_style(style); +impl SliderWithLabel { + pub fn round(mut self, round: f32) -> Self { + let recip = round.recip(); + self.slider.transform = Some(Box::new(move |v| (v * recip).round() / recip)); self } } -impl Widget for SliderWithInput { +impl Widget for SliderWithLabel { fn mount(self, scope: &mut Scope<'_>) { - row((self.slider, self.text_value)).mount(scope) + if self.editable { + row((self.slider, TextInput::new(self.text_value))).mount(scope) + } else { + row(( + self.slider, + StreamWidget(self.text_value.stream().map(Text::new)), + )) + .mount(scope) + } } } diff --git a/violet-web-example/src/lib.rs b/violet-web-example/src/lib.rs index 215ce11..0cb7ae5 100644 --- a/violet-web-example/src/lib.rs +++ b/violet-web-example/src/lib.rs @@ -1,4 +1,4 @@ -use glam::Vec2; +use glam::{Vec2, Vec3}; use tracing_subscriber::{ filter::LevelFilter, fmt::format::Pretty, layer::SubscriberExt, util::SubscriberInitExt, Layer, }; @@ -7,6 +7,7 @@ use violet::{ core::{ components, layout::{Alignment, Direction}, + project::MappedState, style::{ colors::{ EERIE_BLACK_400, EERIE_BLACK_DEFAULT, JADE_200, JADE_DEFAULT, LION_DEFAULT, @@ -16,13 +17,16 @@ use violet::{ }, text::Wrap, unit::Unit, - widget::{List, Rectangle, SignalWidget, SliderWithLabel, Stack, Text, WidgetExt}, + widget::{ + card, column, row, List, Rectangle, SignalWidget, SliderWithLabel, SliderWithLabel, + Stack, Text, WidgetExt, + }, Edges, Scope, Widget, WidgetCollection, }, flax::components::name, futures_signals::signal::{Mutable, SignalExt}, glam::vec2, - palette::Srgba, + palette::{IntoColor, Oklch, Srgba}, }; use wasm_bindgen::prelude::*; @@ -64,10 +68,10 @@ impl Widget for MainApp { column(( row(( Text::new("Lightness"), - SliderWithInput::new(lightness, 0.0, 1.0), + SliderWithLabel::new(lightness, 0.0, 1.0), )), - row((Text::new("Chroma"), SliderWithInput::new(chroma, 0.0, 0.37))), - row((Text::new("Hue"), SliderWithInput::new(hue, 0.0, 360.0))), + row((Text::new("Chroma"), SliderWithLabel::new(chroma, 0.0, 0.37))), + row((Text::new("Hue"), SliderWithLabel::new(hue, 0.0, 360.0))), SignalWidget(color.signal().map(|v| Text::new(format!("{}", v)))), card(SignalWidget(color_rect)), )) From d35afad25cf4ee01580e9184ec00c94a4d534d7d Mon Sep 17 00:00:00 2001 From: Freja Roberts Date: Sun, 10 Mar 2024 20:33:45 +0100 Subject: [PATCH 6/7] wip: demo --- Cargo.lock | 3 + Cargo.toml | 3 +- examples/color.rs | 6 +- examples/flow.rs | 6 +- examples/sizing.rs | 2 +- violet-core/src/input.rs | 31 ++- violet-core/src/lib.rs | 2 +- violet-core/src/{project => state}/bridge.rs | 0 violet-core/src/{project => state}/dedup.rs | 0 violet-core/src/{project => state}/duplex.rs | 0 violet-core/src/{project => state}/filter.rs | 0 violet-core/src/{project => state}/mod.rs | 0 violet-core/src/style/mod.rs | 46 ++-- violet-core/src/utils.rs | 4 +- violet-core/src/widget/container.rs | 13 +- violet-core/src/widget/interactive/button.rs | 5 +- violet-core/src/widget/interactive/input.rs | 2 +- violet-core/src/widget/interactive/slider.rs | 15 +- violet-core/src/widget/mod.rs | 5 + violet-web-example/Cargo.toml | 7 +- violet-web-example/src/lib.rs | 215 +++++++++++++++++-- violet-web-example/src/main.rs | 5 + 22 files changed, 293 insertions(+), 77 deletions(-) rename violet-core/src/{project => state}/bridge.rs (100%) rename violet-core/src/{project => state}/dedup.rs (100%) rename violet-core/src/{project => state}/duplex.rs (100%) rename violet-core/src/{project => state}/filter.rs (100%) rename violet-core/src/{project => state}/mod.rs (100%) create mode 100644 violet-web-example/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 781b185..8f99575 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2539,8 +2539,11 @@ name = "violet-web-example" version = "0.0.1" dependencies = [ "console_error_panic_hook", + "futures", "glam", + "itertools 0.12.1", "tracing-subscriber", + "tracing-tree", "tracing-web", "violet", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index 8add1ec..4c0a542 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,6 +70,7 @@ web-time = "1.0" wasm-bindgen-futures = "0.4" wasm-bindgen = "0.2" web-sys = "0.3" +tracing-tree = "0.3" [dependencies] violet-wgpu = { path = "violet-wgpu" } @@ -92,7 +93,7 @@ futures.workspace = true tokio = { version = "1.0", default_features= false, features = ["macros"] } serde_json = "1.0" -tracing-tree = "0.3" +tracing-tree.workspace = true tracing-subscriber = { version = "0.3", features = [ "env-filter", ] } diff --git a/examples/color.rs b/examples/color.rs index ab5ac4f..80a2aca 100644 --- a/examples/color.rs +++ b/examples/color.rs @@ -7,10 +7,10 @@ use tracing::info; use tracing_subscriber::{layer::SubscriberExt, registry, util::SubscriberInitExt, EnvFilter}; use tracing_tree::HierarchicalLayer; use violet_core::{ - project::{Map, MappedState, StateStream, StateStreamRef}, + state::{Map, MappedState, StateStream, StateStreamRef}, style::{colors::LION_500, SizeExt}, unit::Unit, - utils::zip_latest_clone, + utils::zip_latest, widget::{ card, column, row, Rectangle, SignalWidget, SliderWithLabel, Stack, StreamWidget, Text, }, @@ -93,7 +93,7 @@ impl Widget for MainApp { .round(1.0), )), StreamWidget( - zip_latest_clone(color_oklch.stream(), falloff.stream()) + zip_latest(color_oklch.stream(), falloff.stream()) .map(|(color, falloff)| Tints::new(color, falloff)), ), )) diff --git a/examples/flow.rs b/examples/flow.rs index 3315fd8..0df2683 100644 --- a/examples/flow.rs +++ b/examples/flow.rs @@ -90,15 +90,15 @@ impl Widget for MainApp { column(( Button::with_label("Button"), Button::with_label("Button").with_style(ButtonStyle { - normal_color: style::success_element(), + normal_color: style::success_item(), ..Default::default() }), Button::with_label("Warning").with_style(ButtonStyle { - normal_color: style::warning_element(), + normal_color: style::warning_item(), ..Default::default() }), Button::with_label("Error").with_style(ButtonStyle { - normal_color: style::error_element(), + normal_color: style::danger_item(), ..Default::default() }), )) diff --git a/examples/sizing.rs b/examples/sizing.rs index 8e6a94b..1aa90b3 100644 --- a/examples/sizing.rs +++ b/examples/sizing.rs @@ -20,7 +20,7 @@ use violet::core::{ Edges, Scope, Widget, }; use violet_core::{ - project::MappedState, + state::MappedState, style::{colors::DARK_CYAN_DEFAULT, SizeExt}, text::Wrap, widget::{card, centered, column, row, Slider}, diff --git a/violet-core/src/input.rs b/violet-core/src/input.rs index 64e69e2..8d2bd3d 100644 --- a/violet-core/src/input.rs +++ b/violet-core/src/input.rs @@ -2,7 +2,7 @@ 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, + EntityIds, EntityRef, Fetch, FetchExt, Mutable, Query, Topo, World, }; use glam::Vec2; @@ -123,14 +123,12 @@ impl InputState { pub fn on_cursor_move(&mut self, frame: &mut Frame, pos: Vec2) { self.pos = pos; - if let Some(cur) = &self.focused { - let entity = frame.world.entity(cur.id).unwrap(); - + 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()) { on_input( frame, - &entity, + entity, CursorMove { modifiers: self.modifiers, absolute_pos: pos, @@ -146,14 +144,11 @@ impl InputState { } pub fn on_keyboard_input(&mut self, frame: &mut Frame, event: KeyEvent) { - if let Some(cur) = &self.focused { - tracing::debug!(?cur, "sending keyboard input event"); - let entity = frame.world.entity(cur.id).unwrap(); - + if let Some(entity) = &self.focused(frame.world()) { if let Ok(mut on_input) = entity.get_mut(on_keyboard_input()) { on_input( frame, - &entity, + entity, KeyboardInput { modifiers: self.modifiers, event, @@ -163,18 +158,20 @@ impl InputState { } } + fn focused<'a>(&self, world: &'a World) -> Option> { + self.focused.as_ref().and_then(|v| world.entity(v.id).ok()) + } + fn set_focused(&mut self, frame: &Frame, focused: Option) { - let cur = self.focused.as_ref().map(|v| v.id); + let cur = self.focused(&frame.world); - if cur == focused { + if cur.map(|v| v.id()) == focused { return; } - if let Some(cur) = &self.focused { - let entity = frame.world().entity(cur.id).unwrap(); - - if let Ok(mut on_focus) = entity.get_mut(on_focus()) { - on_focus(frame, &entity, false); + if let Some(cur) = cur { + if let Ok(mut on_focus) = cur.get_mut(on_focus()) { + on_focus(frame, &cur, false); } } diff --git a/violet-core/src/lib.rs b/violet-core/src/lib.rs index c39f283..753ad89 100644 --- a/violet-core/src/lib.rs +++ b/violet-core/src/lib.rs @@ -9,10 +9,10 @@ pub mod executor; mod frame; pub mod input; pub mod layout; -pub mod project; mod scope; pub mod shape; mod sink; +pub mod state; pub mod stored; pub mod style; pub mod systems; diff --git a/violet-core/src/project/bridge.rs b/violet-core/src/state/bridge.rs similarity index 100% rename from violet-core/src/project/bridge.rs rename to violet-core/src/state/bridge.rs diff --git a/violet-core/src/project/dedup.rs b/violet-core/src/state/dedup.rs similarity index 100% rename from violet-core/src/project/dedup.rs rename to violet-core/src/state/dedup.rs diff --git a/violet-core/src/project/duplex.rs b/violet-core/src/state/duplex.rs similarity index 100% rename from violet-core/src/project/duplex.rs rename to violet-core/src/state/duplex.rs diff --git a/violet-core/src/project/filter.rs b/violet-core/src/state/filter.rs similarity index 100% rename from violet-core/src/project/filter.rs rename to violet-core/src/state/filter.rs diff --git a/violet-core/src/project/mod.rs b/violet-core/src/state/mod.rs similarity index 100% rename from violet-core/src/project/mod.rs rename to violet-core/src/state/mod.rs diff --git a/violet-core/src/style/mod.rs b/violet-core/src/style/mod.rs index 423868e..f4d7884 100644 --- a/violet-core/src/style/mod.rs +++ b/violet-core/src/style/mod.rs @@ -241,17 +241,17 @@ pub fn setup_stylesheet() -> EntityBuilder { let mut builder = Entity::builder(); builder - .set(primary_surface(), EERIE_BLACK_DEFAULT) - .set(primary_element(), PLATINUM_DEFAULT) - .set(secondary_surface(), EERIE_BLACK_600) - .set(accent_surface(), EERIE_BLACK_DEFAULT) - .set(accent_element(), JADE_DEFAULT) - .set(success_surface(), EERIE_BLACK_DEFAULT) - .set(success_element(), JADE_DEFAULT) - .set(warning_surface(), EERIE_BLACK_DEFAULT) - .set(warning_element(), LION_DEFAULT) - .set(error_surface(), EERIE_BLACK_DEFAULT) - .set(error_element(), REDWOOD_DEFAULT) + .set(primary_background(), EERIE_BLACK_DEFAULT) + .set(primary_item(), PLATINUM_DEFAULT) + .set(secondary_background(), EERIE_BLACK_600) + .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) @@ -268,24 +268,24 @@ pub fn setup_stylesheet() -> EntityBuilder { flax::component! { pub stylesheet(id): () => [ Exclusive ], /// The primary surface color - pub primary_surface: Srgba, - pub primary_element: Srgba, + pub primary_background: Srgba, + pub primary_item: Srgba, /// Used for secondary surfaces, such as card backgrounds - pub secondary_surface: Srgba, - pub secondary_element: Srgba, + pub secondary_background: Srgba, + pub secondary_item: Srgba, - pub accent_surface: Srgba, - pub accent_element: Srgba, + pub accent_background: Srgba, + pub accent_item: Srgba, - pub success_surface: Srgba, - pub success_element: Srgba, + pub success_background: Srgba, + pub success_item: Srgba, - pub warning_surface: Srgba, - pub warning_element: Srgba, + pub warning_background: Srgba, + pub warning_item: Srgba, - pub error_surface: Srgba, - pub error_element: Srgba, + pub danger_background: Srgba, + pub danger_item: Srgba, pub spacing: SpacingConfig, diff --git a/violet-core/src/utils.rs b/violet-core/src/utils.rs index eb9f5ec..04b487e 100644 --- a/violet-core/src/utils.rs +++ b/violet-core/src/utils.rs @@ -10,12 +10,12 @@ macro_rules! to_owned { } /// Combines two streams yielding the latest value from each stream -pub fn zip_latest(a: A, b: B, func: F) -> ZipLatest { +pub fn zip_latest_ref(a: A, b: B, func: F) -> ZipLatest { ZipLatest::new(a, b, func) } /// Combines two streams yielding the latest value from each stream -pub fn zip_latest_clone( +pub fn zip_latest( a: A, b: B, ) -> ZipLatest (A::Item, B::Item)> diff --git a/violet-core/src/widget/container.rs b/violet-core/src/widget/container.rs index f1045d0..1cef1a9 100644 --- a/violet-core/src/widget/container.rs +++ b/violet-core/src/widget/container.rs @@ -6,7 +6,10 @@ use crate::{ components::{anchor, layout, margin, max_size, min_size, offset, padding, rect}, input::{focusable, on_cursor_move, on_mouse_input}, layout::{Alignment, Direction, FlowLayout, Layout, StackLayout}, - style::{colors::EERIE_BLACK_400, Background, SizeExt, StyleExt, WidgetSize}, + style::{ + colors::{EERIE_BLACK_300, EERIE_BLACK_400}, + Background, SizeExt, StyleExt, WidgetSize, + }, unit::Unit, Edges, Frame, Scope, Widget, WidgetCollection, }; @@ -273,3 +276,11 @@ pub fn card(widget: W) -> Stack { .with_padding(Edges::even(4.0)) .with_margin(Edges::even(4.0)) } + +pub fn card2(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)) +} diff --git a/violet-core/src/widget/interactive/button.rs b/violet-core/src/widget/interactive/button.rs index d255f35..19e70e8 100644 --- a/violet-core/src/widget/interactive/button.rs +++ b/violet-core/src/widget/interactive/button.rs @@ -39,7 +39,10 @@ pub struct Button { } impl Button { - pub fn new(label: W) -> Self { + pub fn new(label: W) -> Self + where + W: Widget, + { Self { on_press: Box::new(|_, _| {}), label, diff --git a/violet-core/src/widget/interactive/input.rs b/violet-core/src/widget/interactive/input.rs index 72d4689..9b36c24 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}, - project::{StateDuplex, StateOwned, StateSink, StateStream}, + state::{StateDuplex, StateOwned, StateSink, StateStream}, style::{ colors::EERIE_BLACK_300, get_stylesheet, interactive_active, interactive_inactive, spacing, Background, SizeExt, StyleExt, WidgetSize, diff --git a/violet-core/src/widget/interactive/slider.rs b/violet-core/src/widget/interactive/slider.rs index e606e59..3997b5d 100644 --- a/violet-core/src/widget/interactive/slider.rs +++ b/violet-core/src/widget/interactive/slider.rs @@ -12,13 +12,15 @@ use crate::{ components::{offset, rect}, input::{focusable, on_cursor_move, on_mouse_input, CursorMove}, layout::Alignment, - project::{Dedup, FilterDuplex, Map, StateDuplex, StateStream}, + state::{Dedup, FilterDuplex, Map, StateDuplex, StateStream}, style::{get_stylesheet, interactive_active, interactive_inactive, spacing, StyleExt}, text::TextSegment, to_owned, unit::Unit, - utils::zip_latest_clone, - widget::{row, BoxSized, ContainerStyle, Positioned, Rectangle, Stack, StreamWidget, Text}, + utils::zip_latest, + widget::{ + row, BoxSized, ContainerStyle, Positioned, Rectangle, Stack, StreamWidget, Text, WidgetExt, + }, Edges, Scope, StreamEffect, Widget, }; @@ -171,7 +173,7 @@ impl Widget for SliderHandle { fn mount(self, scope: &mut Scope<'_>) { let rect_size = Mutable::new(None); - let update = zip_latest_clone(self.value, rect_size.stream()); + let update = zip_latest(self.value, rect_size.stream()); scope.frame_mut().monitor(self.rect_id, rect(), move |v| { rect_size.set(v.map(|v| v.size())); @@ -308,12 +310,15 @@ impl SliderWithLabel { impl Widget for SliderWithLabel { fn mount(self, scope: &mut Scope<'_>) { if self.editable { - row((self.slider, TextInput::new(self.text_value))).mount(scope) + row((self.slider, TextInput::new(self.text_value))) + .with_cross_align(Alignment::Center) + .mount(scope) } else { row(( self.slider, StreamWidget(self.text_value.stream().map(Text::new)), )) + .with_cross_align(Alignment::Center) .mount(scope) } } diff --git a/violet-core/src/widget/mod.rs b/violet-core/src/widget/mod.rs index 89b8cb2..7f64b90 100644 --- a/violet-core/src/widget/mod.rs +++ b/violet-core/src/widget/mod.rs @@ -179,3 +179,8 @@ tuple_impl! { 0 => A, 1 => B, 2 => C, 3 => D, 4 => E } tuple_impl! { 0 => A, 1 => B, 2 => C, 3 => D, 4 => E, 5 => F } tuple_impl! { 0 => A, 1 => B, 2 => C, 3 => D, 4 => E, 5 => F, 6 => G } tuple_impl! { 0 => A, 1 => B, 2 => C, 3 => D, 4 => E, 5 => F, 6 => G, 7 => H } +tuple_impl! { 0 => A, 1 => B, 2 => C, 3 => D, 4 => E, 5 => F, 6 => G, 7 => H, 8 => I } +tuple_impl! { 0 => A, 1 => B, 2 => C, 3 => D, 4 => E, 5 => F, 6 => G, 7 => H, 8 => I, 9 => J } +tuple_impl! { 0 => A, 1 => B, 2 => C, 3 => D, 4 => E, 5 => F, 6 => G, 7 => H, 8 => I, 9 => J, 10 => K } +tuple_impl! { 0 => A, 1 => B, 2 => C, 3 => D, 4 => E, 5 => F, 6 => G, 7 => H, 8 => I, 9 => J, 10 => K, 11 => L } +tuple_impl! { 0 => A, 1 => B, 2 => C, 3 => D, 4 => E, 5 => F, 6 => G, 7 => H, 8 => I, 9 => J, 10 => K, 11 => L, 12 => M } diff --git a/violet-web-example/Cargo.toml b/violet-web-example/Cargo.toml index 39ae215..7fa9759 100644 --- a/violet-web-example/Cargo.toml +++ b/violet-web-example/Cargo.toml @@ -11,17 +11,20 @@ repository.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] -crate-type = ["cdylib"] +crate-type = ["cdylib", "rlib"] [dependencies] violet = { path = ".." } wasm-bindgen = "0.2.91" console_error_panic_hook = "0.1.6" -tracing-subscriber = "0.3" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-web = "0.1" web-sys = { version = "0.3", features = ["Gpu", "CanvasRenderingContext2d", "GpuCanvasContext", "GpuRenderBundle"] } wgpu.workspace = true glam.workspace = true +futures.workspace = true wasm-bindgen-futures = "0.4" +itertools.workspace = true +tracing-tree.workspace = true diff --git a/violet-web-example/src/lib.rs b/violet-web-example/src/lib.rs index 0cb7ae5..9ee5c0b 100644 --- a/violet-web-example/src/lib.rs +++ b/violet-web-example/src/lib.rs @@ -1,4 +1,6 @@ +use futures::StreamExt; use glam::{Vec2, Vec3}; +use itertools::Itertools; use tracing_subscriber::{ filter::LevelFilter, fmt::format::Pretty, layer::SubscriberExt, util::SubscriberInitExt, Layer, }; @@ -7,31 +9,33 @@ use violet::{ core::{ components, layout::{Alignment, Direction}, - project::MappedState, + state::{Map, MappedState, StateStream, StateStreamRef}, style::{ colors::{ EERIE_BLACK_400, EERIE_BLACK_DEFAULT, JADE_200, JADE_DEFAULT, LION_DEFAULT, REDWOOD_DEFAULT, }, - Background, SizeExt, + danger_item, success_item, Background, SizeExt, StyleExt, }, text::Wrap, + to_owned, unit::Unit, + utils::zip_latest, widget::{ - card, column, row, List, Rectangle, SignalWidget, SliderWithLabel, SliderWithLabel, - Stack, Text, WidgetExt, + card, column, row, Button, ButtonStyle, List, Rectangle, SignalWidget, SliderWithLabel, + Stack, StreamWidget, Text, WidgetExt, }, Edges, Scope, Widget, WidgetCollection, }, flax::components::name, futures_signals::signal::{Mutable, SignalExt}, glam::vec2, - palette::{IntoColor, Oklch, Srgba}, + palette::{FromColor, IntoColor, Oklch, Srgb, Srgba}, }; use wasm_bindgen::prelude::*; -#[wasm_bindgen] -pub async fn run() { +#[cfg(target_arch = "wasm32")] +fn setup() { let fmt_layer = tracing_subscriber::fmt::layer() .with_ansi(false) .without_time() @@ -46,6 +50,25 @@ pub async fn run() { .init(); console_error_panic_hook::set_once(); +} + +#[cfg(not(target_arch = "wasm32"))] +fn setup() { + tracing_subscriber::registry() + .with( + tracing_tree::HierarchicalLayer::default() + .with_deferred_spans(true) + .with_span_retrace(true) + .with_indent_lines(true) + .with_indent_amount(4), + ) + .with(tracing_subscriber::EnvFilter::from_default_env()) + .init(); +} + +#[wasm_bindgen] +pub fn run() { + setup(); violet::wgpu::App::new().run(MainApp).unwrap(); } @@ -54,7 +77,12 @@ struct MainApp; impl Widget for MainApp { fn mount(self, scope: &mut Scope<'_>) { - let color = Mutable::new(Vec3::new(0.0, 0.0, 0.0)); + 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 = MappedState::new(color.clone(), |v| &v.x, |v| &mut v.x); let chroma = MappedState::new(color.clone(), |v| &v.y, |v| &mut v.y); @@ -65,17 +93,172 @@ impl Widget for MainApp { Rectangle::new(color).with_min_size(Unit::px2(200.0, 100.0)) }); + let falloff = Mutable::new(50.0); + + let history = Mutable::new(Vec::new()); + + let save_button = Button::new(Text::new("Save color")) + .with_style(ButtonStyle { + normal_color: success_item(), + ..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_stretch(true) + .with_margin(Edges::even(4.0)), + ) + .with_size(Unit::rel2(1.0, 1.0)) + .mount(scope); + } +} + +struct Tints { + base: Oklch, + falloff: f32, +} + +impl Tints { + fn new(base: Oklch, falloff: f32) -> Self { + Self { base, falloff } + } +} + +impl Widget for Tints { + fn mount(self, scope: &mut Scope<'_>) { + row((1..=9) + .map(|i| { + 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 = self.base.lighten(f); + let color = Oklch { + chroma, + l: f, + ..self.base + }; + + Stack::new(column(( + Rectangle::new(color.into_color()).with_min_size(Unit::px2(60.0, 60.0)), + Text::new(format!("{:.2}", f)), + ))) + .with_margin(Edges::even(4.0)) + }) + .collect_vec()) + .mount(scope) + } +} + +pub fn color_hex(color: impl IntoColor) -> String { + let hex: Srgb = color.into_color().into_format(); + format!("#{:0>2x}{:0>2x}{:0>2x}", hex.red, hex.green, hex.blue) +} + +pub struct HistoryView { + items: Mutable>, +} + +impl HistoryView { + pub fn new(items: Mutable>) -> Self { + Self { items } + } +} + +impl Widget for HistoryView { + fn mount(self, scope: &mut Scope<'_>) { + let items = self.items.clone(); + let discard = move |i| { + let items = items.clone(); + Button::new(Text::new("X")) + .on_press({ + move |_, _| { + items.lock_mut().remove(i); + } + }) + .with_style(ButtonStyle { + normal_color: danger_item(), + ..Default::default() + }) + }; + + StreamWidget(self.items.stream_ref(move |items| { + let items = items + .iter() + .enumerate() + .map(|(i, item)| card(row((discard(i), *item)))) + .collect_vec(); + + column(items) + })) + .mount(scope) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct HistoryItem { + color: Oklch, + falloff: f32, +} + +impl Widget for HistoryItem { + fn mount(self, scope: &mut Scope<'_>) { column(( + Text::new(color_hex(self.color)), row(( - Text::new("Lightness"), - SliderWithLabel::new(lightness, 0.0, 1.0), + Rectangle::new(self.color.into_color()).with_size(Unit::px2(100.0, 50.0)), + Tints::new(self.color, self.falloff), )), - row((Text::new("Chroma"), SliderWithLabel::new(chroma, 0.0, 0.37))), - row((Text::new("Hue"), SliderWithLabel::new(hue, 0.0, 360.0))), - SignalWidget(color.signal().map(|v| Text::new(format!("{}", v)))), - card(SignalWidget(color_rect)), )) - .with_margin(Edges::even(4.0)) - .mount(scope); + .mount(scope) } } diff --git a/violet-web-example/src/main.rs b/violet-web-example/src/main.rs new file mode 100644 index 0000000..9871cbe --- /dev/null +++ b/violet-web-example/src/main.rs @@ -0,0 +1,5 @@ +use violet_web_example::run; + +fn main() { + run(); +} From 36f1412e5994888b44cda6e74ba2d5278967df45 Mon Sep 17 00:00:00 2001 From: Freja Roberts Date: Mon, 11 Mar 2024 19:14:42 +0100 Subject: [PATCH 7/7] feat: semantic value reference system --- Cargo.lock | 7 +- Cargo.toml | 4 +- examples/basic.rs | 109 +++----- examples/color.rs | 18 +- examples/counter.rs | 19 +- examples/flow.rs | 41 ++- examples/sizing.rs | 6 +- recipes.json | 3 + violet-core/Cargo.toml | 1 + violet-core/src/input.rs | 2 - violet-core/src/scope.rs | 18 +- violet-core/src/state/dedup.rs | 25 +- violet-core/src/state/duplex.rs | 2 - violet-core/src/state/feedback.rs | 84 ++++++ violet-core/src/state/filter.rs | 10 +- violet-core/src/state/map.rs | 69 +++++ violet-core/src/state/mod.rs | 147 +++++------ violet-core/src/style/mod.rs | 243 ++++++++++-------- violet-core/src/types.rs | 6 + violet-core/src/widget/basic.rs | 32 ++- violet-core/src/widget/container.rs | 26 -- violet-core/src/widget/interactive/button.rs | 65 +++-- violet-core/src/widget/interactive/input.rs | 38 ++- violet-core/src/widget/interactive/slider.rs | 78 +++--- violet-core/src/widget/mod.rs | 2 +- .../Cargo.toml | 2 +- .../index.html | 0 .../src/lib.rs | 33 ++- violet-demo/src/main.rs | 5 + violet-web-example/src/main.rs | 5 - 30 files changed, 603 insertions(+), 497 deletions(-) delete mode 100644 violet-core/src/state/duplex.rs create mode 100644 violet-core/src/state/feedback.rs create mode 100644 violet-core/src/state/map.rs rename {violet-web-example => violet-demo}/Cargo.toml (96%) rename {violet-web-example => violet-demo}/index.html (100%) rename {violet-web-example => violet-demo}/src/lib.rs (87%) create mode 100644 violet-demo/src/main.rs delete mode 100644 violet-web-example/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 8f99575..faa5d47 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#aa6cf3489d4ffe3e66ec8fe4937c47f7f6df0e1b" +source = "git+https://github.com/ten3roberts/flax#39c01757b9ea5f142faeaf9d8b1473e1745ffcca" dependencies = [ "anyhow", "atomic_refcell", @@ -648,7 +648,7 @@ dependencies = [ [[package]] name = "flax-derive" version = "0.6.0" -source = "git+https://github.com/ten3roberts/flax#aa6cf3489d4ffe3e66ec8fe4937c47f7f6df0e1b" +source = "git+https://github.com/ten3roberts/flax#39c01757b9ea5f142faeaf9d8b1473e1745ffcca" dependencies = [ "itertools 0.11.0", "proc-macro-crate", @@ -2501,6 +2501,7 @@ name = "violet-core" version = "0.0.1" dependencies = [ "anyhow", + "arrayvec", "atomic_refcell", "bytemuck", "bytes", @@ -2535,7 +2536,7 @@ dependencies = [ ] [[package]] -name = "violet-web-example" +name = "violet-demo" version = "0.0.1" dependencies = [ "console_error_panic_hook", diff --git a/Cargo.toml b/Cargo.toml index 4c0a542..305e3b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = [ "violet-wgpu", "violet-core", "violet-web-example" ] +members = [ "violet-wgpu", "violet-core", "violet-demo" ] [workspace.package] version = "0.0.1" @@ -42,7 +42,7 @@ once_cell = "1.18" slab = "0.4" tynm ="0.1" tokio = { version = "1.0", default-features = false, features = ["macros", "rt"] } -either = "1.10" +arrayvec = "0.7" bytemuck = { version = "1.13", features = ["derive"] } winit = "0.29" diff --git a/examples/basic.rs b/examples/basic.rs index 3b24a56..f7d3ba1 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -2,7 +2,7 @@ use flax::{components::name, FetchExt, Query}; use futures_signals::signal::Mutable; use glam::{vec2, Vec2}; use itertools::Itertools; -use palette::{Hsva, IntoColor, Srgba}; +use palette::{Hsva, IntoColor}; use std::time::Duration; use tracing_subscriber::{ prelude::__tracing_subscriber_SubscriberExt, registry, util::SubscriberInitExt, EnvFilter, @@ -19,35 +19,16 @@ use violet::core::{ Scope, StreamEffect, Widget, }; use violet_core::{ - style::Background, + style::{ + colors::{DARK_CYAN_DEFAULT, JADE_DEFAULT, LION_DEFAULT}, + danger_item, primary_background, secondary_background, spacing_medium, spacing_small, + Background, SizeExt, ValueOrRef, + }, widget::{BoxSized, ContainerStyle}, - Edges, }; struct MainApp; -macro_rules! srgba { - ($color:literal) => {{ - let [r, g, b] = color_hex::color_from_hex!($color); - - Srgba::new(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0) - }}; -} - -const MARGIN: Edges = Edges::even(10.0); -const MARGIN_SM: Edges = Edges::even(5.0); - -pub const EERIE_BLACK: Srgba = srgba!("#222525"); -pub const EERIE_BLACK_300: Srgba = srgba!("#151616"); -pub const EERIE_BLACK_400: Srgba = srgba!("#1b1e1e"); -pub const EERIE_BLACK_600: Srgba = srgba!("#4c5353"); -pub const PLATINUM: Srgba = srgba!("#dddddf"); -pub const VIOLET: Srgba = srgba!("#8000ff"); -pub const TEAL: Srgba = srgba!("#247b7b"); -pub const EMERALD: Srgba = srgba!("#50c878"); -pub const BRONZE: Srgba = srgba!("#cd7f32"); -pub const CHILI_RED: Srgba = srgba!("#d34131"); - impl Widget for MainApp { fn mount(self, scope: &mut Scope) { scope @@ -73,16 +54,13 @@ impl Widget for MainApp { .with_min_size(Unit::px(size)) .with_aspect_ratio(1.0), ) - .with_style(ContainerStyle { - margin: MARGIN, - ..Default::default() - }) + .with_margin(spacing_medium()) }) .collect_vec(), ) .with_name("Images"), Stack::new((Text::rich([ - TextSegment::new("Violet").with_color(VIOLET), + TextSegment::new("Violet"), TextSegment::new(" now has support for "), TextSegment::new("rich ").with_style(Style::Italic), TextSegment::new("text. I wanted to "), @@ -95,22 +73,20 @@ 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(TEAL), + .with_color(DARK_CYAN_DEFAULT), 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(BRONZE) + .with_color(LION_DEFAULT) .with_style(Style::Italic), TextSegment::new("); \n}") .with_family(FontFamily::named("JetBrainsMono Nerd Font")), ]) .with_font_size(18.0),)) - .with_style(ContainerStyle { - background: Some(Background::new(EERIE_BLACK)), - padding: MARGIN, - margin: MARGIN, - }), + .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 🐕") @@ -120,30 +96,25 @@ impl Widget for MainApp { // .with_family("Inter") .with_font_size(18.0), ) - .with_style(ContainerStyle { - background: Some(Background::new(EERIE_BLACK)), - padding: MARGIN, - margin: MARGIN, - }), + .with_margin(spacing_small()) + .with_padding(spacing_small()) + .with_background(Background::new(primary_background())), Stack::new(( - BoxSized::new(Rectangle::new(CHILI_RED)) + BoxSized::new(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(TEAL)) + BoxSized::new(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(EERIE_BLACK_300)) - .with_padding(MARGIN) - .with_margin(MARGIN), + .with_background(Background::new(secondary_background())) + .with_padding(spacing_small()) + .with_margin(spacing_small()), )) - .with_style(ContainerStyle { - background: Some(Background::new(EERIE_BLACK_600)), - ..Default::default() - }) + .with_background(Background::new(secondary_background())) .contain_margins(true) .with_direction(Direction::Vertical) .mount(scope); @@ -182,12 +153,12 @@ struct StackTest {} impl Widget for StackTest { fn mount(self, scope: &mut Scope<'_>) { - Stack::new((Text::new("This is an overlaid text").with_color(EMERALD),)) + Stack::new((Text::new("This is an overlaid text").with_color(JADE_DEFAULT),)) .with_style(ContainerStyle { - background: Some(Background::new(EERIE_BLACK_300)), - padding: MARGIN, - margin: MARGIN, + background: Some(Background::new(secondary_background())), }) + .with_margin(spacing_small()) + .with_padding(spacing_small()) .mount(scope) } } @@ -202,16 +173,13 @@ impl Widget for LayoutFlexTest { let size = vec2(100.0, 20.0); Stack::new( - BoxSized::new(Rectangle::new( + BoxSized::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_size(Unit::px(size * vec2(i as f32, 1.0))), ) - .with_style(ContainerStyle { - margin: MARGIN, - ..Default::default() - }) + .with_margin(spacing_small()) }) .collect_vec(), ) @@ -234,15 +202,14 @@ impl Widget for LayoutTest { TextSegment::new("This is "), TextSegment::new("sparta") .with_style(Style::Italic) - .with_color(BRONZE), + .with_color(LION_DEFAULT), ]) .with_font_size(16.0) .with_wrap(Wrap::None), ) + .with_margin(spacing_small()) .with_style(ContainerStyle { - background: Some(Background::new(EERIE_BLACK)), - padding: MARGIN_SM, - margin: MARGIN_SM, + background: Some(Background::new(primary_background())), }), )) .on_press({ @@ -271,19 +238,19 @@ impl Widget for LayoutTest { )) .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(EERIE_BLACK)), - padding: MARGIN, - margin: MARGIN, + 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(EERIE_BLACK_300)), - padding: MARGIN, - margin: MARGIN, + background: Some(Background::new(secondary_background())), }) .mount(scope); } diff --git a/examples/color.rs b/examples/color.rs index 80a2aca..79eaf18 100644 --- a/examples/color.rs +++ b/examples/color.rs @@ -2,13 +2,12 @@ use futures::StreamExt; use futures_signals::signal::{Mutable, SignalExt}; use glam::Vec3; use itertools::Itertools; -use palette::{num::Powi, FromColor, IntoColor, Lighten, Oklch, Srgb}; -use tracing::info; +use palette::{FromColor, IntoColor, Oklch, Srgb}; use tracing_subscriber::{layer::SubscriberExt, registry, util::SubscriberInitExt, EnvFilter}; use tracing_tree::HierarchicalLayer; use violet_core::{ - state::{Map, MappedState, StateStream, StateStreamRef}, - style::{colors::LION_500, SizeExt}, + state::{Map, MapRef, StateStream, StateStreamRef}, + style::{SizeExt, ValueOrRef}, unit::Unit, utils::zip_latest, widget::{ @@ -46,13 +45,13 @@ impl Widget for MainApp { |v| Vec3::new(v.l, v.chroma, v.hue.into_positive_degrees()), ); - let lightness = MappedState::new(color.clone(), |v| &v.x, |v| &mut v.x); - let chroma = MappedState::new(color.clone(), |v| &v.y, |v| &mut v.y); - let hue = MappedState::new(color.clone(), |v| &v.z, |v| &mut v.z); + let lightness = MapRef::new(color.clone(), |v| &v.x, |v| &mut v.x); + let chroma = MapRef::new(color.clone(), |v| &v.y, |v| &mut v.y); + let hue = MapRef::new(color.clone(), |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(color).with_min_size(Unit::px2(200.0, 100.0)) + Rectangle::new(ValueOrRef::value(color)).with_min_size(Unit::px2(200.0, 100.0)) }); let falloff = Mutable::new(50.0); @@ -131,7 +130,8 @@ impl Widget for Tints { }; Stack::new(column(( - Rectangle::new(color.into_color()).with_min_size(Unit::px2(60.0, 60.0)), + Rectangle::new(ValueOrRef::value(color.into_color())) + .with_min_size(Unit::px2(60.0, 60.0)), Text::new(format!("{:.2}", f)), ))) .with_margin(Edges::even(4.0)) diff --git a/examples/counter.rs b/examples/counter.rs index d8fcafd..dbd8192 100644 --- a/examples/counter.rs +++ b/examples/counter.rs @@ -7,12 +7,11 @@ use tracing_tree::HierarchicalLayer; use violet::core::{ components::size, layout::Alignment, - style::StyleExt, unit::Unit, widget::{Button, List, SignalWidget, Stack, Text}, Scope, Widget, }; -use violet_core::{style::Background, widget::ContainerStyle, Edges}; +use violet_core::style::{accent_item, secondary_background, spacing_small, Background, SizeExt}; macro_rules! srgba { ($color:literal) => {{ @@ -22,8 +21,6 @@ macro_rules! srgba { }}; } -const MARGIN_SM: Edges = Edges::even(5.0); - pub const EERIE_BLACK: Srgba = srgba!("#222525"); pub const EERIE_BLACK_300: Srgba = srgba!("#151616"); pub const EERIE_BLACK_400: Srgba = srgba!("#1b1e1e"); @@ -36,11 +33,10 @@ pub const BRONZE: Srgba = srgba!("#cd7f32"); pub const CHILI_RED: Srgba = srgba!("#d34131"); fn pill(widget: impl Widget) -> impl Widget { - Stack::new(widget).with_style(ContainerStyle { - background: Some(Background::new(EERIE_BLACK_300)), - padding: MARGIN_SM, - margin: MARGIN_SM, - }) + Stack::new(widget) + .with_background(Background::new(secondary_background())) + .with_margin(spacing_small()) + .with_padding(spacing_small()) } struct MainApp; @@ -64,10 +60,7 @@ impl Widget for MainApp { "Please click the button to increment the counter", )), )) - .with_style(ContainerStyle { - background: Some(Background::new(EMERALD)), - ..Default::default() - }) + .with_background(Background::new(accent_item())) .with_cross_align(Alignment::Center) .mount(scope); } diff --git a/examples/flow.rs b/examples/flow.rs index 0df2683..d98b8e0 100644 --- a/examples/flow.rs +++ b/examples/flow.rs @@ -17,32 +17,26 @@ use violet::core::{ use violet_core::{ style::{ self, - colors::{EERIE_BLACK_300, EERIE_BLACK_600, EERIE_BLACK_DEFAULT}, - Background, SizeExt, + colors::{EERIE_BLACK_600, EERIE_BLACK_DEFAULT}, + secondary_background, spacing_small, Background, SizeExt, }, text::Wrap, widget::{ card, column, row, BoxSized, Button, ButtonStyle, ContainerStyle, SliderWithLabel, TextInput, }, - Edges, }; use violet_wgpu::renderer::RendererConfig; -const MARGIN: Edges = Edges::even(8.0); -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_padding(spacing_small()) + .with_margin(spacing_small()) } fn pill(widget: impl Widget) -> impl Widget { Stack::new(widget).with_style(ContainerStyle { - background: Some(Background::new(EERIE_BLACK_300)), - padding: MARGIN, - margin: MARGIN, + background: Some(Background::new(secondary_background())), }) } @@ -81,24 +75,20 @@ impl Widget for MainApp { }}); column(( - row((Text::new("Input: "), TextInput::new(content))).with_style(ContainerStyle { - margin: MARGIN_SM, - padding: MARGIN_SM, - ..Default::default() - }), + row((Text::new("Input: "), TextInput::new(content))), card( column(( Button::with_label("Button"), Button::with_label("Button").with_style(ButtonStyle { - normal_color: style::success_item(), + normal_color: style::success_item().into(), ..Default::default() }), Button::with_label("Warning").with_style(ButtonStyle { - normal_color: style::warning_item(), + normal_color: style::warning_item().into(), ..Default::default() }), Button::with_label("Error").with_style(ButtonStyle { - normal_color: style::danger_item(), + normal_color: style::danger_item().into(), ..Default::default() }), )) @@ -190,11 +180,14 @@ impl Widget for ItemList { )) .with_wrap(Wrap::None), ) - .with_background(Background::new( - Hsva::new(i as f32 * 30.0, 0.6, 0.7, 1.0).into_color(), - )) - .with_padding(MARGIN_SM) - .with_margin(MARGIN_SM) + .with_background(Background::new(Srgba::from_color(Hsva::new( + i as f32 * 30.0, + 0.6, + 0.7, + 1.0, + )))) + .with_padding(spacing_small()) + .with_margin(spacing_small()) // .with_cross_align(Alignment::Center) .with_vertical_alignment(Alignment::Center) .with_horizontal_alignment(Alignment::Center) diff --git a/examples/sizing.rs b/examples/sizing.rs index 1aa90b3..5e95038 100644 --- a/examples/sizing.rs +++ b/examples/sizing.rs @@ -20,7 +20,7 @@ use violet::core::{ Edges, Scope, Widget, }; use violet_core::{ - state::MappedState, + state::MapRef, style::{colors::DARK_CYAN_DEFAULT, SizeExt}, text::Wrap, widget::{card, centered, column, row, Slider}, @@ -73,8 +73,8 @@ impl Widget for Vec2Editor { fn mount(self, scope: &mut Scope<'_>) { let value = self.value; - let x = MappedState::new(value.clone(), |v| &v.x, |v| &mut v.x); - let y = MappedState::new(value.clone(), |v| &v.y, |v| &mut v.y); + 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(( row((label(self.x_label), Slider::new(x, 0.0, 200.0))), diff --git a/recipes.json b/recipes.json index 7450cb6..e4397fb 100644 --- a/recipes.json +++ b/recipes.json @@ -2,6 +2,9 @@ "check": { "cmd": "cargo check --all-targets --all-features" }, + "run demo": { + "cmd": "cargo run --package violet-demo" + }, "run basic": { "cmd": "cargo run --package violet --example basic" }, diff --git a/violet-core/Cargo.toml b/violet-core/Cargo.toml index 9354172..04f14bc 100644 --- a/violet-core/Cargo.toml +++ b/violet-core/Cargo.toml @@ -26,6 +26,7 @@ anyhow.workspace = true once_cell.workspace = true slab.workspace = true tynm.workspace = true +arrayvec.workspace = true bytemuck.workspace = true palette.workspace = true diff --git a/violet-core/src/input.rs b/violet-core/src/input.rs index 8d2bd3d..c205731 100644 --- a/violet-core/src/input.rs +++ b/violet-core/src/input.rs @@ -87,7 +87,6 @@ impl InputState { // Released after focusing a widget (ElementState::Released, Some(cur), _) => { if !cur.sticky { - tracing::info!(?cur, "focus lost on release"); self.set_focused(frame, None); } } @@ -99,7 +98,6 @@ impl InputState { if let Some((id, origin)) = intersect { let entity = frame.world().entity(id).unwrap(); - tracing::info!(%entity, "sending input event"); let cursor = CursorMove { modifiers: self.modifiers, absolute_pos: self.pos, diff --git a/violet-core/src/scope.rs b/violet-core/src/scope.rs index b35586f..e63fd9e 100644 --- a/violet-core/src/scope.rs +++ b/violet-core/src/scope.rs @@ -13,7 +13,7 @@ use pin_project::pin_project; use crate::{ assets::AssetCache, components::children, effect::Effect, input::InputEventHandler, - stored::Handle, Frame, FutureEffect, StreamEffect, Widget, + stored::Handle, style::get_stylesheet_from_entity, Frame, FutureEffect, StreamEffect, Widget, }; /// The scope within a [`Widget`][crate::Widget] is mounted or modified @@ -96,7 +96,7 @@ impl<'a> Scope<'a> { } pub fn entity(&self) -> EntityRef { - assert!(self.data.is_empty(), "EntityBuilder not flushed"); + // assert!(self.data.is_empty(), "EntityBuilder not flushed"); self.frame.world().entity(self.id).unwrap() } @@ -163,8 +163,13 @@ impl<'a> Scope<'a> { self.spawn_effect(FutureEffect::new(fut, |_: &mut Scope<'_>, _| {})) } - pub fn spawn_stream(&mut self, stream: impl 'static + Stream) { - self.spawn_effect(StreamEffect::new(stream, |_: &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 @@ -222,6 +227,11 @@ impl<'a> Scope<'a> { ) -> &mut Self { self.set(event, Box::new(func) as _) } + + /// Returns the active stylesheet for this scope + pub fn stylesheet(&self) -> EntityRef { + get_stylesheet_from_entity(&self.entity()) + } } impl Drop for Scope<'_> { diff --git a/violet-core/src/state/dedup.rs b/violet-core/src/state/dedup.rs index 58f917b..302c482 100644 --- a/violet-core/src/state/dedup.rs +++ b/violet-core/src/state/dedup.rs @@ -7,24 +7,17 @@ use tracing::info; use super::{State, StateSink, StateStream, StateStreamRef}; -/// Deduplicates a state updates for both sending and receiving halves. +/// Deduplicates a state updates for receiving streams. /// -/// This means that if the same item is sent to the sink multiple times in a row, it will only be -/// sent once. -/// -/// Likewise, the stream will be filtered for duplicate items, to catch duplicates from external -/// sinks (as items can arrive from other sinks than the one that is being deduplicated). +/// **NOTE**: Does not deduplicate for sending to sinks as it is not possible to know if the item +/// has been set by another sink or not without readback. pub struct Dedup { - last_sent: Mutable>, inner: T, } impl Dedup { pub fn new(inner: T) -> Self { - Self { - inner, - last_sent: Default::default(), - } + Self { inner } } } @@ -45,13 +38,10 @@ where Self: Sized, { let mut last_seen = None; - let mut last_sent = self.last_sent.signal_cloned().to_stream(); self.inner .stream_ref(move |item| { - let last_sent = last_sent.next().now_or_never().flatten().flatten(); - - if last_seen.as_ref() != Some(item) && last_sent.as_ref() != Some(item) { + if last_seen.as_ref() != Some(item) { last_seen = Some(item.clone()); Some(func(item)) } else { @@ -69,12 +59,10 @@ where { fn stream(&self) -> futures::prelude::stream::BoxStream<'static, Self::Item> { let mut last_seen = None; - let mut last_sent = self.last_sent.signal_cloned().to_stream(); self.inner .stream() .filter_map(move |v| { - let last_sent = last_sent.next().now_or_never().flatten().flatten(); - if last_seen.as_ref() != Some(&v) && last_sent.as_ref() != Some(&v) { + if last_seen.as_ref() != Some(&v) { last_seen = Some(v.clone()); ready(Some(v)) } else { @@ -91,7 +79,6 @@ where T::Item: 'static + Send + Sync + PartialEq + Clone, { fn send(&self, item: Self::Item) { - self.last_sent.set(Some(item.clone())); self.inner.send(item); } } diff --git a/violet-core/src/state/duplex.rs b/violet-core/src/state/duplex.rs deleted file mode 100644 index 139597f..0000000 --- a/violet-core/src/state/duplex.rs +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/violet-core/src/state/feedback.rs b/violet-core/src/state/feedback.rs new file mode 100644 index 0000000..b57d95b --- /dev/null +++ b/violet-core/src/state/feedback.rs @@ -0,0 +1,84 @@ +use std::future::ready; + +use futures::{FutureExt, StreamExt}; +use futures_signals::signal::{Mutable, SignalExt}; + +use super::{State, StateSink, StateStream, StateStreamRef}; + +/// Prevents feedback loops by dropping items in the receiving stream that were sent to the sink. +pub struct PreventFeedback { + last_sent: Mutable>, + inner: T, +} + +impl PreventFeedback { + pub fn new(inner: T) -> Self { + Self { + inner, + last_sent: Default::default(), + } + } +} + +impl State for PreventFeedback { + type Item = T::Item; +} + +impl StateStreamRef for PreventFeedback +where + T: StateStreamRef, + T::Item: 'static + Send + Sync + Clone + PartialEq, +{ + fn stream_ref V, V: 'static + Send + Sync>( + &self, + mut func: F, + ) -> impl futures::prelude::Stream + 'static + Send + where + Self: Sized, + { + let mut last_sent = self.last_sent.signal_cloned().to_stream().fuse(); + + self.inner + .stream_ref(move |item| { + let last_sent = last_sent.select_next_some().now_or_never().flatten(); + if last_sent.as_ref() != Some(item) { + Some(func(item)) + } else { + None + } + }) + .filter_map(ready) + } +} + +impl StateStream for PreventFeedback +where + T: StateStream, + T::Item: 'static + Send + Sync + PartialEq + Clone, +{ + fn stream(&self) -> futures::prelude::stream::BoxStream<'static, Self::Item> { + let mut last_sent = self.last_sent.signal_cloned().to_stream().fuse(); + self.inner + .stream() + .filter_map(move |v| { + let last_sent = last_sent.select_next_some().now_or_never().flatten(); + if last_sent.as_ref() != Some(&v) { + ready(Some(v)) + } else { + ready(None) + } + }) + .boxed() + } +} + +impl StateSink for PreventFeedback +where + T: StateSink, + T::Item: 'static + Send + Sync + PartialEq + Clone, +{ + fn send(&self, item: Self::Item) { + self.last_sent.set(Some(item.clone())); + self.inner.send(item); + } +} diff --git a/violet-core/src/state/filter.rs b/violet-core/src/state/filter.rs index 7a8971f..2bd49ab 100644 --- a/violet-core/src/state/filter.rs +++ b/violet-core/src/state/filter.rs @@ -5,19 +5,19 @@ use futures::{stream::BoxStream, StreamExt}; use super::{State, StateSink, StateStream}; /// Transforms one type to another through fallible conversion. -pub struct FilterDuplex { +pub struct FilterMap { inner: C, conv_to: Arc, conv_from: G, _marker: PhantomData, } -impl State for FilterDuplex { +impl State for FilterMap { type Item = U; } impl Option, G: Fn(U) -> Option> - FilterDuplex + FilterMap { pub fn new(inner: C, conv_to: F, conv_from: G) -> Self { Self { @@ -29,7 +29,7 @@ impl Option, G: Fn(U) -> Option> } } -impl StateStream for FilterDuplex +impl StateStream for FilterMap where C: StateStream, C::Item: 'static + Send, @@ -46,7 +46,7 @@ where } /// Bridge update-by-reference to update-by-value -impl StateSink for FilterDuplex +impl StateSink for FilterMap where C: StateSink, G: Fn(U) -> Option, diff --git a/violet-core/src/state/map.rs b/violet-core/src/state/map.rs new file mode 100644 index 0000000..9dd5e0c --- /dev/null +++ b/violet-core/src/state/map.rs @@ -0,0 +1,69 @@ +use std::{marker::PhantomData, sync::Arc}; + +use futures::{stream::BoxStream, StreamExt}; + +use super::{State, StateOwned, StateSink, StateStream}; + +/// Transforms one state to another through type conversion +/// +/// +/// This allows deriving state from another where the derived state is not present in the original. +/// +/// 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, + conv_from: G, + _marker: PhantomData, +} + +impl State for Map { + type Item = U; +} + +impl U, G: Fn(U) -> C::Item> Map { + pub fn new(inner: C, project: F, project_mut: G) -> Self { + Self { + inner, + conv_to: Arc::new(project), + conv_from: project_mut, + _marker: PhantomData, + } + } +} + +impl StateOwned for Map +where + C: StateOwned, + F: Fn(C::Item) -> U, +{ + fn read(&self) -> Self::Item { + (self.conv_to)(self.inner.read()) + } +} + +impl StateStream for Map +where + C: StateStream, + C::Item: 'static + Send, + U: 'static + Send + Sync, + F: 'static + Fn(C::Item) -> U + Sync + Send, +{ + fn stream(&self) -> BoxStream<'static, Self::Item> { + let project = self.conv_to.clone(); + self.inner.stream().map(move |v| (project)(v)).boxed() + } +} + +/// Bridge update-by-reference to update-by-value +impl StateSink for Map +where + C: StateSink, + F: Fn(C::Item) -> U, + G: Fn(U) -> C::Item, +{ + fn send(&self, value: Self::Item) { + self.inner.send((self.conv_from)(value)) + } +} diff --git a/violet-core/src/state/mod.rs b/violet-core/src/state/mod.rs index 39ef9b0..0dab0a0 100644 --- a/violet-core/src/state/mod.rs +++ b/violet-core/src/state/mod.rs @@ -8,13 +8,69 @@ use futures::{stream::BoxStream, Stream, StreamExt}; use futures_signals::signal::{Mutable, SignalExt}; mod dedup; +mod feedback; mod filter; +mod map; pub use dedup::*; +pub use feedback::*; pub use filter::*; +pub use map::*; pub trait State { type Item; + + /// Map a state from one type to another through reference projection + fn map_ref &U, G: Fn(&mut Self::Item) -> &mut U, U>( + self, + f: F, + g: G, + ) -> MapRef + where + Self: Sized, + { + MapRef::new(self, f, g) + } + + /// Map a state from one type to another + fn map U, G: Fn(U) -> Self::Item, U>( + self, + f: F, + g: G, + ) -> Map + where + Self: Sized, + { + Map::new(self, f, g) + } + + /// 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, + ) -> FilterMap + where + Self: Sized, + { + FilterMap::new(self, f, g) + } + + fn dedup(self) -> Dedup + where + Self: Sized, + Self::Item: PartialEq + Clone, + { + Dedup::new(self) + } + + fn prevent_feedback(self) -> PreventFeedback + where + Self: Sized, + Self::Item: PartialEq + Clone, + { + PreventFeedback::new(self) + } } /// A trait to read a reference from a generic state @@ -37,7 +93,9 @@ pub trait StateMut: StateRef { fn write_mut V, V>(&self, f: F) -> V; } -/// A trait to read a stream from a generic state +/// Convert a state to a stream of state changes through reference projection. +/// +/// This is only available for some states as is used to lower or transform non-cloneable states into smaller parts. pub trait StateStreamRef: State { /// Subscribe to a stream of the state /// @@ -51,6 +109,7 @@ pub trait StateStreamRef: State { Self: Sized; } +/// Convert a state to a stream of state changes. pub trait StateStream: State { fn stream(&self) -> BoxStream<'static, Self::Item>; } @@ -122,14 +181,14 @@ impl StateSink for Mutable { /// /// Can be used both to mutate and read the state, as well as to operate as a duplex sink and /// stream. -pub struct MappedState { +pub struct MapRef { inner: C, project: Arc, project_mut: G, _marker: PhantomData, } -impl &U, G: Fn(&mut C::Item) -> &mut U> MappedState { +impl &U, G: Fn(&mut C::Item) -> &mut U> MapRef { pub fn new(inner: C, project: F, project_mut: G) -> Self { Self { inner, @@ -140,11 +199,11 @@ impl &U, G: Fn(&mut C::Item) -> &mut U> Mappe } } -impl State for MappedState { +impl State for MapRef { type Item = U; } -impl StateRef for MappedState +impl StateRef for MapRef where C: StateRef, F: Fn(&C::Item) -> &U, @@ -155,7 +214,7 @@ where } } -impl StateOwned for MappedState +impl StateOwned for MapRef where C: StateRef, U: Clone, @@ -166,7 +225,7 @@ where } } -impl StateMut for MappedState +impl StateMut for MapRef where C: StateMut, F: Fn(&C::Item) -> &U, @@ -177,7 +236,7 @@ where } } -impl StateStreamRef for MappedState +impl StateStreamRef for MapRef where C: StateStreamRef, F: 'static + Fn(&C::Item) -> &U + Sync + Send, @@ -191,7 +250,7 @@ where } } -impl StateStream for MappedState +impl StateStream for MapRef where C: StateStreamRef, U: 'static + Send + Sync + Clone, @@ -203,7 +262,7 @@ where } /// Bridge update-by-reference to update-by-value -impl StateSink for MappedState +impl StateSink for MapRef where C: StateMut, F: Fn(&C::Item) -> &U, @@ -214,70 +273,6 @@ where } } -/// Transforms one state to another through type conversion -/// -/// -/// This allows deriving state from another where the derived state is not present in the original. -/// -/// 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, - conv_from: G, - _marker: PhantomData, -} - -impl State for Map { - type Item = U; -} - -impl U, G: Fn(U) -> C::Item> Map { - pub fn new(inner: C, project: F, project_mut: G) -> Self { - Self { - inner, - conv_to: Arc::new(project), - conv_from: project_mut, - _marker: PhantomData, - } - } -} - -impl StateOwned for Map -where - C: StateOwned, - F: Fn(C::Item) -> U, -{ - fn read(&self) -> Self::Item { - (self.conv_to)(self.inner.read()) - } -} - -impl StateStream for Map -where - C: StateStream, - C::Item: 'static + Send, - U: 'static + Send + Sync, - F: 'static + Fn(C::Item) -> U + Sync + Send, -{ - fn stream(&self) -> BoxStream<'static, Self::Item> { - let project = self.conv_to.clone(); - self.inner.stream().map(move |v| (project)(v)).boxed() - } -} - -/// Bridge update-by-reference to update-by-value -impl StateSink for Map -where - C: StateSink, - F: Fn(C::Item) -> U, - G: Fn(U) -> C::Item, -{ - fn send(&self, value: Self::Item) { - self.inner.send((self.conv_from)(value)) - } -} - // type MappedMutableStream = // SignalStream V>>>; // pub struct MappedStream { @@ -415,7 +410,7 @@ mod tests { async fn mapped_mutable() { let state = Mutable::new((1, 2)); - let a = MappedState::new(state.clone(), |v| &v.0, |v| &mut v.0); + let a = MapRef::new(state.clone(), |v| &v.0, |v| &mut v.0); assert_eq!(a.read(), 1); @@ -432,7 +427,7 @@ mod tests { async fn project_duplex() { let state = Mutable::new((1, 2)); - let a = MappedState::new(state.clone(), |v| &v.0, |v| &mut v.0); + let a = MapRef::new(state.clone(), |v| &v.0, |v| &mut v.0); let a = Box::new(a) as Box>; diff --git a/violet-core/src/style/mod.rs b/violet-core/src/style/mod.rs index f4d7884..3db7190 100644 --- a/violet-core/src/style/mod.rs +++ b/violet-core/src/style/mod.rs @@ -1,25 +1,22 @@ pub mod colors; use flax::{ - components::child_of, Entity, EntityBuilder, EntityRef, EntityRefMut, Exclusive, FetchExt, - RelationExt, -}; -use glam::{vec2, IVec2, Vec2}; -use palette::{ - named::{BLACK, GRAY, GREEN, LIMEGREEN, ORANGE, RED, SLATEGRAY, WHITE}, - IntoColor, Oklab, Srgba, WithAlpha, + component::ComponentValue, components::child_of, Component, Entity, EntityBuilder, EntityRef, + Exclusive, FetchExt, RelationExt, }; +use glam::Vec2; +use palette::{IntoColor, Oklab, Srgba}; use crate::{ - components::{color, draw_shape, max_size, min_size, size}, + components::{color, draw_shape, margin, max_size, min_size, padding, size}, shape::shape_rectangle, unit::Unit, Edges, Scope, }; use self::colors::{ - EERIE_BLACK_600, EERIE_BLACK_700, EERIE_BLACK_800, EERIE_BLACK_DEFAULT, JADE_400, JADE_600, - JADE_DEFAULT, LION_DEFAULT, PLATINUM_DEFAULT, REDWOOD_DEFAULT, + EERIE_BLACK_600, EERIE_BLACK_700, EERIE_BLACK_DEFAULT, JADE_400, JADE_600, JADE_DEFAULT, + LION_DEFAULT, PLATINUM_DEFAULT, REDWOOD_DEFAULT, }; #[macro_export] @@ -52,12 +49,14 @@ pub trait StyleExt { fn with_style(self, style: Self::Style) -> Self; } -/// Base properties for widget size +/// Base properties for widget size and spacing #[derive(Debug, Clone, Default)] pub struct WidgetSize { pub size: Option>, pub min_size: Option>, pub max_size: Option>, + pub margin: Option>, + pub padding: Option>, } impl WidgetSize { @@ -66,15 +65,61 @@ impl WidgetSize { } pub fn mount(&self, scope: &mut Scope<'_>) { + let stylesheet = scope.stylesheet(); + + let m = self.margin.map(|v| v.resolve(stylesheet)); + let p = self.padding.map(|v| v.resolve(stylesheet)); + scope + .set_opt(margin(), m) + .set_opt(padding(), p) .set_opt(size(), self.size) .set_opt(min_size(), self.min_size) .set_opt(max_size(), self.max_size); } + + /// Set the size + pub fn with_size(mut self, size: Unit) -> Self { + self.size = Some(size); + self + } + + /// Set the min size + pub fn with_min_size(mut self, size: Unit) -> Self { + self.min_size = Some(size); + self + } + + /// Set the max size + pub fn with_max_size(mut self, size: Unit) -> Self { + self.max_size = Some(size); + self + } + + /// Set the margin + pub fn with_margin(mut self, margin: impl Into>) -> Self { + self.margin = Some(margin.into()); + self + } + + /// Set the padding around inner content. + pub fn with_padding(mut self, padding: impl Into>) -> Self { + self.padding = Some(padding.into()); + self + } } /// A widget that allows you to set its sizing properties pub trait SizeExt { + /// Override all the size properties of the widget + fn with_size_props(mut self, size: WidgetSize) -> Self + where + Self: Sized, + { + *self.size_mut() = size; + self + } + /// Set the preferred size fn with_size(mut self, size: Unit) -> Self where @@ -102,145 +147,115 @@ pub trait SizeExt { self } - fn size_mut(&mut self) -> &mut WidgetSize; -} - -#[derive(Debug, Clone, Copy)] -pub struct Background { - pub color: Srgba, -} - -impl Background { - pub fn new(color: Srgba) -> Self { - Self { color } + /// Set the margin + fn with_margin(mut self, margin: impl Into>) -> Self + where + Self: Sized, + { + self.size_mut().margin = Some(margin.into()); + self } - pub fn mount(self, scope: &mut Scope) { - scope - .set(draw_shape(shape_rectangle()), ()) - .set(color(), self.color); + /// Set the padding around inner content. + /// + /// **NOTE**: Padding has no effect on widgets without children. Padding strictly affect + /// the distance between the widget and the contained children. Notable examples include lists + /// and stacks. This is merely added for consistency and not adding **too** many different + /// traits to implement :P + fn with_padding(mut self, padding: impl Into>) -> Self + where + Self: Sized, + { + self.size_mut().padding = Some(padding.into()); + self } -} -#[derive(Debug, Clone, Copy)] -pub struct SpacingConfig { - /// The size of the default spacing unit - pub base_scale: f32, + fn size_mut(&mut self) -> &mut WidgetSize; } -impl Default for SpacingConfig { - fn default() -> Self { - Self { base_scale: 4.0 } - } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ValueOrRef { + Value(T), + Ref(Component), } -impl SpacingConfig { - pub fn small>(&self) -> T { - T::from_spacing(self.base_scale, 1) +impl ValueOrRef { + pub fn value(value: T) -> Self { + Self::Value(value) } - pub fn medium>(&self) -> T { - T::from_spacing(self.base_scale, 2) + pub fn ref_(component: Component) -> Self { + Self::Ref(component) } - - pub fn large>(&self) -> T { - T::from_spacing(self.base_scale, 4) - } - - pub fn size, S>(&self, size: S) -> T { - T::from_spacing(self.base_scale, size) - } -} - -/// Converts a size to a pixel value -pub trait FromSize { - fn from_spacing(base_scale: f32, size: S) -> Self; } -impl FromSize> for Unit -where - T: FromSize, -{ - fn from_spacing(base_scale: f32, size: Unit) -> Self { - Unit::new( - T::from_spacing(base_scale, size.px), - T::from_spacing(base_scale, size.rel), - ) +impl Default for ValueOrRef { + fn default() -> Self { + Self::Value(Default::default()) } } -impl FromSize for Vec2 { - fn from_spacing(base_scale: f32, size: IVec2) -> Self { - vec2(base_scale * size.x as f32, base_scale * size.y as f32) +impl From> for ValueOrRef { + fn from(v: Component) -> Self { + Self::Ref(v) } } -impl FromSize for f32 { - fn from_spacing(base_scale: f32, size: usize) -> Self { - base_scale * size as f32 +impl From for ValueOrRef { + fn from(v: T) -> Self { + Self::Value(v) } } -impl FromSize for Edges { - fn from_spacing(base_scale: f32, size: usize) -> Self { - Edges::even(base_scale * size as f32) +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 + } + } } } #[derive(Debug, Clone, Copy)] -pub struct SemanticColors { - pub primary_element: Srgba, - pub secondary_element: Srgba, - pub accent_element: Srgba, - pub success_element: Srgba, - pub warning_element: Srgba, - pub error_element: Srgba, - - pub primary_surface: Srgba, - pub secondary_surface: Srgba, - pub accent_surface: Srgba, - pub success_surface: Srgba, - pub warning_surface: Srgba, - pub error_surface: Srgba, +pub struct Background { + pub color: ValueOrRef, } -impl Default for SemanticColors { - fn default() -> Self { - SemanticColors { - primary_element: WHITE.with_alpha(1.0).into_format(), - secondary_element: GRAY.with_alpha(1.0).into_format(), - accent_element: LIMEGREEN.with_alpha(1.0).into_format(), - - success_element: GREEN.with_alpha(1.0).into_format(), - warning_element: ORANGE.with_alpha(1.0).into_format(), - error_element: RED.with_alpha(1.0).into_format(), - - primary_surface: BLACK.with_alpha(1.0).into_format(), - secondary_surface: SLATEGRAY.with_alpha(1.0).into_format(), - accent_surface: SLATEGRAY.with_alpha(1.0).into_format(), - success_surface: SLATEGRAY.with_alpha(1.0).into_format(), - warning_surface: SLATEGRAY.with_alpha(1.0).into_format(), - error_surface: SLATEGRAY.with_alpha(1.0).into_format(), +impl Background { + pub fn new(color: impl Into>) -> Self { + Self { + color: color.into(), } } + + pub fn mount(self, scope: &mut Scope) { + let c = self.color.resolve(scope.stylesheet()); + scope.set(draw_shape(shape_rectangle()), ()).set(color(), c); + } +} + +pub enum Spacing { + Small, + Medium, + Large, } -pub fn get_stylesheet<'a>(scope: &'a Scope<'_>) -> EntityRef<'a> { +pub fn get_stylesheet_from_entity<'a>(entity: &EntityRef<'a>) -> EntityRef<'a> { let query = stylesheet.first_relation().traverse(child_of); - let (id, _) = scope - .entity() - .query(&query) - .get() - .expect("No stylesheet found"); + let (id, _) = entity.query(&query).get().expect("No stylesheet found"); - scope.frame().world.entity(id).unwrap() + entity.world().entity(id).unwrap() } pub fn setup_stylesheet() -> EntityBuilder { let mut builder = Entity::builder(); builder + // colors .set(primary_background(), EERIE_BLACK_DEFAULT) .set(primary_item(), PLATINUM_DEFAULT) .set(secondary_background(), EERIE_BLACK_600) @@ -256,7 +271,10 @@ pub fn setup_stylesheet() -> EntityBuilder { .set(interactive_hover(), JADE_600) .set(interactive_pressed(), JADE_400) .set(interactive_inactive(), EERIE_BLACK_700) - .set(spacing(), SpacingConfig { base_scale: 4.0 }); + // spacing + .set(spacing_small(), 4.0.into()) + .set(spacing_medium(), 8.0.into()) + .set(spacing_large(), 16.0.into()); builder } @@ -287,11 +305,14 @@ flax::component! { pub danger_background: Srgba, pub danger_item: Srgba, - pub spacing: SpacingConfig, /// Used for the main parts of interactive elements pub interactive_active: Srgba, pub interactive_inactive: Srgba, pub interactive_hover: Srgba, pub interactive_pressed: Srgba, + + pub spacing_small: Edges, + pub spacing_medium: Edges, + pub spacing_large: Edges, } diff --git a/violet-core/src/types.rs b/violet-core/src/types.rs index 487d9f9..cf38e93 100644 --- a/violet-core/src/types.rs +++ b/violet-core/src/types.rs @@ -11,6 +11,12 @@ pub struct Edges { pub bottom: f32, } +impl From for Edges { + fn from(value: f32) -> Self { + Self::even(value) + } +} + impl std::ops::Sub for Edges { type Output = Self; diff --git a/violet-core/src/widget/basic.rs b/violet-core/src/widget/basic.rs index b27575b..af2335b 100644 --- a/violet-core/src/widget/basic.rs +++ b/violet-core/src/widget/basic.rs @@ -8,7 +8,7 @@ use crate::{ self, aspect_ratio, color, draw_shape, font_size, min_size, size, text, text_wrap, }, shape, - style::{SizeExt, StyleExt, WidgetSize}, + style::{spacing_large, spacing_small, SizeExt, StyleExt, ValueOrRef, WidgetSize}, text::{TextSegment, Wrap}, unit::Unit, Scope, Widget, @@ -17,14 +17,14 @@ use crate::{ /// A rectangular widget #[derive(Debug, Clone)] pub struct Rectangle { - color: Srgba, + color: ValueOrRef, size: WidgetSize, } impl Rectangle { - pub fn new(color: Srgba) -> Self { + pub fn new(color: impl Into>) -> Self { Self { - color, + color: color.into(), size: Default::default(), } } @@ -34,9 +34,11 @@ impl Widget for Rectangle { fn mount(self, scope: &mut Scope) { self.size.mount(scope); + let c = self.color.resolve(scope.stylesheet()); + scope .set(draw_shape(shape::shape_rectangle()), ()) - .set(color(), self.color); + .set(color(), c); } } @@ -95,20 +97,19 @@ impl Default for TextStyle { pub struct Text { text: Vec, style: TextStyle, + size: WidgetSize, } impl Text { pub fn new(text: impl Into) -> Self { - Self { - text: vec![TextSegment::new(text.into())], - style: TextStyle::default(), - } + Self::rich([TextSegment::new(text.into())]) } pub fn rich(text: impl IntoIterator) -> Self { Self { text: text.into_iter().collect(), style: TextStyle::default(), + size: Default::default(), } } @@ -139,8 +140,16 @@ impl StyleExt for Text { } } +impl SizeExt for Text { + fn size_mut(&mut self) -> &mut WidgetSize { + &mut self.size + } +} + impl Widget for Text { fn mount(self, scope: &mut Scope) { + self.size.mount(scope); + scope .set(draw_shape(shape::shape_text()), ()) .set(font_size(), self.style.font_size) @@ -150,6 +159,11 @@ impl Widget for Text { } } +/// A text with a margin +pub fn label(text: impl Into) -> Text { + Text::new(text).with_margin(spacing_small()) +} + /// Allows a widget to be manually positioned and offset pub struct Positioned { offset: Unit, diff --git a/violet-core/src/widget/container.rs b/violet-core/src/widget/container.rs index 1cef1a9..e31e859 100644 --- a/violet-core/src/widget/container.rs +++ b/violet-core/src/widget/container.rs @@ -21,8 +21,6 @@ use crate::{ /// **NOTE**: direction and alignment are not included here, and should be given on a per-widget basis. #[derive(Default, Debug, Clone)] pub struct ContainerStyle { - pub margin: Edges, - pub padding: Edges, pub background: Option, } @@ -31,10 +29,6 @@ impl ContainerStyle { if let Some(background) = self.background { background.mount(scope); } - - scope - .set(margin(), self.margin) - .set(padding(), self.padding); } } @@ -68,16 +62,6 @@ impl Stack { self } - pub fn with_margin(mut self, margin: Edges) -> Self { - self.style.margin = margin; - self - } - - pub fn with_padding(mut self, padding: Edges) -> Self { - self.style.padding = padding; - self - } - pub fn with_background(mut self, background: Background) -> Self { self.style.background = Some(background); self @@ -153,16 +137,6 @@ impl List { self } - pub fn with_margin(mut self, margin: Edges) -> Self { - self.style.margin = margin; - self - } - - pub fn with_padding(mut self, padding: Edges) -> Self { - self.style.padding = padding; - self - } - pub fn with_background(mut self, background: Background) -> Self { self.style.background = Some(background); self diff --git a/violet-core/src/widget/interactive/button.rs b/violet-core/src/widget/interactive/button.rs index 19e70e8..0f1c8c4 100644 --- a/violet-core/src/widget/interactive/button.rs +++ b/violet-core/src/widget/interactive/button.rs @@ -1,32 +1,31 @@ -use flax::Component; use palette::Srgba; use winit::event::{ElementState, MouseButton}; use crate::{ components::color, - input::{focusable, on_focus, on_mouse_input}, + input::{focusable, on_mouse_input}, layout::Alignment, style::{ - get_stylesheet, interactive_active, interactive_inactive, interactive_pressed, spacing, - Background, StyleExt, + danger_item, interactive_inactive, interactive_pressed, spacing_medium, success_item, + warning_item, Background, SizeExt, StyleExt, ValueOrRef, WidgetSize, }, widget::{ContainerStyle, Stack, Text}, - Edges, Frame, Scope, Widget, + Frame, Scope, Widget, }; type ButtonCallback = Box; #[derive(Debug, Clone)] pub struct ButtonStyle { - pub normal_color: Component, - pub pressed_color: Component, + pub normal_color: ValueOrRef, + pub pressed_color: ValueOrRef, } impl Default for ButtonStyle { fn default() -> Self { Self { - normal_color: interactive_inactive(), - pressed_color: interactive_pressed(), + normal_color: interactive_inactive().into(), + pressed_color: interactive_pressed().into(), } } } @@ -36,6 +35,7 @@ pub struct Button { on_press: ButtonCallback, label: W, style: ButtonStyle, + size: WidgetSize, } impl Button { @@ -47,6 +47,7 @@ impl Button { on_press: Box::new(|_, _| {}), label, style: Default::default(), + size: WidgetSize::default().with_padding(spacing_medium()), } } @@ -58,6 +59,21 @@ impl Button { self.on_press = Box::new(on_press); self } + + pub fn success(mut self) -> Self { + self.style.normal_color = success_item().into(); + self + } + + pub fn danger(mut self) -> Self { + self.style.normal_color = danger_item().into(); + self + } + + pub fn warning(mut self) -> Self { + self.style.normal_color = warning_item().into(); + self + } } impl Button { @@ -75,40 +91,37 @@ impl StyleExt for Button { } } +impl SizeExt for Button { + fn size_mut(&mut self) -> &mut WidgetSize { + &mut self.size + } +} + impl Widget for Button { fn mount(mut self, scope: &mut Scope<'_>) { - let stylesheet = get_stylesheet(scope); - - let spacing = stylesheet.get_copy(spacing()).unwrap_or_default(); - let margin = spacing.medium(); - let padding = spacing.medium(); + let stylesheet = scope.stylesheet(); - let pressed_color = stylesheet - .get_copy(self.style.pressed_color) - .unwrap_or_default(); + let pressed_color = self.style.pressed_color.resolve(stylesheet); + let normal_color = self.style.normal_color.resolve(stylesheet); - let normal_color = stylesheet - .get_copy(self.style.normal_color) - .unwrap_or_default(); scope .set(focusable(), ()) - .on_event(on_focus(), move |_, entity, focus| { - entity.update_dedup(color(), if focus { pressed_color } else { normal_color }); - }) - .on_event(on_mouse_input(), move |frame, _, input| { + .on_event(on_mouse_input(), move |frame, entity, input| { if input.state == ElementState::Pressed { + entity.update_dedup(color(), pressed_color); (self.on_press)(frame, input.button); + } else { + entity.update_dedup(color(), normal_color); } }); Stack::new(self.label) .with_style(ContainerStyle { - margin, - padding, 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-core/src/widget/interactive/input.rs b/violet-core/src/widget/interactive/input.rs index 9b36c24..d071d37 100644 --- a/violet-core/src/widget/interactive/input.rs +++ b/violet-core/src/widget/interactive/input.rs @@ -1,11 +1,10 @@ use core::panic; use std::{fmt::Display, future::ready, str::FromStr, sync::Arc}; -use flax::Component; use futures::{FutureExt, StreamExt}; use futures_signals::signal::{self, Mutable, SignalExt}; use glam::{vec2, Vec2}; -use itertools::{Itertools, Position}; +use itertools::Itertools; use palette::Srgba; use winit::{ event::ElementState, @@ -16,10 +15,10 @@ 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, StateOwned, StateSink, StateStream}, + state::{State, StateDuplex, StateSink, StateStream}, style::{ - colors::EERIE_BLACK_300, get_stylesheet, interactive_active, interactive_inactive, spacing, - Background, SizeExt, StyleExt, WidgetSize, + colors::EERIE_BLACK_300, interactive_active, spacing_small, Background, SizeExt, StyleExt, + ValueOrRef, WidgetSize, }, text::{LayoutGlyphs, TextSegment}, to_owned, @@ -31,7 +30,7 @@ use crate::{ }; pub struct TextInputStyle { - pub cursor_color: Component, + pub cursor_color: ValueOrRef, pub background: Background, pub font_size: f32, } @@ -39,9 +38,9 @@ pub struct TextInputStyle { impl Default for TextInputStyle { fn default() -> Self { Self { - cursor_color: interactive_active(), + cursor_color: interactive_active().into(), background: Background::new(EERIE_BLACK_300), - font_size: 18.0, + font_size: 16.0, } } } @@ -58,7 +57,9 @@ impl TextInput { Self { content: Arc::new(content), style: Default::default(), - size: Default::default(), + size: WidgetSize::default() + .with_margin(spacing_small()) + .with_padding(spacing_small()), } } } @@ -80,21 +81,17 @@ impl SizeExt for TextInput { impl Widget for TextInput { fn mount(self, scope: &mut Scope<'_>) { - let stylesheet = get_stylesheet(scope); - let spacing = stylesheet.get_copy(spacing()).unwrap_or_default(); - let cursor_color = stylesheet - .get_copy(self.style.cursor_color) - .unwrap_or_default(); + let stylesheet = scope.stylesheet(); - let (tx, rx) = flume::unbounded(); + let cursor_color = self.style.cursor_color.resolve(stylesheet); - let content = self.content.clone(); + let (tx, rx) = flume::unbounded(); let focused = Mutable::new(false); // Internal text to keep track of non-bijective text changes, such as incomplete numeric // input - let mut text_content = Mutable::new(String::new()); + let text_content = Mutable::new(String::new()); let mut editor = TextEditor::new(); let layout_glyphs = Mutable::new(None); @@ -104,7 +101,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.clone(); + let content = self.content; scope.spawn({ let mut layout_glyphs = layout_glyphs.signal_cloned().to_stream(); @@ -165,7 +162,7 @@ impl Widget for TextInput { .send(Box::new(Stack::new( ( focused.then(|| Positioned::new(Rectangle::new(cursor_color) - .with_min_size(Unit::px2(2.0, 18.0))) + .with_min_size(Unit::px2(2.0, 16.0))) .with_offset(Unit::px(cursor_pos))), ) ))) @@ -221,8 +218,7 @@ impl Widget for TextInput { SignalWidget(editor_props_rx), )) .with_background(self.style.background) - .with_padding(spacing.small()) - .with_margin(spacing.small()) + .with_size_props(self.size) .mount(scope) } } diff --git a/violet-core/src/widget/interactive/slider.rs b/violet-core/src/widget/interactive/slider.rs index 3997b5d..03b4734 100644 --- a/violet-core/src/widget/interactive/slider.rs +++ b/violet-core/src/widget/interactive/slider.rs @@ -1,27 +1,23 @@ use std::{fmt::Display, str::FromStr, sync::Arc}; -use cosmic_text::Wrap; use flax::{Component, Entity, EntityRef}; use futures::{stream::BoxStream, StreamExt}; use futures_signals::signal::Mutable; -use glam::{IVec2, Vec2}; -use palette::{num::Recip, Srgba}; +use glam::Vec2; +use palette::Srgba; use winit::event::ElementState; use crate::{ components::{offset, rect}, input::{focusable, on_cursor_move, on_mouse_input, CursorMove}, layout::Alignment, - state::{Dedup, FilterDuplex, Map, StateDuplex, StateStream}, - style::{get_stylesheet, interactive_active, interactive_inactive, spacing, StyleExt}, - text::TextSegment, + state::{State, StateDuplex, StateStream}, + style::{interactive_active, interactive_inactive, spacing_small, SizeExt, StyleExt}, to_owned, unit::Unit, utils::zip_latest, - widget::{ - row, BoxSized, ContainerStyle, Positioned, Rectangle, Stack, StreamWidget, Text, WidgetExt, - }, - Edges, Scope, StreamEffect, Widget, + widget::{row, BoxSized, ContainerStyle, Positioned, Rectangle, Stack, StreamWidget, Text}, + Scope, StreamEffect, Widget, }; use super::input::TextInput; @@ -30,8 +26,8 @@ use super::input::TextInput; pub struct SliderStyle { pub track_color: Component, pub handle_color: Component, - pub track_size: Unit, - pub handle_size: Unit, + pub track_size: Unit, + pub handle_size: Unit, } impl Default for SliderStyle { @@ -39,8 +35,8 @@ impl Default for SliderStyle { Self { track_color: interactive_inactive(), handle_color: interactive_active(), - track_size: Unit::px2i(64, 1), - handle_size: Unit::px2i(1, 4), + track_size: Unit::px2(256.0, 4.0), + handle_size: Unit::px2(4.0, 16.0), } } } @@ -82,7 +78,7 @@ impl Slider { impl Widget for Slider { fn mount(self, scope: &mut Scope<'_>) { - let stylesheet = get_stylesheet(scope); + let stylesheet = scope.stylesheet(); let track_color = stylesheet .get_copy(self.style.track_color) @@ -91,10 +87,8 @@ impl Widget for Slider { .get_copy(self.style.handle_color) .unwrap_or_default(); - let spacing = stylesheet.get_copy(spacing()).unwrap_or_default(); - - let handle_size = spacing.size(self.style.handle_size); - let track_size = spacing.size(self.style.track_size); + 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)); @@ -122,16 +116,9 @@ impl Widget for Slider { handle_size, }; - let value = Arc::new(Map::new( - self.value, + let value = Arc::new(self.value.map( |v| v, - move |v| { - if let Some(transform) = &self.transform { - transform(v) - } else { - v - } - }, + move |v| self.transform.as_ref().map(|f| f(v)).unwrap_or(v), )); scope @@ -149,14 +136,13 @@ impl Widget for Slider { move |_, entity, input| update(entity, input, min, max, &*value) }); - let slider = Stack::new(handle) + Stack::new(handle) .with_vertical_alignment(Alignment::Center) .with_style(ContainerStyle { - margin: Edges::even(5.0), ..Default::default() - }); - - slider.mount(scope) + }) + .with_margin(spacing_small()) + .mount(scope) } } @@ -252,20 +238,18 @@ impl SliderWithLabel< // Wrap in dedup to prevent updating equal numeric values like `0` and `0.` etc when typing let value = Arc::new(value); - let text_value = Box::new(FilterDuplex::new( - Dedup::new(value.clone()), - |v| Some(format!("{v}")), - move |v: String| { - let v = v.parse::().ok()?; - let v = if v < min { - min - } else if v > max { - max - } else { - v - }; - - Some(v) + let text_value = Box::new(value.clone().dedup().prevent_feedback().filter_map( + move |v: V| Some(format!("{v}")), + move |v| { + v.parse::().ok().map(|v| { + if v < min { + min + } else if v > max { + max + } else { + v + } + }) }, )); diff --git a/violet-core/src/widget/mod.rs b/violet-core/src/widget/mod.rs index 7f64b90..babdc1e 100644 --- a/violet-core/src/widget/mod.rs +++ b/violet-core/src/widget/mod.rs @@ -4,7 +4,7 @@ mod container; mod future; mod interactive; -pub use basic::{BoxSized, Image, Positioned, Rectangle, Text}; +pub use basic::*; pub use container::*; use flax::{component::ComponentValue, components::name, Component}; pub use future::{SignalWidget, StreamWidget}; diff --git a/violet-web-example/Cargo.toml b/violet-demo/Cargo.toml similarity index 96% rename from violet-web-example/Cargo.toml rename to violet-demo/Cargo.toml index 7fa9759..88e6b3f 100644 --- a/violet-web-example/Cargo.toml +++ b/violet-demo/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "violet-web-example" +name = "violet-demo" version.workspace = true edition.workspace = true authors.workspace = true diff --git a/violet-web-example/index.html b/violet-demo/index.html similarity index 100% rename from violet-web-example/index.html rename to violet-demo/index.html diff --git a/violet-web-example/src/lib.rs b/violet-demo/src/lib.rs similarity index 87% rename from violet-web-example/src/lib.rs rename to violet-demo/src/lib.rs index 9ee5c0b..b39ffcd 100644 --- a/violet-web-example/src/lib.rs +++ b/violet-demo/src/lib.rs @@ -9,21 +9,21 @@ use violet::{ core::{ components, layout::{Alignment, Direction}, - state::{Map, MappedState, StateStream, StateStreamRef}, + state::{Map, MapRef, State, 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, + danger_item, success_item, Background, SizeExt, StyleExt, ValueOrRef, }, text::Wrap, to_owned, unit::Unit, utils::zip_latest, widget::{ - card, column, row, Button, ButtonStyle, List, Rectangle, SignalWidget, SliderWithLabel, - Stack, StreamWidget, Text, WidgetExt, + card, column, label, row, Button, ButtonStyle, List, Rectangle, SignalWidget, + SliderWithLabel, Stack, StreamWidget, Text, WidgetExt, }, Edges, Scope, Widget, WidgetCollection, }, @@ -84,13 +84,14 @@ impl Widget for MainApp { |v| Vec3::new(v.l, v.chroma, v.hue.into_positive_degrees()), ); - let lightness = MappedState::new(color.clone(), |v| &v.x, |v| &mut v.x); - let chroma = MappedState::new(color.clone(), |v| &v.y, |v| &mut v.y); - let hue = MappedState::new(color.clone(), |v| &v.z, |v| &mut v.z); + 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(color).with_min_size(Unit::px2(200.0, 100.0)) + 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); @@ -99,7 +100,7 @@ impl Widget for MainApp { let save_button = Button::new(Text::new("Save color")) .with_style(ButtonStyle { - normal_color: success_item(), + normal_color: success_item().into(), ..Default::default() }) .on_press({ @@ -155,7 +156,6 @@ impl Widget for MainApp { save_button, HistoryView::new(history), )) - .with_stretch(true) .with_margin(Edges::even(4.0)), ) .with_size(Unit::rel2(1.0, 1.0)) @@ -189,7 +189,8 @@ impl Widget for Tints { }; Stack::new(column(( - Rectangle::new(color.into_color()).with_min_size(Unit::px2(60.0, 60.0)), + Rectangle::new(ValueOrRef::value(color.into_color())) + .with_min_size(Unit::px2(60.0, 60.0)), Text::new(format!("{:.2}", f)), ))) .with_margin(Edges::even(4.0)) @@ -225,10 +226,7 @@ impl Widget for HistoryView { items.lock_mut().remove(i); } }) - .with_style(ButtonStyle { - normal_color: danger_item(), - ..Default::default() - }) + .danger() }; StreamWidget(self.items.stream_ref(move |items| { @@ -253,9 +251,10 @@ pub struct HistoryItem { impl Widget for HistoryItem { fn mount(self, scope: &mut Scope<'_>) { column(( - Text::new(color_hex(self.color)), + label(color_hex(self.color)), row(( - Rectangle::new(self.color.into_color()).with_size(Unit::px2(100.0, 50.0)), + Rectangle::new(ValueOrRef::value(self.color.into_color())) + .with_size(Unit::px2(100.0, 50.0)), Tints::new(self.color, self.falloff), )), )) diff --git a/violet-demo/src/main.rs b/violet-demo/src/main.rs new file mode 100644 index 0000000..e7646f4 --- /dev/null +++ b/violet-demo/src/main.rs @@ -0,0 +1,5 @@ +use violet_demo::run; + +fn main() { + run(); +} diff --git a/violet-web-example/src/main.rs b/violet-web-example/src/main.rs deleted file mode 100644 index 9871cbe..0000000 --- a/violet-web-example/src/main.rs +++ /dev/null @@ -1,5 +0,0 @@ -use violet_web_example::run; - -fn main() { - run(); -}