diff --git a/crates/yakui-core/src/input/input_state.rs b/crates/yakui-core/src/input/input_state.rs index 82ee2755..da318e8e 100644 --- a/crates/yakui-core/src/input/input_state.rs +++ b/crates/yakui-core/src/input/input_state.rs @@ -507,7 +507,7 @@ fn hit_test(_dom: &Dom, layout: &LayoutDom, coords: Vec2, output: &mut Vec, - clip_stack: Vec, + clip_stack: Arena>, + current_clip_stack: Option, unscaled_viewport: Rect, scale_factor: f32, @@ -44,6 +45,9 @@ pub struct LayoutDomNode { /// What events the widget reported interest in. pub event_interest: EventInterest, + + /// Which clip stack the widget belongs in. + pub clip_stack_id: Index, } impl LayoutDom { @@ -51,7 +55,8 @@ impl LayoutDom { pub fn new() -> Self { Self { nodes: Arena::new(), - clip_stack: Vec::new(), + clip_stack: Arena::new(), + current_clip_stack: None, unscaled_viewport: Rect::ONE, scale_factor: 1.0, @@ -120,6 +125,7 @@ impl LayoutDom { log::debug!("LayoutDom::calculate_all()"); self.clip_stack.clear(); + self.current_clip_stack = None; self.interest_mouse.clear(); let constraints = Constraints::tight(self.viewport().size()); @@ -141,6 +147,7 @@ impl LayoutDom { constraints: Constraints, ) -> Vec2 { dom.enter(id); + let dom_node = dom.get(id).unwrap(); let context = LayoutContext { @@ -168,16 +175,24 @@ impl LayoutDom { self.interest_mouse.pop_layer(); } + if self.current_clip_stack.is_none() { + self.new_clip_stack(dom); + } + + // There should always be a currently active clip stack. + let clip_stack_id = self.current_clip_stack.unwrap(); + let clip_stack = self.clip_stack.get_mut(clip_stack_id).unwrap(); + // If the widget called enable_clipping() during layout, it will be on // top of the clip stack at this point. - let clipping_enabled = self.clip_stack.last() == Some(&id); + let clipping_enabled = clip_stack.last() == Some(&id); // If this node enabled clipping, the next node under that is the node // that clips this one. let clipped_by = if clipping_enabled { - self.clip_stack.iter().nth_back(2).copied() + clip_stack.iter().nth_back(2).copied() } else { - self.clip_stack.last().copied() + clip_stack.last().copied() }; self.nodes.insert_at( @@ -188,11 +203,12 @@ impl LayoutDom { new_layer, clipped_by, event_interest, + clip_stack_id, }, ); if clipping_enabled { - self.clip_stack.pop(); + clip_stack.pop(); } dom.exit(id); @@ -201,7 +217,18 @@ impl LayoutDom { /// Enables clipping for the currently active widget. pub fn enable_clipping(&mut self, dom: &Dom) { - self.clip_stack.push(dom.current()); + self.clip_stack + .get_mut(self.current_clip_stack.unwrap()) + .unwrap() + .push(dom.current()); + } + + /// Create a new clip stack for the currently active widget. + pub fn new_clip_stack(&mut self, dom: &Dom) { + self.current_clip_stack = Some(dom.current().index()); + let old = self.clip_stack.insert_at(dom.current().index(), Vec::new()); + debug_assert!(old.is_none(), "clip_stack id clashed"); + self.enable_clipping(dom); } /// Put this widget and its children into a new layer. diff --git a/crates/yakui-core/src/paint/paint_dom.rs b/crates/yakui-core/src/paint/paint_dom.rs index 0a568eac..350a6ed8 100644 --- a/crates/yakui-core/src/paint/paint_dom.rs +++ b/crates/yakui-core/src/paint/paint_dom.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use glam::Vec2; -use thunderdome::Arena; +use thunderdome::{Arena, Index}; use crate::dom::Dom; use crate::geometry::Rect; @@ -24,7 +24,8 @@ pub struct PaintDom { scale_factor: f32, layers: PaintLayers, - clip_stack: Vec, + clip_stack: Arena>, + current_clip_id: Option, } impl PaintDom { @@ -37,7 +38,8 @@ impl PaintDom { unscaled_viewport: Rect::ONE, scale_factor: 1.0, layers: PaintLayers::new(), - clip_stack: Vec::new(), + clip_stack: Arena::new(), + current_clip_id: None, } } @@ -45,6 +47,7 @@ impl PaintDom { pub fn start(&mut self) { self.texture_edits.clear(); self.clip_stack.clear(); + self.current_clip_id = None; } /// Returns the size of the surface that is being painted onto. @@ -73,12 +76,25 @@ impl PaintDom { profiling::scope!("PaintDom::paint"); let layout_node = layout.get(id).unwrap(); - if layout_node.clipping_enabled { - self.push_clip(layout_node.rect); - } + if layout_node.new_layer { self.layers.push(); } + if Some(layout_node.clip_stack_id) != self.current_clip_id { + println!( + "{:?}, {:?}", + self.current_clip_id, layout_node.clip_stack_id + ); + self.current_clip_id = Some(layout_node.clip_stack_id); + + if !self.clip_stack.contains(layout_node.clip_stack_id) { + self.clip_stack + .insert_at(layout_node.clip_stack_id, Vec::new()); + } + } + if layout_node.clipping_enabled { + self.push_clip(layout_node.rect); + } dom.enter(id); @@ -184,7 +200,10 @@ impl PaintDom { .current_mut() .expect("an active layer is required to call add_mesh"); - let current_clip = self.clip_stack.last().copied(); + let current_clip_id = self.current_clip_id.unwrap(); + let clip_stack = self.clip_stack.get(current_clip_id).unwrap(); + let current_clip = clip_stack.last().copied(); + let call = match layer.calls.last_mut() { Some(PaintCall::Yakui(call)) if call.texture == texture_id @@ -231,16 +250,27 @@ impl PaintDom { region.size() * self.scale_factor, ); - if let Some(previous) = self.clip_stack.last() { + let current_clip_id = self.current_clip_id.unwrap(); + let clip_stack = self.clip_stack.get_mut(current_clip_id).unwrap(); + + if let Some(previous) = clip_stack.last() { + println!("{previous:?}"); unscaled = unscaled.constrain(*previous); } - self.clip_stack.push(unscaled); + clip_stack.push(unscaled); } /// Pop the most recent clip region, restoring the previous clipping rect. fn pop_clip(&mut self) { - let top = self.clip_stack.pop(); + let current_clip_id = self.current_clip_id.unwrap(); + + let top = { + let clip_stack = self.clip_stack.get_mut(current_clip_id).unwrap(); + + clip_stack.pop() + }; + debug_assert!( top.is_some(), "cannot call pop_clip without a corresponding push_clip call" diff --git a/crates/yakui-widgets/src/widgets/layer.rs b/crates/yakui-widgets/src/widgets/layer.rs index e5287efc..9a9663a4 100644 --- a/crates/yakui-widgets/src/widgets/layer.rs +++ b/crates/yakui-widgets/src/widgets/layer.rs @@ -46,6 +46,7 @@ impl Widget for LayerWidget { fn layout(&self, mut ctx: LayoutContext<'_>, constraints: Constraints) -> Vec2 { ctx.layout.new_layer(ctx.dom); + ctx.layout.new_clip_stack(ctx.dom); let node = ctx.dom.get_current(); let mut size = Vec2::ZERO; diff --git a/crates/yakui/examples/dropdown.rs b/crates/yakui/examples/dropdown.rs index 09ef7ad1..a6d51118 100644 --- a/crates/yakui/examples/dropdown.rs +++ b/crates/yakui/examples/dropdown.rs @@ -3,51 +3,61 @@ #![allow(clippy::collapsible_if)] -use yakui::widgets::Layer; -use yakui::{align, button, column, reflow, use_state, widgets::Pad, Alignment, Dim2}; +use yakui::widgets::{Layer, Scrollable}; +use yakui::{button, column, reflow, use_state, Alignment, Dim2}; +use yakui::{constrained, row, Constraints, Vec2}; use yakui_core::Pivot; pub fn run() { let open = use_state(|| false); - let options = ["Hello", "World", "Foobar"]; + let options = ["Hello", "World", "Foobar", "Meow", "Woof"]; let selected = use_state(|| 0); - align(Alignment::TOP_LEFT, || { - column(|| { - if button("Upper Button").clicked { - println!("Upper button clicked"); - } + row(|| { + constrained(Constraints::loose(Vec2::new(f32::INFINITY, 50.0)), || { + Scrollable::vertical().show(|| { + column(|| { + if button("Upper Button").clicked { + println!("Upper button clicked"); + } - column(|| { - if button(options[selected.get()]).clicked { - open.modify(|x| !x); - } + column(|| { + if button(options[selected.get()]).clicked { + open.modify(|x| !x); + } - if open.get() { - Pad::ZERO.show(|| { - Layer::new().show(|| { + if open.get() { reflow(Alignment::BOTTOM_LEFT, Pivot::TOP_LEFT, Dim2::ZERO, || { - column(|| { - let current = selected.get(); - for (i, option) in options.iter().enumerate() { - if i != current { - if button(*option).clicked { - selected.set(i); - open.set(false); - } - } - } + Layer::new().show(|| { + constrained( + Constraints::loose(Vec2::new(f32::INFINITY, 80.0)), + || { + Scrollable::vertical().show(|| { + column(|| { + let current = selected.get(); + for (i, option) in options.iter().enumerate() { + if i != current { + if button(*option).clicked { + selected.set(i); + open.set(false); + } + } + } + }); + }); + }, + ); }); }); - }); + } }); - } + }); }); - - if button("Lower Button").clicked { - println!("Lower button clicked"); - } }); + + if button("Side Button").clicked { + println!("Side button clicked"); + } }); }