From 7bb14fd282b830b2b79cf344d64cb477f7c6eba4 Mon Sep 17 00:00:00 2001 From: Tei Leelo Roberts Date: Sun, 24 Mar 2024 23:39:28 +0100 Subject: [PATCH] feat: scroll --- examples/basic.rs | 1 - examples/offset.rs | 37 +++++ examples/scroll.rs | 85 ++++++++++ violet-core/src/components.rs | 23 ++- violet-core/src/hierarchy.rs | 36 +++++ violet-core/src/input.rs | 99 ++++++------ violet-core/src/layout/mod.rs | 8 +- violet-core/src/layout/stack.rs | 155 +++++++++++-------- violet-core/src/lib.rs | 3 +- violet-core/src/style/mod.rs | 2 +- violet-core/src/systems.rs | 59 ++++--- violet-core/src/types.rs | 9 +- violet-core/src/widget/container.rs | 5 + violet-core/src/widget/interactive/button.rs | 29 +++- violet-core/src/widget/interactive/input.rs | 16 +- violet-core/src/widget/interactive/slider.rs | 1 + violet-core/src/widget/mod.rs | 2 + violet-core/src/widget/scroll.rs | 80 ++++++++++ violet-wgpu/src/app.rs | 52 ++++--- violet-wgpu/src/renderer/debug_renderer.rs | 23 +-- violet-wgpu/src/renderer/mod.rs | 66 +++----- violet-wgpu/src/renderer/rect_renderer.rs | 32 ++-- violet-wgpu/src/renderer/text_renderer.rs | 21 +-- 23 files changed, 591 insertions(+), 253 deletions(-) create mode 100644 examples/offset.rs create mode 100644 examples/scroll.rs create mode 100644 violet-core/src/hierarchy.rs create mode 100644 violet-core/src/widget/scroll.rs diff --git a/examples/basic.rs b/examples/basic.rs index 8a4b2f7..9db974c 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -88,5 +88,4 @@ fn app() -> impl Widget { })), ))), )) - .contain_margins(true) } diff --git a/examples/offset.rs b/examples/offset.rs new file mode 100644 index 0000000..4f4aa85 --- /dev/null +++ b/examples/offset.rs @@ -0,0 +1,37 @@ +use tracing_subscriber::{ + prelude::__tracing_subscriber_SubscriberExt, registry, util::SubscriberInitExt, EnvFilter, +}; +use tracing_tree::HierarchicalLayer; +use violet::core::{ + style::{colors::EMERALD_500, secondary_background, spacing_medium, Background, SizeExt}, + unit::Unit, + widget::{Positioned, Rectangle, Stack}, + Widget, +}; + +pub fn main() -> anyhow::Result<()> { + registry() + .with( + HierarchicalLayer::default() + .with_deferred_spans(true) + .with_span_retrace(true) + .with_indent_lines(true), + ) + .with(EnvFilter::from_default_env()) + .init(); + + violet_wgpu::AppBuilder::new().run(app()) +} + +fn app() -> impl Widget { + Stack::new( + Positioned::new( + Rectangle::new(EMERALD_500) + .with_min_size(Unit::px2(100.0, 100.0)) + .with_margin(spacing_medium()), + ) + .with_offset(Unit::px2(10.0, 10.0)), + ) + .with_padding(spacing_medium()) + .with_background(Background::new(secondary_background())) +} diff --git a/examples/scroll.rs b/examples/scroll.rs new file mode 100644 index 0000000..d5db94b --- /dev/null +++ b/examples/scroll.rs @@ -0,0 +1,85 @@ +use futures::StreamExt; +use futures_signals::signal::Mutable; +use glam::{vec2, Vec2}; +use itertools::Itertools; +use palette::{FromColor, Hsl, Hsv, IntoColor, Oklcha, Srgba}; +use tracing_subscriber::{ + prelude::__tracing_subscriber_SubscriberExt, registry, util::SubscriberInitExt, EnvFilter, +}; +use tracing_tree::HierarchicalLayer; +use violet::core::{ + style::{spacing_small, SizeExt}, + unit::Unit, + widget::{col, Rectangle}, + Widget, +}; +use violet_core::{ + state::{State, StateStream}, + style::{colors::AMBER_500, secondary_background, spacing_medium, Background}, + widget::{label, Button, Checkbox, Scroll, StreamWidget}, +}; + +pub fn main() -> anyhow::Result<()> { + registry() + .with( + HierarchicalLayer::default() + .with_deferred_spans(true) + .with_span_retrace(true) + .with_indent_lines(true), + ) + .with(EnvFilter::from_default_env()) + .init(); + + violet_wgpu::AppBuilder::new().run(app()) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ColorSpace { + Oklcha, + Hsv, + Hsl, +} + +fn app() -> impl Widget { + let color_space = Mutable::new(ColorSpace::Oklcha); + col(( + Checkbox::label( + "Oklch", + color_space.clone().map( + |v| v == ColorSpace::Oklcha, + |v| { + if v { + ColorSpace::Oklcha + } else { + ColorSpace::Hsv + } + }, + ), + ), + Scroll::new( + StreamWidget(color_space.stream().map(|color_space| { + col((0..180) + .map(|v| { + let hue = v as f32 * 2.0; + + let color: Srgba = match color_space { + ColorSpace::Oklcha => Oklcha::new(0.5, 0.37, hue, 1.0).into_color(), + ColorSpace::Hsv => Hsv::new(hue, 1.0, 1.0).into_color(), + ColorSpace::Hsl => Hsl::new(hue, 1.0, 0.5).into_color(), + }; + + Rectangle::new(color) + // .with_margin(spacing_medium()) + .with_min_size(Unit::px2(100.0, 20.0)) + .with_maximize(Vec2::X) + }) + .collect_vec()) + .with_padding(spacing_medium()) + })), + // .with_background(Background::new(violet_core::style::accent_background())) + ), + Button::label("Button"), + )) + .with_background(Background::new(secondary_background())) + .with_padding(spacing_medium()) +} diff --git a/violet-core/src/components.rs b/violet-core/src/components.rs index 7f83022..7b1b8c0 100644 --- a/violet-core/src/components.rs +++ b/violet-core/src/components.rs @@ -1,7 +1,7 @@ use std::time::Duration; use flax::{component, Debuggable, Entity, EntityRef, Exclusive}; -use glam::Vec2; +use glam::{Mat4, Vec2}; use image::DynamicImage; use palette::Srgba; @@ -21,18 +21,25 @@ component! { /// Defines the outer bounds of a widget relative to its position pub rect: Rect => [ Debuggable ], - pub screen_rect: Rect => [ Debuggable ], + /// Clips rendering to the bounds of the widget, relative to the widget itself + pub clip_mask: Rect => [ Debuggable ], - /// Position relative to parent + /// The merged clip mask of the widget and its parents + pub screen_clip_mask: Rect => [ Debuggable ], + + /// Position relative to parent for layout position. pub local_position: Vec2 => [ Debuggable ], - /// Specifies in screen space where the widget rect upper left corner is - pub screen_position: Vec2 => [ Debuggable ], + /// Offset the widget from its original position. + /// + /// This influences the layout bounds and the final position of the widget, and will move other + /// widgets around in flow layouts. + pub offset: Unit => [ Debuggable ], - pub rotation: f32 => [ Debuggable ], + /// Optional transform of the widget. Applied after layout + pub transform: Mat4, - /// Offset the widget from its original position - pub offset: Unit => [ Debuggable ], + pub screen_transform: Mat4, /// Explicit widget size. This will override the intrinsic size of the widget. /// diff --git a/violet-core/src/hierarchy.rs b/violet-core/src/hierarchy.rs new file mode 100644 index 0000000..c20615f --- /dev/null +++ b/violet-core/src/hierarchy.rs @@ -0,0 +1,36 @@ +use flax::{EntityRef, World}; + +use crate::components::children; + +pub struct OrderedDfsIterator<'a> { + world: &'a World, + // queue: VecDeque>, + stack: Vec>, +} + +impl<'a> OrderedDfsIterator<'a> { + pub fn new(world: &'a World, stack: EntityRef<'a>) -> Self { + Self { + world, + stack: vec![stack], + } + } +} + +impl<'a> Iterator for OrderedDfsIterator<'a> { + type Item = EntityRef<'a>; + + fn next(&mut self) -> Option { + let entity = self.stack.pop()?; + if let Ok(children) = entity.get(children()) { + self.stack.extend( + children + .iter() + .rev() + .map(|&id| self.world.entity(id).unwrap()), + ); + } + + Some(entity) + } +} diff --git a/violet-core/src/input.rs b/violet-core/src/input.rs index 07e8914..322361a 100644 --- a/violet-core/src/input.rs +++ b/violet-core/src/input.rs @@ -1,8 +1,5 @@ -use flax::{ - component, components::child_of, entity_ids, fetch::Satisfied, filter::All, Component, Entity, - EntityIds, EntityRef, Fetch, FetchExt, Query, Topo, World, -}; -use glam::Vec2; +use flax::{component, Entity, EntityRef, FetchExt, World}; +use glam::{Vec2, Vec3Swizzles}; /// NOTE: maybe redefine these types ourselves pub use winit::{event, keyboard}; @@ -12,34 +9,14 @@ use winit::{ }; use crate::{ - components::{rect, screen_position, screen_rect}, + components::{rect, screen_transform}, + hierarchy::OrderedDfsIterator, scope::ScopeRef, - Frame, Rect, + Frame, }; pub struct Input {} -#[derive(Fetch)] -struct IntersectQuery { - id: EntityIds, - rect: Component, - screen_pos: Component, - sticky: Satisfied>, - focusable: Component<()>, -} - -impl IntersectQuery { - pub fn new() -> Self { - Self { - id: entity_ids(), - rect: rect(), - screen_pos: screen_position(), - sticky: focus_sticky().satisfied(), - focusable: focusable(), - } - } -} - #[derive(Debug, Clone)] struct FocusedEntity { id: Entity, @@ -47,38 +24,43 @@ struct FocusedEntity { } pub struct InputState { + root: Entity, focused: Option, pos: Vec2, - intersect_query: Query, modifiers: ModifiersState, } impl InputState { - pub fn new(pos: Vec2) -> Self { + pub fn new(root: Entity, pos: Vec2) -> Self { Self { focused: None, pos, - intersect_query: Query::new(IntersectQuery::new()).topo(child_of), modifiers: Default::default(), + root, } } - pub fn on_mouse_input(&mut self, frame: &mut Frame, state: ElementState, button: MouseButton) { - let cursor_pos = self.pos; + fn find_intersect(&self, frame: &Frame, pos: Vec2) -> Option<(Entity, Vec2)> { + let query = (screen_transform(), rect()).filtered(focusable().with()); + OrderedDfsIterator::new(&frame.world, frame.world.entity(self.root).unwrap()) + .filter_map(|entity| { + let mut query = entity.query(&query); + let (transform, rect) = query.get()?; - let intersect = self - .intersect_query - .borrow(frame.world()) - .iter() - .filter_map(|item| { - let local_pos = cursor_pos - *item.screen_pos; - if item.rect.contains_point(local_pos) { - Some((item.id, (*item.screen_pos + item.rect.min))) + let local_pos = transform.inverse().transform_point3(pos.extend(0.0)).xy(); + + if rect.contains_point(local_pos) { + Some((entity.id(), local_pos)) } else { None } }) - .last(); + .last() + } + + pub fn on_mouse_input(&mut self, frame: &mut Frame, state: ElementState, button: MouseButton) { + let cursor_pos = self.pos; + let intersect = self.find_intersect(frame, cursor_pos); match (state, &self.focused, intersect) { // Focus changed @@ -94,13 +76,13 @@ impl InputState { // Send the event to the intersected entity - if let Some((id, origin)) = intersect { + if let Some((id, local_pos)) = intersect { let entity = frame.world().entity(id).unwrap(); let cursor = CursorMove { modifiers: self.modifiers, absolute_pos: self.pos, - local_pos: self.pos - origin, + local_pos, }; if let Ok(mut on_input) = entity.get_mut(on_mouse_input()) { let s = ScopeRef::new(frame, entity); @@ -121,7 +103,7 @@ impl InputState { self.pos = pos; if let Some(entity) = &self.focused(&frame.world) { - let screen_rect = entity.get_copy(screen_rect()).unwrap_or_default(); + let transform = entity.get_copy(screen_transform()).unwrap_or_default(); if let Ok(mut on_input) = entity.get_mut(on_cursor_move()) { let s = ScopeRef::new(frame, *entity); on_input( @@ -129,7 +111,25 @@ impl InputState { CursorMove { modifiers: self.modifiers, absolute_pos: pos, - local_pos: pos - screen_rect.min, + local_pos: transform.inverse().transform_point3(pos.extend(0.0)).xy(), + }, + ); + } + } + } + + pub fn on_scroll(&mut self, frame: &mut Frame, delta: Vec2) { + let intersect = self.find_intersect(frame, self.pos); + + if let Some((id, _)) = intersect { + let entity = frame.world().entity(id).unwrap(); + if let Ok(mut on_input) = entity.get_mut(on_scroll()) { + let s = ScopeRef::new(frame, entity); + on_input( + &s, + Scroll { + delta, + modifiers: self.modifiers, }, ); } @@ -206,6 +206,12 @@ pub struct CursorMove { pub local_pos: Vec2, } +#[derive(Debug, Clone)] +pub struct Scroll { + pub delta: Vec2, + pub modifiers: ModifiersState, +} + pub struct KeyboardInput { pub modifiers: ModifiersState, @@ -221,4 +227,5 @@ component! { pub on_cursor_move: InputEventHandler, pub on_mouse_input: InputEventHandler, pub on_keyboard_input: InputEventHandler, + pub on_scroll: InputEventHandler, } diff --git a/violet-core/src/layout/mod.rs b/violet-core/src/layout/mod.rs index d3e80d2..7d4106e 100644 --- a/violet-core/src/layout/mod.rs +++ b/violet-core/src/layout/mod.rs @@ -277,7 +277,7 @@ pub(crate) fn query_size(world: &World, entity: &EntityRef, args: QueryArgs) -> // Check if cache is valid for cached in cache.get_query(args.direction) { if validate_cached_query(cached, limits, args.content_area) { - return cached.value; + // return cached.value; } } @@ -408,7 +408,7 @@ pub(crate) fn apply_layout( // assert!(limits.min_size.x <= limits.max_size.x); // assert!(limits.min_size.y <= limits.max_size.y); // let _span = tracing::info_span!( "Updating subtree", %entity, ?constraints).entered(); - let _span = tracing::debug_span!("update_subtree", %entity).entered(); + let _span = tracing::debug_span!("update_subtree", %limits, %content_area, %entity).entered(); let query = ( layout_cache().as_mut(), @@ -442,9 +442,9 @@ pub(crate) fn apply_layout( if let Some(value) = &cache.layout { if validate_cached_layout(value, limits, content_area, cache.hints.relative_size) { - tracing::debug!(%entity, %value.value.rect, %value.value.can_grow, "found valid cached layout"); + tracing::trace!(%entity, %value.value.rect, %value.value.can_grow, "found valid cached layout"); - return value.value; + // return value.value; } } diff --git a/violet-core/src/layout/stack.rs b/violet-core/src/layout/stack.rs index a674fef..cca30f0 100644 --- a/violet-core/src/layout/stack.rs +++ b/violet-core/src/layout/stack.rs @@ -3,69 +3,13 @@ use glam::{vec2, BVec2, Vec2}; use itertools::Itertools; use crate::{ - components, + components::{self, clip_mask}, layout::{query_size, SizingHints}, Edges, Rect, }; use super::{apply_layout, resolve_pos, Alignment, Block, LayoutLimits, QueryArgs, Sizing}; -#[derive(Debug)] -pub struct StackableBounds { - inner: Rect, - outer: Rect, -} - -impl Default for StackableBounds { - fn default() -> Self { - Self { - inner: Rect { - min: Vec2::MAX, - max: Vec2::MIN, - }, - outer: Rect { - min: Vec2::MAX, - max: Vec2::MIN, - }, - } - } -} - -impl StackableBounds { - fn new(rect: Rect, margin: Edges) -> Self { - Self { - inner: rect, - outer: rect.pad(&margin), - } - } - - fn from_rect(rect: Rect) -> Self { - Self { - inner: rect, - outer: rect, - } - } - - fn merge(&self, other: &Self) -> Self { - Self { - inner: self.inner.merge(other.inner), - outer: self.outer.merge(other.outer), - } - } - - fn margin(&self) -> Edges { - let min = self.inner.min - self.outer.min; - let max = self.outer.max - self.inner.max; - - Edges { - left: min.x, - right: max.x, - top: min.y, - bottom: max.y, - } - } -} - /// The stack layout /// /// A stack layout is the Swiss army knife of layouts. @@ -85,6 +29,7 @@ impl StackableBounds { pub struct StackLayout { pub horizontal_alignment: Alignment, pub vertical_alignment: Alignment, + pub clip: bool, } impl StackLayout { @@ -135,6 +80,7 @@ impl StackLayout { let mut can_grow = BVec2::FALSE; let offset = resolve_pos(entity, content_area.size(), size); + for (entity, block) in blocks { let block_size = block.rect.size(); let offset = content_area.min @@ -144,7 +90,11 @@ impl StackLayout { self.vertical_alignment.align_offset(size.y, block_size.y), ); - tracing::debug!(?offset, %entity); + let clip_mask = if self.clip { + Rect::from_size(limits.max_size) + } else { + Rect::new(Vec2::MIN, Vec2::MAX) + }; aligned_bounds = aligned_bounds.merge(&StackableBounds::new( block.rect.translate(offset), @@ -156,15 +106,19 @@ impl StackLayout { // entity.update_dedup(components::rect(), block.rect.translate(offset)); entity.update_dedup(components::rect(), block.rect); entity.update_dedup(components::local_position(), offset); + entity + .update_dedup(components::clip_mask(), clip_mask) + .unwrap(); } // aligned_bounds.inner = aligned_bounds.inner.max_size(limits.min_size); - let rect = aligned_bounds.inner.max_size(limits.min_size); + let mut rect = aligned_bounds.inner.max_size(limits.min_size); - let margin = aligned_bounds.margin(); + if self.clip { + rect = rect.clamp_size(limits.min_size, limits.max_size); + } - // rect.min += content_area.min; - // rect.max += content_area.min; + let margin = aligned_bounds.margin(); Block::new(rect, margin, can_grow) } @@ -214,14 +168,83 @@ impl StackLayout { let min_margin = min_bounds.margin(); let preferred_margin = preferred_bounds.margin(); + // tracing::info!(%args.limits.max_size); + + let min = min_bounds.inner.max_size(args.limits.min_size); + let preferred = preferred_bounds.inner.max_size(preferred_size); + + let clamp_size = if self.clip { + args.limits.max_size + } else { + Vec2::MAX + }; + Sizing { - min: min_bounds.inner.max_size(args.limits.min_size), - // .clamp_size(limits.min_size, limits.max_size), - preferred: preferred_bounds.inner.max_size(preferred_size), - // .clamp_size(limits.min_size, limits.max_size), + min: if self.clip { + min.with_size(Vec2::ZERO) + } else { + min.min_size(clamp_size) + }, + preferred: preferred.min_size(clamp_size), margin: min_margin.max(preferred_margin), hints, maximize, } } } + +#[derive(Debug)] +pub struct StackableBounds { + inner: Rect, + outer: Rect, +} + +impl Default for StackableBounds { + fn default() -> Self { + Self { + inner: Rect { + min: Vec2::MAX, + max: Vec2::MIN, + }, + outer: Rect { + min: Vec2::MAX, + max: Vec2::MIN, + }, + } + } +} + +impl StackableBounds { + fn new(rect: Rect, margin: Edges) -> Self { + Self { + inner: rect, + outer: rect.pad(&margin), + } + } + + fn from_rect(rect: Rect) -> Self { + Self { + inner: rect, + outer: rect, + } + } + + fn merge(&self, other: &Self) -> Self { + Self { + inner: self.inner.merge(other.inner), + outer: self.outer.merge(other.outer), + } + } + + fn margin(&self) -> Edges { + let min = self.inner.min - self.outer.min; + let max = self.outer.max - self.inner.max; + + Edges { + left: min.x, + right: max.x, + top: min.y, + bottom: max.y, + } + } +} diff --git a/violet-core/src/lib.rs b/violet-core/src/lib.rs index e927df7..ea62a34 100644 --- a/violet-core/src/lib.rs +++ b/violet-core/src/lib.rs @@ -7,7 +7,9 @@ pub mod editor; pub mod effect; pub mod executor; mod frame; +pub mod hierarchy; pub mod input; +pub mod io; pub mod layout; mod scope; pub mod shape; @@ -22,7 +24,6 @@ mod types; pub mod unit; pub mod utils; pub mod widget; -pub mod io; pub use effect::{FutureEffect, StreamEffect}; pub use frame::Frame; diff --git a/violet-core/src/style/mod.rs b/violet-core/src/style/mod.rs index 889db34..f0dd489 100644 --- a/violet-core/src/style/mod.rs +++ b/violet-core/src/style/mod.rs @@ -279,7 +279,7 @@ pub fn setup_stylesheet() -> EntityBuilder { .set(danger_item(), REDWOOD_400) .set(interactive_active(), EMERALD_500) .set(interactive_passive(), ZINC_800) - .set(interactive_hover(), EMERALD_800) + .set(interactive_hover(), EMERALD_400) .set(interactive_pressed(), EMERALD_500) .set(interactive_inactive(), ZINC_700) // spacing diff --git a/violet-core/src/systems.rs b/violet-core/src/systems.rs index 3f943c2..a286973 100644 --- a/violet-core/src/systems.rs +++ b/violet-core/src/systems.rs @@ -1,7 +1,6 @@ use std::{ collections::HashSet, sync::{Arc, Weak}, - thread::scope, }; use atomic_refcell::AtomicRefCell; @@ -11,16 +10,17 @@ use flax::{ components::child_of, entity_ids, events::{EventData, EventSubscriber}, + fetch::entity_refs, filter::Or, - query::TopoBorrow, - BoxedSystem, CommandBuffer, Dfs, DfsBorrow, Entity, EntityBuilder, Fetch, FetchExt, FetchItem, - Query, QueryBorrow, System, Topo, World, + BoxedSystem, CommandBuffer, Dfs, DfsBorrow, Entity, EntityBuilder, EntityRef, Fetch, FetchExt, + FetchItem, Query, QueryBorrow, System, World, }; -use glam::Vec2; +use glam::{Mat4, Vec2, Vec3, Vec3Swizzles}; use crate::{ components::{ - self, children, layout_bounds, local_position, rect, screen_position, screen_rect, text, + self, children, clip_mask, layout_bounds, local_position, rect, screen_clip_mask, + screen_transform, text, transform, }, layout::{ apply_layout, @@ -46,9 +46,11 @@ pub fn hydrate_text() -> BoxedSystem { pub fn widget_template(entity: &mut EntityBuilder, name: String) { entity .set(flax::components::name(), name) - .set_default(screen_position()) + .set_default(screen_transform()) + .set_default(transform()) .set_default(local_position()) - .set_default(screen_rect()) + .set(clip_mask(), Rect::new(Vec2::MIN, Vec2::MAX)) + .set_default(screen_clip_mask()) .set_default(rect()); } @@ -163,6 +165,8 @@ pub fn layout_system(root: Entity) -> BoxedSystem { puffin::profile_scope!("layout_system"); + tracing::info!(%canvas_rect, "apply_layout"); + for &child in children { let entity = world.entity(child).unwrap(); @@ -183,26 +187,41 @@ pub fn layout_system(root: Entity) -> BoxedSystem { } /// Updates the apparent screen position of entities based on the hierarchy -pub fn transform_system() -> BoxedSystem { +pub fn transform_system(root: Entity) -> BoxedSystem { System::builder() .with_query( Query::new(( - screen_position().as_mut(), - screen_rect().as_mut(), - rect(), + screen_transform().as_mut(), + screen_clip_mask().as_mut(), + clip_mask(), local_position(), + transform().opt_or_default(), )) .with_strategy(Dfs::new(child_of)), ) - .build(|mut query: DfsBorrow<_>| { - query.traverse( - &Vec2::ZERO, - |(pos, screen_rect, rect, local_pos): (&mut Vec2, &mut Rect, &Rect, &Vec2), + .build(move |mut query: DfsBorrow<_>| { + query.traverse_from( + root, + &(Mat4::IDENTITY, Rect::new(Vec2::MIN, Vec2::MAX)), + |(screen_trans, screen_mask, &mask, &local_pos, &trans): ( + &mut Mat4, + &mut Rect, + &Rect, + &Vec2, + &Mat4, + ), _, - parent_pos| { - *pos = *parent_pos + *local_pos; - *screen_rect = rect.translate(*pos); - *pos + &(parent, parent_mask)| { + let local_transform = Mat4::from_translation(local_pos.extend(0.0)) * trans; + + let mask_offset = parent.transform_point3(Vec3::ZERO).xy(); + *screen_mask = mask.translate(mask_offset).intersect(parent_mask); + + tracing::info!(%screen_mask); + + *screen_trans = parent * local_transform; + + (*screen_trans, *screen_mask) }, ); }) diff --git a/violet-core/src/types.rs b/violet-core/src/types.rs index 67a4fba..dabda5f 100644 --- a/violet-core/src/types.rs +++ b/violet-core/src/types.rs @@ -133,7 +133,7 @@ pub struct Rect { impl Display for Rect { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("({:?},{:?})", self.min, self.max)) + f.write_fmt(format_args!("({},{})", self.min, self.max)) } } @@ -275,6 +275,13 @@ impl Rect { let max = min + size; Rect { min, max } } + + pub(crate) fn intersect(&self, other: Rect) -> Rect { + let min = self.min.max(other.min).min(other.max); + let max = self.max.min(other.max).max(other.min); + + Rect { min, max } + } } impl Display for Edges { diff --git a/violet-core/src/widget/container.rs b/violet-core/src/widget/container.rs index 0d10477..a4f49b8 100644 --- a/violet-core/src/widget/container.rs +++ b/violet-core/src/widget/container.rs @@ -66,6 +66,11 @@ impl Stack { self.style.background = Some(background); self } + + pub fn with_clip(mut self, clip: bool) -> Self { + self.layout.clip = clip; + self + } } impl StyleExt for Stack { diff --git a/violet-core/src/widget/interactive/button.rs b/violet-core/src/widget/interactive/button.rs index 56b37b7..dd9533b 100644 --- a/violet-core/src/widget/interactive/button.rs +++ b/violet-core/src/widget/interactive/button.rs @@ -133,14 +133,15 @@ impl Widget for Button { } } -pub struct Checkbox { +pub struct Checkbox { state: Box>, style: ButtonStyle, size: WidgetSize, + label: W, } -impl Checkbox { - pub fn new(state: impl 'static + Send + Sync + StateDuplex) -> Self { +impl Checkbox { + pub fn new(label: W, state: impl 'static + Send + Sync + StateDuplex) -> Self { Self { state: Box::new(state), style: Default::default(), @@ -148,11 +149,21 @@ impl Checkbox { .with_padding(spacing_medium()) .with_margin(spacing_medium()) .with_min_size(Unit::px2(28.0, 28.0)), + label, } } } -impl Widget for Checkbox { +impl Checkbox { + pub fn label( + label: impl Into, + state: impl 'static + Send + Sync + StateDuplex, + ) -> Self { + Self::new(Text::new(label.into()), state) + } +} + +impl Widget for Checkbox { fn mount(self, scope: &mut Scope<'_>) { let stylesheet = scope.stylesheet(); @@ -179,7 +190,7 @@ impl Widget for Checkbox { } }); - Stack::new(()) + Stack::new(self.label) .with_style(ContainerStyle { background: Some(Background::new(normal_color)), }) @@ -211,6 +222,14 @@ impl Radio { } } } +impl Radio { + pub fn label( + label: impl Into, + state: impl 'static + Send + Sync + StateDuplex, + ) -> Self { + Self::new(Text::new(label.into()), state) + } +} impl Widget for Radio { fn mount(self, scope: &mut Scope<'_>) { diff --git a/violet-core/src/widget/interactive/input.rs b/violet-core/src/widget/interactive/input.rs index 48b880a..f6717b1 100644 --- a/violet-core/src/widget/interactive/input.rs +++ b/violet-core/src/widget/interactive/input.rs @@ -3,7 +3,7 @@ use std::{fmt::Display, future::ready, str::FromStr, sync::Arc}; use futures::StreamExt; use futures_signals::signal::{self, Mutable, SignalExt}; -use glam::{vec2, Vec2}; +use glam::{vec2, Mat4, Vec2, Vec3, Vec3Swizzles}; use itertools::Itertools; use palette::{Srgba, WithAlpha}; use web_time::Duration; @@ -13,7 +13,7 @@ use winit::{ }; use crate::{ - components::{self, screen_rect}, + components::{self, screen_transform}, editor::{CursorMove, EditAction, EditorAction, TextEditor}, input::{ focus_sticky, focusable, on_cursor_move, on_focus, on_keyboard_input, on_mouse_input, @@ -22,8 +22,8 @@ use crate::{ io, state::{State, StateDuplex, StateSink, StateStream}, style::{ - interactive_active, interactive_hover, interactive_inactive, interactive_passive, - spacing_small, Background, SizeExt, StyleExt, ValueOrRef, WidgetSize, + interactive_active, interactive_hover, interactive_passive, spacing_small, Background, + SizeExt, StyleExt, ValueOrRef, WidgetSize, }, text::{CursorLocation, LayoutGlyphs, TextSegment}, time::sleep, @@ -110,7 +110,7 @@ impl Widget for TextInput { let mut editor = TextEditor::new(); let layout_glyphs = Mutable::new(None); - let text_bounds: Mutable> = Mutable::new(None); + let text_bounds: Mutable> = Mutable::new(None); editor.set_cursor_at_end(); @@ -271,7 +271,8 @@ impl Widget for TextInput { if let (Some(glyphs), Some(text_bounds)) = (&*glyphs, &*text_bounds.lock_ref()) { if input.state == ElementState::Pressed { - let text_pos = input.cursor.absolute_pos - text_bounds.min; + let text_pos = input.cursor.absolute_pos + - text_bounds.transform_point3(Vec3::ZERO).xy(); if let Some(hit) = glyphs.hit(text_pos) { dragging.set(Some(hit)); @@ -300,6 +301,7 @@ impl Widget for TextInput { if let Some(glyphs) = &*glyphs { let text_pos = input.local_pos; + tracing::info!(?text_pos); if let Some(hit) = glyphs.hit(text_pos) { tx.send(Action::Editor(EditorAction::SelectionMove( CursorMove::SetPosition(dragging), @@ -331,7 +333,7 @@ impl Widget for TextInput { Text::rich([TextSegment::new(v)]) .with_font_size(self.style.font_size) .monitor_signal(components::layout_glyphs(), layout_glyphs.clone()) - .monitor_signal(screen_rect(), text_bounds.clone()) + .monitor_signal(screen_transform(), text_bounds.clone()) })), Float::new(SignalWidget(editor_props_rx)), )) diff --git a/violet-core/src/widget/interactive/slider.rs b/violet-core/src/widget/interactive/slider.rs index 5ce72c6..a1b5adb 100644 --- a/violet-core/src/widget/interactive/slider.rs +++ b/violet-core/src/widget/interactive/slider.rs @@ -176,6 +176,7 @@ impl Widget for SliderHandle { })); Positioned::new(Rectangle::new(self.handle_color).with_min_size(self.handle_size)) + .with_offset(Unit::px2(100.0, 10.0)) .with_anchor(Unit::rel2(0.5, 0.0)) .mount(scope) } diff --git a/violet-core/src/widget/mod.rs b/violet-core/src/widget/mod.rs index c997a60..1b3b62a 100644 --- a/violet-core/src/widget/mod.rs +++ b/violet-core/src/widget/mod.rs @@ -3,6 +3,7 @@ mod basic; mod container; mod future; mod interactive; +mod scroll; pub use basic::*; pub use container::*; @@ -10,6 +11,7 @@ use flax::{component::ComponentValue, components::name, Component}; pub use future::{SignalWidget, StreamWidget}; use futures_signals::signal::Mutable; pub use interactive::{button::*, input::*, slider::*}; +pub use scroll::Scroll; /// A widget is a description of a part of the Ui with the capability to mount itself into the world. /// diff --git a/violet-core/src/widget/scroll.rs b/violet-core/src/widget/scroll.rs new file mode 100644 index 0000000..d8b9c7e --- /dev/null +++ b/violet-core/src/widget/scroll.rs @@ -0,0 +1,80 @@ +use futures_signals::signal::Mutable; +use glam::{vec2, Mat4, Vec2}; + +use crate::{ + components::{rect, transform}, + input::{focusable, on_scroll}, + state::{StateMut, StateStream}, + style::{interactive_active, Background}, + to_owned, + utils::zip_latest, + Scope, Widget, +}; + +use super::Stack; + +pub struct Scroll { + items: W, +} + +impl Scroll { + pub fn new(items: W) -> Self { + Self { items } + } +} + +impl Widget for Scroll { + fn mount(self, scope: &mut Scope<'_>) { + let size = Mutable::new(Vec2::ZERO); + + let scroll_pos = Mutable::new(Vec2::ZERO); + let sensitivity = vec2(32.0, -32.0); + scope.on_event(on_scroll(), { + to_owned![size, scroll_pos]; + move |_, scroll| { + scroll_pos.write_mut(|v| { + *v = (*v + scroll.delta * sensitivity).clamp(Vec2::ZERO, size.get()) + }); + } + }); + + scope.set(focusable(), ()); + + Stack::new(ScrolledContent { + items: self.items, + scroll_pos, + size, + }) + .with_clip(true) + .mount(scope) + } +} + +struct ScrolledContent { + items: W, + scroll_pos: Mutable, + size: Mutable, +} + +impl Widget for ScrolledContent { + fn mount(self, scope: &mut Scope<'_>) { + scope.spawn_stream( + zip_latest(self.size.stream(), self.scroll_pos.stream()), + |scope, (size, scroll)| { + tracing::info!(%scroll, %size); + let scroll = scroll.clamp(Vec2::ZERO, size); + scope.set(transform(), Mat4::from_translation(-scroll.extend(0.0))); + }, + ); + + scope.monitor(rect(), move |v| { + if let Some(v) = v { + self.size.set(v.size()); + } + }); + + Stack::new(self.items) + .with_background(Background::new(interactive_active())) + .mount(scope) + } +} diff --git a/violet-wgpu/src/app.rs b/violet-wgpu/src/app.rs index a1935b2..7b14cdc 100644 --- a/violet-wgpu/src/app.rs +++ b/violet-wgpu/src/app.rs @@ -15,7 +15,7 @@ use winit::{ use violet_core::{ animation::update_animations, assets::AssetCache, - components::{self, local_position, rect, screen_position}, + components::{self, local_position, max_size, rect, screen_transform, size}, executor::Executor, input::InputState, io::{self, Clipboard}, @@ -25,7 +25,8 @@ use violet_core::{ transform_system, }, to_owned, - widget::col, + unit::Unit, + widget::{col, Stack}, Frame, FutureEffect, Rect, Scope, Widget, }; @@ -47,20 +48,20 @@ impl Widget for Canvas { scope .set(name(), "Canvas".into()) .set(stylesheet(self.stylesheet), ()) - .set( - rect(), - Rect { - min: Vec2::ZERO, - max: self.size, - }, - ) - .set_default(screen_position()) + .set_default(rect()) + .set(max_size(), Unit::px(self.size)) + .set(size(), Unit::px(self.size)) + .set_default(screen_transform()) .set_default(local_position()); - col(self.root) - .contain_margins(true) - .with_background(Background::new(primary_background())) - .mount(scope); + scope.attach( + Stack::new( + col(self.root) + .contain_margins(true) + .with_background(Background::new(primary_background())), + ) + .with_clip(true), + ); } } @@ -129,8 +130,6 @@ impl AppBuilder { window.request_inner_size(winit::dpi::PhysicalSize::new(w, h)); } - let mut input_state = InputState::new(Vec2::ZERO); - let stylesheet = setup_stylesheet().spawn(frame.world_mut()); let clipboard = frame.store_mut().insert(Arc::new(Clipboard::new())); @@ -143,6 +142,8 @@ impl AppBuilder { root, }); + let mut input_state = InputState::new(root, Vec2::ZERO); + tracing::info!("creating gpu"); let window = Arc::new(window); @@ -179,7 +180,7 @@ impl AppBuilder { .with_system(update_text_buffers(text_system.clone())) .with_system(invalidate_cached_layout_system(&mut frame.world)) .with_system(layout_system(root)) - .with_system(transform_system()); + .with_system(transform_system(root)); let start_time = Instant::now(); @@ -217,7 +218,7 @@ impl AppBuilder { .borrow(&instance.frame.world) .iter() .count(); - tracing::info!(archetype_count = archetypes.len(), entity_count, pruned); + tracing::debug!(archetype_count = archetypes.len(), entity_count, pruned); // let report = instance.?stats.report(); // window.set_title(&format!( @@ -256,6 +257,19 @@ impl AppBuilder { vec2(position.x as f32, position.y as f32), ) } + WindowEvent::MouseWheel { + device_id, + delta, + phase, + } => { + puffin::profile_scope!("MouseWheel"); + match delta { + winit::event::MouseScrollDelta::LineDelta(x, y) => { + input_state.on_scroll(&mut instance.frame, vec2(x, y)) + } + winit::event::MouseScrollDelta::PixelDelta(_) => todo!(), + } + } WindowEvent::ScaleFactorChanged { scale_factor: s, .. } => { @@ -329,6 +343,8 @@ impl App { ) .unwrap(); + self.schedule.execute_seq(&mut self.frame.world).unwrap(); + if let Some(renderer) = &mut self.renderer { renderer.resize(size, self.scale_factor); } diff --git a/violet-wgpu/src/renderer/debug_renderer.rs b/violet-wgpu/src/renderer/debug_renderer.rs index 9cb7dfc..bf18038 100644 --- a/violet-wgpu/src/renderer/debug_renderer.rs +++ b/violet-wgpu/src/renderer/debug_renderer.rs @@ -6,11 +6,8 @@ use image::DynamicImage; use itertools::Itertools; use violet_core::{ assets::Asset, - components::screen_rect, - layout::{ - cache::{layout_cache, LayoutUpdate}, - Direction, - }, + components::{rect, screen_clip_mask, transform}, + layout::cache::{layout_cache, LayoutUpdate}, stored::{self, Handle}, Frame, }; @@ -201,13 +198,16 @@ impl DebugRenderer { }); let objects = objects.filter_map(|(entity, shader, color)| { - let screen_rect = entity.get(screen_rect()).ok()?.align_to_grid(); + let rect = entity.get_copy(rect()).ok()?.align_to_grid(); + let transform = entity.get_copy(transform()).ok()?; + let clip_mask = entity.get_copy(screen_clip_mask()).ok()?; - let model_matrix = Mat4::from_scale_rotation_translation( - screen_rect.size().extend(1.0), - Quat::IDENTITY, - screen_rect.pos().extend(0.2), - ); + let model_matrix = transform * Mat4::from_scale(rect.size().extend(1.0)); + // 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, @@ -220,6 +220,7 @@ impl DebugRenderer { bind_group: self.bind_group.clone(), mesh: self.mesh.clone(), index_count: 6, + clip_mask: (clip_mask.min.as_uvec2(), clip_mask.max.as_uvec2()), }, object_data, )) diff --git a/violet-wgpu/src/renderer/mod.rs b/violet-wgpu/src/renderer/mod.rs index fb53254..5435452 100644 --- a/violet-wgpu/src/renderer/mod.rs +++ b/violet-wgpu/src/renderer/mod.rs @@ -4,15 +4,15 @@ use bytemuck::Zeroable; use flax::{ entity_ids, fetch::{entity_refs, EntityRefs, NthRelation}, - CommandBuffer, Component, Entity, EntityRef, Fetch, Query, QueryBorrow, RelationExt, World, + CommandBuffer, Component, Entity, Fetch, Query, QueryBorrow, RelationExt, }; -use glam::{vec4, Mat4, Vec4}; +use glam::{vec4, Mat4, UVec2, Vec4}; use itertools::Itertools; use palette::Srgba; use parking_lot::Mutex; -use smallvec::{smallvec, SmallVec}; use violet_core::{ - components::{children, draw_shape}, + components::draw_shape, + hierarchy::OrderedDfsIterator, layout::cache::LayoutUpdate, stored::{self, Store}, Frame, @@ -108,6 +108,7 @@ pub(crate) struct DrawCommand { pub(crate) mesh: Arc, /// TODO: generate inside renderer pub(crate) index_count: u32, + clip_mask: (UVec2, UVec2), } /// Compatible draw commands are given an instance in the object buffer and merged together @@ -275,21 +276,19 @@ impl MainRenderer { puffin::profile_scope!("create_draw_commands"); let query = DrawQuery::new(); - let commands = RendererIter { - world: &frame.world, - stack: smallvec![frame.world.entity(self.root).unwrap()], - } - .filter_map(|entity| { - let mut query = entity.query(&query); - let item = query.get()?; + let commands = + OrderedDfsIterator::new(&frame.world, frame.world.entity(self.root).unwrap()) + .filter_map(|entity| { + let mut query = entity.query(&query); + let item = query.get()?; - Some((item.draw_cmd.clone(), *item.object_data)) - }) - .chain( - self.debug_renderer - .iter() - .flat_map(|v| v.draw_commands().iter().cloned()), - ); + Some((item.draw_cmd.clone(), *item.object_data)) + }) + .chain( + self.debug_renderer + .iter() + .flat_map(|v| v.draw_commands().iter().cloned()), + ); self.commands.clear(); self.object_data.clear(); @@ -324,6 +323,14 @@ impl MainRenderer { let shader = &self.store.shaders[shader]; let bind_group = &self.store.bind_groups[bind_group]; + let (mask_min, mask_max) = cmd.draw_cmd.clip_mask; + tracing::info!(%mask_min, %mask_max); + render_pass.set_scissor_rect( + mask_min.x, + mask_min.y, + mask_max.x - mask_min.x, + mask_max.y - mask_min.y, + ); render_pass.set_pipeline(shader.pipeline()); render_pass.set_bind_group(0, &ctx.globals_bind_group, &[]); @@ -394,29 +401,6 @@ pub(crate) struct ObjectData { pub(crate) color: Vec4, } -struct RendererIter<'a> { - world: &'a World, - // queue: VecDeque>, - stack: SmallVec<[EntityRef<'a>; 16]>, -} - -impl<'a> Iterator for RendererIter<'a> { - type Item = EntityRef<'a>; - - fn next(&mut self) -> Option { - let entity = self.stack.pop()?; - if let Ok(children) = entity.get(children()) { - self.stack.extend( - children - .iter() - .rev() - .map(|&id| self.world.entity(id).unwrap()), - ); - } - - Some(entity) - } -} #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] #[repr(C)] /// TODO: move to main renderer diff --git a/violet-wgpu/src/renderer/rect_renderer.rs b/violet-wgpu/src/renderer/rect_renderer.rs index 915eafa..2871190 100644 --- a/violet-wgpu/src/renderer/rect_renderer.rs +++ b/violet-wgpu/src/renderer/rect_renderer.rs @@ -12,7 +12,7 @@ use wgpu::{BindGroup, BindGroupLayout, SamplerDescriptor, ShaderStages, TextureF use violet_core::{ assets::{map::HandleMap, Asset, AssetCache, AssetKey}, - components::{anchor, color, draw_shape, image, rotation, screen_rect}, + components::{anchor, color, draw_shape, image, rect, screen_clip_mask, screen_transform}, shape::{self, shape_rectangle}, stored::{self, WeakHandle}, unit::Unit, @@ -63,8 +63,10 @@ impl AssetKey for ImageFromColor { #[derive(Fetch)] struct RectObjectQuery { - screen_rect: Component, - rotation: OptOr, f32>, + transform: Component, + rect: Component, + // screen_rect: Component, + // rotation: OptOr, f32>, anchor: OptOr>, Unit>, // pos: Component, // local_pos: Component, @@ -75,8 +77,10 @@ struct RectObjectQuery { impl RectObjectQuery { fn new() -> Self { Self { - screen_rect: screen_rect(), - rotation: rotation().opt_or(0.0), + // screen_rect: screen_rect(), + // rotation: rotation().opt_or(0.0), + rect: rect(), + transform: screen_transform(), anchor: anchor().opt_or_default(), object_data: object_data().as_mut(), color: color().opt_or(Srgba::new(1.0, 1.0, 1.0, 1.0)), @@ -91,6 +95,7 @@ struct RectDrawQuery { id: EntityIds, image: Opt>>, shape: Component<()>, + clip_mask: Component, } impl RectDrawQuery { @@ -99,6 +104,7 @@ impl RectDrawQuery { id: entity_ids(), image: image().opt(), shape: draw_shape(shape::shape_rectangle()), + clip_mask: screen_clip_mask(), } } } @@ -214,6 +220,7 @@ impl RectRenderer { shader: self.shader.clone(), mesh: self.mesh.clone(), index_count: 6, + clip_mask: (item.clip_mask.min.as_uvec2(), item.clip_mask.max.as_uvec2()), }, ); }); @@ -228,20 +235,19 @@ impl RectRenderer { .borrow(&frame.world) .iter() .for_each(|item| { - tracing::debug!(color=%srgba_to_vec4(*item.color), ?item.screen_rect); - let rect = item.screen_rect.align_to_grid(); + let rect = item.rect.align_to_grid(); // if rect.size().x < 0.01 || rect.size().y < 0.01 { // tracing::warn!("rect too small to render"); // return; // } - let anchor = item.anchor.resolve(rect.size()).extend(0.0); - - let model_matrix = Mat4::from_translation(rect.pos().extend(0.1) + anchor) - * Mat4::from_rotation_z(*item.rotation) - * Mat4::from_translation(-anchor) - * Mat4::from_scale(rect.size().extend(1.0)); + let model_matrix = *item.transform + * Mat4::from_scale_rotation_translation( + rect.size().extend(1.0), + Quat::IDENTITY, + rect.pos().extend(0.0), + ); *item.object_data = ObjectData { model_matrix, diff --git a/violet-wgpu/src/renderer/text_renderer.rs b/violet-wgpu/src/renderer/text_renderer.rs index 09701d4..8e83fb8 100644 --- a/violet-wgpu/src/renderer/text_renderer.rs +++ b/violet-wgpu/src/renderer/text_renderer.rs @@ -16,7 +16,8 @@ use wgpu::{BindGroup, BindGroupLayout, Sampler, SamplerDescriptor, ShaderStages, use violet_core::{ assets::AssetCache, components::{ - color, draw_shape, font_size, layout_bounds, layout_glyphs, rect, screen_position, text, + color, draw_shape, font_size, layout_bounds, layout_glyphs, rect, screen_clip_mask, + screen_transform, text, }, shape::shape_text, stored::{self, Handle}, @@ -43,7 +44,7 @@ use super::{DrawCommand, ObjectData, RendererStore}; struct ObjectQuery { draw_shape: With, rect: Component, - pos: Component, + transform: Component, object_data: Mutable, color: OptOr, Srgba>, } @@ -53,7 +54,7 @@ impl ObjectQuery { Self { draw_shape: draw_shape(shape_text()).with(), rect: rect(), - pos: screen_position(), + transform: screen_transform(), object_data: object_data().as_mut(), color: color().opt_or(Srgba::new(1.0, 1.0, 1.0, 1.0)), } @@ -285,8 +286,10 @@ pub(crate) struct TextMeshQuery { layout_bounds: Component, #[fetch(ignore)] font_size: OptOr, f32>, - #[fetch(ignore)] + // #[fetch(ignore)] layout_glyphs: Opt>, + + clip_mask: Component, } impl TextMeshQuery { @@ -301,6 +304,7 @@ impl TextMeshQuery { font_size: font_size().opt_or(16.0), state: text_buffer_state().as_mut(), layout_glyphs: layout_glyphs().as_mut().opt(), + clip_mask: screen_clip_mask(), } } } @@ -392,6 +396,7 @@ impl TextRenderer { shader: self.mesh_generator.shader.clone(), mesh: text_mesh.clone(), index_count, + clip_mask: (item.clip_mask.min.as_uvec2(), item.clip_mask.max.as_uvec2()), }, ); @@ -409,12 +414,8 @@ impl TextRenderer { .borrow(&frame.world) .iter() .for_each(|item| { - let rect = item.rect.translate(*item.pos).align_to_grid(); - let model_matrix = Mat4::from_scale_rotation_translation( - Vec3::ONE, - Quat::IDENTITY, - rect.pos().extend(0.1), - ); + let rect = item.rect.align_to_grid(); + let model_matrix = *item.transform; *item.object_data = ObjectData { model_matrix,