Skip to main content

microui_redux/
layout.rs

1//
2// Copyright 2022-Present (c) Raja Lehtihet & Wael El Oraiby
3//
4// Redistribution and use in source and binary forms, with or without
5// modification, are permitted provided that the following conditions are met:
6//
7// 1. Redistributions of source code must retain the above copyright notice,
8// this list of conditions and the following disclaimer.
9//
10// 2. Redistributions in binary form must reproduce the above copyright notice,
11// this list of conditions and the following disclaimer in the documentation
12// and/or other materials provided with the distribution.
13//
14// 3. Neither the name of the copyright holder nor the names of its contributors
15// may be used to endorse or promote products derived from this software without
16// specific prior written permission.
17//
18// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
22// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28// POSSIBILITY OF SUCH DAMAGE.
29//
30// -----------------------------------------------------------------------------
31// Ported to rust from https://github.com/rxi/microui/ and the original license
32//
33// Copyright (c) 2020 rxi
34//
35// Permission is hereby granted, free of charge, to any person obtaining a copy
36// of this software and associated documentation files (the "Software"), to
37// deal in the Software without restriction, including without limitation the
38// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
39// sell copies of the Software, and to permit persons to whom the Software is
40// furnished to do so, subject to the following conditions:
41//
42// The above copyright notice and this permission notice shall be included in
43// all copies or substantial portions of the Software.
44//
45// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
46// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
47// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
48// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
49// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
50// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
51// IN THE SOFTWARE.
52//
53use super::*;
54
55// Layout internals follow a two-layer model:
56// 1) `LayoutEngine` owns scope stack, coordinate transforms, and content extents.
57// 2) `LayoutFlow` implementations (`RowFlow`, `StackFlow`) decide how local cells are emitted.
58//
59// This keeps scroll/extent bookkeeping centralized while allowing specialized placement logic.
60
61/// Describes how a layout dimension should be resolved.
62#[derive(Copy, Clone, Debug, PartialEq, Eq)]
63/// Size policy used by rows and columns when resolving cells.
64pub enum SizePolicy {
65    /// Uses the default cell size defined by the style.
66    Auto,
67    /// Reserves a fixed number of pixels.
68    Fixed(i32),
69    /// Consumes the remaining space with an optional margin.
70    Remainder(i32),
71}
72
73impl SizePolicy {
74    fn resolve(self, default_size: i32, available_space: i32) -> i32 {
75        let resolved = match self {
76            SizePolicy::Auto => default_size,
77            SizePolicy::Fixed(value) => value,
78            SizePolicy::Remainder(margin) => available_space.saturating_sub(margin),
79        };
80        resolved.max(0)
81    }
82}
83
84impl Default for SizePolicy {
85    fn default() -> Self {
86        SizePolicy::Auto
87    }
88}
89
90/// Direction used by stack flows when emitting vertical cells.
91#[derive(Copy, Clone, Debug, PartialEq, Eq)]
92pub enum StackDirection {
93    /// Place cells from the current row start downward.
94    TopToBottom,
95    /// Place cells from the bottom of the current scope upward.
96    BottomToTop,
97}
98
99impl Default for StackDirection {
100    fn default() -> Self {
101        Self::TopToBottom
102    }
103}
104
105#[derive(Clone, Default)]
106struct ScopeState {
107    // Scope rectangle expressed in local space (already offset by scroll).
108    body: Recti,
109    // Current cursor in local coordinates.
110    cursor: Vec2i,
111    // Max absolute extent reached by generated cells (for scroll/content sizing).
112    max: Option<Vec2i>,
113    // Y coordinate where the next logical line should start.
114    next_row: i32,
115    // Horizontal indentation applied to the scope.
116    indent: i32,
117}
118
119impl ScopeState {
120    // Reset cursor to the start of the next line while preserving active indentation.
121    fn reset_cursor_for_next_row(&mut self) {
122        self.cursor = vec2(self.indent, self.next_row);
123    }
124}
125
126#[derive(Copy, Clone)]
127struct ResolveCtx {
128    // Global inter-cell spacing from style.
129    spacing: i32,
130    // Width fallback after preferred-size resolution.
131    default_width: i32,
132    // Height fallback after preferred-size resolution.
133    default_height: i32,
134}
135
136trait LayoutFlow {
137    // Produces the next local cell and advances scope-local cursors/state.
138    fn next_local(&mut self, scope: &mut ScopeState, ctx: ResolveCtx) -> Recti;
139}
140
141#[derive(Clone, Default)]
142struct RowFlow {
143    // Width policy for each slot in the active row pattern.
144    widths: Vec<SizePolicy>,
145    // Height policy shared by all cells in the row pattern.
146    height: SizePolicy,
147    // Current slot index in `widths`.
148    item_index: usize,
149}
150
151impl RowFlow {
152    fn new(widths: &[SizePolicy], height: SizePolicy) -> Self {
153        Self {
154            widths: widths.to_vec(),
155            height,
156            item_index: 0,
157        }
158    }
159
160    fn apply_template(&mut self, widths: Vec<SizePolicy>, height: SizePolicy) {
161        self.widths = widths;
162        self.height = height;
163    }
164}
165
166impl LayoutFlow for RowFlow {
167    fn next_local(&mut self, scope: &mut ScopeState, ctx: ResolveCtx) -> Recti {
168        // Once all row slots are consumed, wrap to the next line and restart the pattern.
169        let row_len = self.widths.len();
170        if self.item_index == row_len {
171            self.item_index = 0;
172            scope.reset_cursor_for_next_row();
173        }
174
175        // Empty width patterns are treated as a single Auto slot.
176        let width_policy = if self.widths.is_empty() {
177            SizePolicy::Auto
178        } else {
179            self.widths.get(self.item_index).copied().unwrap_or(SizePolicy::Auto)
180        };
181
182        let x = scope.cursor.x;
183        let y = scope.cursor.y;
184
185        // Resolve dimensions from policy + remaining space inside scope bounds.
186        let available_width = scope.body.width.saturating_sub(x);
187        let available_height = scope.body.height.saturating_sub(y);
188        let width = width_policy.resolve(ctx.default_width, available_width);
189        let height = self.height.resolve(ctx.default_height, available_height);
190
191        if self.item_index < self.widths.len() {
192            self.item_index += 1;
193        }
194
195        // Advance cursor to the right and grow the next-line marker by the tallest seen cell.
196        scope.cursor.x = scope.cursor.x.saturating_add(width).saturating_add(ctx.spacing);
197        let line_end = y.saturating_add(height).saturating_add(ctx.spacing);
198        scope.next_row = max(scope.next_row, line_end);
199
200        rect(x, y, width, height)
201    }
202}
203
204#[derive(Clone)]
205struct StackFlow {
206    // Width policy used for every stacked item.
207    width: SizePolicy,
208    // Height policy used for every stacked item.
209    height: SizePolicy,
210    // Vertical direction for cell emission.
211    direction: StackDirection,
212    // Offset consumed from the stack anchor (used by bottom-up stacks).
213    offset: i32,
214}
215
216impl Default for StackFlow {
217    fn default() -> Self {
218        Self {
219            width: SizePolicy::Remainder(0),
220            height: SizePolicy::Auto,
221            direction: StackDirection::TopToBottom,
222            offset: 0,
223        }
224    }
225}
226
227impl StackFlow {
228    fn new(width: SizePolicy, height: SizePolicy, direction: StackDirection) -> Self {
229        Self { width, height, direction, offset: 0 }
230    }
231
232    fn apply_template(&mut self, width: SizePolicy, height: SizePolicy, direction: StackDirection) {
233        self.width = width;
234        self.height = height;
235        self.direction = direction;
236        self.offset = 0;
237    }
238}
239
240impl LayoutFlow for StackFlow {
241    fn next_local(&mut self, scope: &mut ScopeState, ctx: ResolveCtx) -> Recti {
242        let x = scope.indent;
243        let available_width = scope.body.width.saturating_sub(x);
244        let width = self.width.resolve(ctx.default_width, available_width);
245
246        match self.direction {
247            StackDirection::TopToBottom => {
248                // Top-down stacks continue from the scope's row cursor.
249                let y = scope.next_row;
250                let available_height = scope.body.height.saturating_sub(y);
251                let height = self.height.resolve(ctx.default_height, available_height);
252
253                // Move directly to the next stacked row.
254                let next = y.saturating_add(height).saturating_add(ctx.spacing);
255                scope.next_row = next;
256                scope.cursor = vec2(scope.indent, next);
257
258                rect(x, y, width, height)
259            }
260            StackDirection::BottomToTop => {
261                // Bottom-up stacks are anchored to the scope bottom and use local offset.
262                let available_height = scope.body.height.saturating_sub(self.offset);
263                let height = self.height.resolve(ctx.default_height, available_height);
264                let y = scope.body.height.saturating_sub(self.offset).saturating_sub(height);
265                self.offset = self.offset.saturating_add(height).saturating_add(ctx.spacing);
266                rect(x, y, width, height)
267            }
268        }
269    }
270}
271
272#[derive(Clone)]
273enum FlowState {
274    // Repeating row pattern with N slots.
275    Row(RowFlow),
276    // One-cell-per-line vertical stack.
277    Stack(StackFlow),
278}
279
280impl Default for FlowState {
281    fn default() -> Self {
282        FlowState::Row(RowFlow::new(&[SizePolicy::Auto], SizePolicy::Auto))
283    }
284}
285
286impl FlowState {
287    // Store a flow as a lightweight template so scoped overrides can be restored later.
288    fn as_template(&self) -> FlowTemplate {
289        match self {
290            FlowState::Row(row) => FlowTemplate::Row {
291                widths: row.widths.clone(),
292                height: row.height,
293            },
294            FlowState::Stack(stack) => FlowTemplate::Stack {
295                width: stack.width,
296                height: stack.height,
297                direction: stack.direction,
298            },
299        }
300    }
301
302    fn apply_template(&mut self, template: FlowTemplate) {
303        match template {
304            FlowTemplate::Row { widths, height } => match self {
305                FlowState::Row(row) => row.apply_template(widths, height),
306                _ => {
307                    *self = FlowState::Row(RowFlow::new(widths.as_slice(), height));
308                }
309            },
310            FlowTemplate::Stack { width, height, direction } => match self {
311                FlowState::Stack(stack) => stack.apply_template(width, height, direction),
312                _ => {
313                    *self = FlowState::Stack(StackFlow::new(width, height, direction));
314                }
315            },
316        }
317    }
318
319    // Delegate cell generation to the active flow implementation.
320    fn next_local(&mut self, scope: &mut ScopeState, ctx: ResolveCtx) -> Recti {
321        match self {
322            FlowState::Row(flow) => flow.next_local(scope, ctx),
323            FlowState::Stack(flow) => flow.next_local(scope, ctx),
324        }
325    }
326}
327
328#[derive(Clone)]
329struct LayoutFrame {
330    // Coordinates/cursors for one nested layout scope.
331    scope: ScopeState,
332    // Placement logic used for this scope.
333    flow: FlowState,
334}
335
336impl LayoutFrame {
337    fn new(body: Recti, scroll: Vec2i) -> Self {
338        Self {
339            scope: ScopeState {
340                // Scope body is shifted by scroll so local coordinates remain stable while content moves.
341                body: rect(body.x - scroll.x, body.y - scroll.y, body.width, body.height),
342                cursor: vec2(0, 0),
343                max: None,
344                next_row: 0,
345                indent: 0,
346            },
347            flow: FlowState::default(),
348        }
349    }
350}
351
352#[derive(Clone, Default)]
353pub(crate) struct LayoutEngine {
354    // Style snapshot used by resolution rules (spacing/default widths/padding fallbacks).
355    pub style: Style,
356    // Last emitted absolute rectangle.
357    pub last_rect: Recti,
358    // Default control height seeded by container setup.
359    default_cell_height: i32,
360    // Nested scope stack (window body, columns, etc.).
361    stack: Vec<LayoutFrame>,
362}
363
364impl LayoutEngine {
365    // Pushes a scope with an explicit flow (used by reset/column).
366    fn push_scope_with_flow(&mut self, body: Recti, scroll: Vec2i, flow: FlowState) {
367        let mut frame = LayoutFrame::new(body, scroll);
368        frame.flow = flow;
369        self.stack.push(frame);
370    }
371
372    fn top(&self) -> &LayoutFrame {
373        self.stack.last().expect("Layout stack should never be empty when accessed")
374    }
375
376    fn top_mut(&mut self) -> &mut LayoutFrame {
377        self.stack.last_mut().expect("Layout stack should never be empty when accessed")
378    }
379
380    fn fallback_dimensions(&self, preferred: Dimensioni) -> (i32, i32) {
381        let padding = self.style.padding;
382        // Width fallback mirrors legacy behavior: default width + horizontal padding.
383        let fallback_width = self.style.default_cell_width + padding * 2;
384        // Height fallback prefers container-provided default cell height, then padding-only fallback.
385        let base_height = if self.default_cell_height > 0 { self.default_cell_height } else { 0 };
386        let fallback_height = if base_height > 0 { base_height } else { padding * 2 };
387
388        let default_width = if preferred.width > 0 { preferred.width } else { fallback_width };
389        let default_height = if preferred.height > 0 { preferred.height } else { fallback_height };
390        (default_width, default_height)
391    }
392
393    pub fn reset(&mut self, body: Recti, scroll: Vec2i) {
394        self.stack.clear();
395        self.last_rect = Recti::default();
396        // Root scope starts with default row flow.
397        self.push_scope_with_flow(body, scroll, FlowState::default());
398    }
399
400    pub fn set_default_cell_height(&mut self, height: i32) {
401        self.default_cell_height = height.max(0);
402    }
403
404    pub fn current_body(&self) -> Recti {
405        self.top().scope.body
406    }
407
408    pub fn current_max(&self) -> Option<Vec2i> {
409        self.top().scope.max
410    }
411
412    pub fn pop_scope(&mut self) {
413        self.stack.pop();
414    }
415
416    pub fn adjust_indent(&mut self, delta: i32) {
417        self.top_mut().scope.indent += delta;
418    }
419
420    pub fn begin_column(&mut self) {
421        // A column is allocated from the parent as one cell, then becomes a nested scope.
422        let layout_rect = self.next();
423        self.push_scope_with_flow(layout_rect, vec2(0, 0), FlowState::Row(RowFlow::new(&[SizePolicy::Auto], SizePolicy::Auto)));
424    }
425
426    pub fn end_column(&mut self) {
427        let finished = self.stack.pop().expect("cannot end column without an active child layout");
428        let parent = self.top_mut();
429
430        // Merge child cursor/row extents back into parent-local space.
431        let child_position_x = finished.scope.cursor.x + finished.scope.body.x - parent.scope.body.x;
432        let child_next_row = finished.scope.next_row + finished.scope.body.y - parent.scope.body.y;
433
434        parent.scope.cursor.x = max(parent.scope.cursor.x, child_position_x);
435        parent.scope.next_row = max(parent.scope.next_row, child_next_row);
436
437        // Merge absolute max extents for content-size/scroll calculations.
438        match (&mut parent.scope.max, finished.scope.max) {
439            (None, None) => (),
440            (Some(_), None) => (),
441            (None, Some(m)) => parent.scope.max = Some(m),
442            (Some(am), Some(bm)) => {
443                parent.scope.max = Some(Vec2i::new(max(am.x, bm.x), max(am.y, bm.y)));
444            }
445        }
446    }
447
448    pub fn row(&mut self, widths: &[SizePolicy], height: SizePolicy) {
449        let frame = self.top_mut();
450        frame.flow = FlowState::Row(RowFlow::new(widths, height));
451        // Applying a new flow resets placement to the current line start.
452        frame.scope.reset_cursor_for_next_row();
453    }
454
455    pub fn stack(&mut self, width: SizePolicy, height: SizePolicy) {
456        self.stack_with_direction(width, height, StackDirection::TopToBottom);
457    }
458
459    pub fn stack_with_direction(&mut self, width: SizePolicy, height: SizePolicy, direction: StackDirection) {
460        let frame = self.top_mut();
461        frame.flow = FlowState::Stack(StackFlow::new(width, height, direction));
462        // Applying a new flow resets placement to the current line start.
463        frame.scope.reset_cursor_for_next_row();
464    }
465
466    pub(crate) fn snapshot_flow_state(&self) -> FlowSnapshot {
467        FlowSnapshot::from_layout(self.top())
468    }
469
470    pub(crate) fn restore_flow_state(&mut self, snapshot: FlowSnapshot) {
471        snapshot.apply(self.top_mut());
472    }
473
474    pub fn next(&mut self) -> Recti {
475        self.next_with_preferred(Dimensioni::new(0, 0))
476    }
477
478    pub fn next_with_preferred(&mut self, preferred: Dimensioni) -> Recti {
479        let spacing = self.style.spacing;
480        let (default_width, default_height) = self.fallback_dimensions(preferred);
481        let mut local = {
482            let frame = self.top_mut();
483            let ctx = ResolveCtx { spacing, default_width, default_height };
484            frame.flow.next_local(&mut frame.scope, ctx)
485        };
486
487        // Convert local cell coordinates into absolute container coordinates.
488        let origin = {
489            let frame = self.top();
490            vec2(frame.scope.body.x, frame.scope.body.y)
491        };
492
493        local.x += origin.x;
494        local.y += origin.y;
495
496        {
497            let frame = self.top_mut();
498            // Track absolute max extent reached by emitted content.
499            match frame.scope.max {
500                None => frame.scope.max = Some(Vec2i::new(local.x + local.width, local.y + local.height)),
501                Some(am) => {
502                    frame.scope.max = Some(Vec2i::new(max(am.x, local.x + local.width), max(am.y, local.y + local.height)));
503                }
504            }
505        }
506
507        self.last_rect = local;
508        self.last_rect
509    }
510}
511
512#[derive(Clone)]
513enum FlowTemplate {
514    // Snapshot for row flow configuration.
515    Row {
516        widths: Vec<SizePolicy>,
517        height: SizePolicy,
518    },
519    // Snapshot for stack flow configuration.
520    Stack {
521        width: SizePolicy,
522        height: SizePolicy,
523        direction: StackDirection,
524    },
525}
526
527pub(crate) struct FlowSnapshot {
528    // Captures active flow configuration for scoped overrides.
529    flow: FlowTemplate,
530}
531
532impl FlowSnapshot {
533    fn from_layout(layout: &LayoutFrame) -> Self {
534        Self { flow: layout.flow.as_template() }
535    }
536
537    fn apply(self, layout: &mut LayoutFrame) {
538        layout.flow.apply_template(self.flow);
539    }
540}
541
542pub(crate) type LayoutManager = LayoutEngine;
543
544#[cfg(test)]
545mod tests {
546    use super::*;
547
548    #[test]
549    fn layout_next_advances_row() {
550        let mut layout = LayoutManager::default();
551        layout.style = Style::default();
552        let body = rect(0, 0, 100, 100);
553        layout.reset(body, vec2(0, 0));
554        layout.set_default_cell_height(10);
555        layout.row(&[SizePolicy::Auto], SizePolicy::Auto);
556
557        let first = layout.next();
558        let second = layout.next();
559
560        let expected_width = layout.style.default_cell_width + layout.style.padding * 2;
561        assert_eq!(first.x, body.x);
562        assert_eq!(first.y, body.y);
563        assert_eq!(first.width, expected_width);
564        assert_eq!(first.height, 10);
565        assert_eq!(second.x, body.x);
566        assert_eq!(second.y, body.y + first.height + layout.style.spacing);
567    }
568
569    #[test]
570    fn layout_remainder_consumes_available_width() {
571        let mut layout = LayoutManager::default();
572        layout.style = Style::default();
573        let body = rect(0, 0, 120, 40);
574        layout.reset(body, vec2(0, 0));
575        layout.set_default_cell_height(10);
576        layout.row(&[SizePolicy::Remainder(0)], SizePolicy::Fixed(10));
577
578        let cell = layout.next();
579        assert_eq!(cell.width, body.width);
580        assert_eq!(cell.height, 10);
581    }
582
583    #[test]
584    fn stack_flow_uses_full_width_by_default() {
585        let mut layout = LayoutManager::default();
586        layout.style = Style::default();
587        let body = rect(0, 0, 120, 60);
588        layout.reset(body, vec2(0, 0));
589        layout.set_default_cell_height(10);
590        layout.stack(SizePolicy::Remainder(0), SizePolicy::Auto);
591
592        let first = layout.next();
593        let second = layout.next();
594
595        assert_eq!(first.width, body.width);
596        assert_eq!(second.y, first.y + first.height + layout.style.spacing);
597    }
598
599    #[test]
600    fn stack_flow_bottom_to_top_anchors_to_scope_bottom() {
601        let mut layout = LayoutManager::default();
602        layout.style = Style::default();
603        let body = rect(0, 0, 120, 60);
604        layout.reset(body, vec2(0, 0));
605        layout.set_default_cell_height(10);
606        layout.stack_with_direction(SizePolicy::Remainder(0), SizePolicy::Fixed(10), StackDirection::BottomToTop);
607
608        let first = layout.next();
609        let second = layout.next();
610
611        assert_eq!(first.width, body.width);
612        assert_eq!(first.y, body.y + body.height - 10);
613        assert_eq!(second.y, first.y - (10 + layout.style.spacing));
614    }
615}