Skip to main content

slt/context/widgets_interactive/
collections.rs

1use super::*;
2
3impl Context {
4    /// Render children in a fixed grid with the given number of columns.
5    ///
6    /// Children are placed left-to-right, top-to-bottom. Each cell has equal
7    /// width (`area_width / cols`). Rows wrap automatically.
8    ///
9    /// # Example
10    ///
11    /// ```no_run
12    /// # slt::run(|ui: &mut slt::Context| {
13    /// ui.grid(3, |ui| {
14    ///     for i in 0..9 {
15    ///         ui.text(format!("Cell {i}"));
16    ///     }
17    /// });
18    /// # });
19    /// ```
20    pub fn grid(&mut self, cols: u32, f: impl FnOnce(&mut Context)) -> Response {
21        slt_assert(cols > 0, "grid() requires at least 1 column");
22        let interaction_id = self.next_interaction_id();
23        let border = self.theme.border;
24
25        self.commands
26            .push(Command::BeginContainer(Box::new(BeginContainerArgs {
27                direction: Direction::Column,
28                gap: 0,
29                align: Align::Start,
30                align_self: None,
31                justify: Justify::Start,
32                border: None,
33                border_sides: BorderSides::all(),
34                border_style: Style::new().fg(border),
35                bg_color: None,
36                padding: Padding::default(),
37                margin: Margin::default(),
38                constraints: Constraints::default(),
39                title: None,
40                grow: 0,
41                group_name: None,
42            })));
43
44        let children_start = self.commands.len();
45        f(self);
46        let child_commands: Vec<Command> = self.commands.drain(children_start..).collect();
47
48        let elements = collect_grid_elements(child_commands);
49
50        let cols = cols.max(1) as usize;
51        for row in elements.chunks(cols) {
52            self.skip_interaction_slot();
53            self.commands
54                .push(Command::BeginContainer(Box::new(BeginContainerArgs {
55                    direction: Direction::Row,
56                    gap: 0,
57                    align: Align::Start,
58                    align_self: None,
59                    justify: Justify::Start,
60                    border: None,
61                    border_sides: BorderSides::all(),
62                    border_style: Style::new().fg(border),
63                    bg_color: None,
64                    padding: Padding::default(),
65                    margin: Margin::default(),
66                    constraints: Constraints::default(),
67                    title: None,
68                    grow: 0,
69                    group_name: None,
70                })));
71
72            for element in row {
73                self.skip_interaction_slot();
74                self.commands
75                    .push(Command::BeginContainer(Box::new(BeginContainerArgs {
76                        direction: Direction::Column,
77                        gap: 0,
78                        align: Align::Start,
79                        align_self: None,
80                        justify: Justify::Start,
81                        border: None,
82                        border_sides: BorderSides::all(),
83                        border_style: Style::new().fg(border),
84                        bg_color: None,
85                        padding: Padding::default(),
86                        margin: Margin::default(),
87                        constraints: Constraints::default(),
88                        title: None,
89                        grow: 1,
90                        group_name: None,
91                    })));
92                self.commands.extend(element.iter().cloned());
93                self.commands.push(Command::EndContainer);
94            }
95
96            self.commands.push(Command::EndContainer);
97        }
98
99        self.commands.push(Command::EndContainer);
100        self.rollback.last_text_idx = None;
101
102        self.response_for(interaction_id)
103    }
104
105    /// Render children in a grid with per-column width specifications.
106    ///
107    /// The number of columns is determined by the length of `columns`. Children
108    /// are placed left-to-right, top-to-bottom, wrapping into rows
109    /// automatically.
110    ///
111    /// # Column specifications
112    ///
113    /// - [`GridColumn::Auto`] — equal-width flex column (same as `grid()`)
114    /// - [`GridColumn::Fixed(n)`](GridColumn::Fixed) — exactly `n` character cells wide
115    /// - [`GridColumn::Grow(w)`](GridColumn::Grow) — flexible with grow weight `w`
116    /// - [`GridColumn::Percent(p)`](GridColumn::Percent) — `p`% of the grid width
117    ///
118    /// # Example
119    ///
120    /// ```no_run
121    /// use slt::GridColumn;
122    /// # slt::run(|ui: &mut slt::Context| {
123    /// ui.grid_with(&[
124    ///     GridColumn::Fixed(8),
125    ///     GridColumn::Grow(1),
126    ///     GridColumn::Grow(1),
127    ///     GridColumn::Fixed(4),
128    /// ], |ui| {
129    ///     for i in 0..8 {
130    ///         ui.text(format!("Cell {i}"));
131    ///     }
132    /// });
133    /// # });
134    /// ```
135    pub fn grid_with(&mut self, columns: &[GridColumn], f: impl FnOnce(&mut Context)) -> Response {
136        let cols = columns.len().max(1);
137        let interaction_id = self.next_interaction_id();
138        let border = self.theme.border;
139
140        self.commands
141            .push(Command::BeginContainer(Box::new(BeginContainerArgs {
142                direction: Direction::Column,
143                gap: 0,
144                align: Align::Start,
145                align_self: None,
146                justify: Justify::Start,
147                border: None,
148                border_sides: BorderSides::all(),
149                border_style: Style::new().fg(border),
150                bg_color: None,
151                padding: Padding::default(),
152                margin: Margin::default(),
153                constraints: Constraints::default(),
154                title: None,
155                grow: 0,
156                group_name: None,
157            })));
158
159        let children_start = self.commands.len();
160        f(self);
161        let child_commands: Vec<Command> = self.commands.drain(children_start..).collect();
162
163        let elements = collect_grid_elements(child_commands);
164
165        for row in elements.chunks(cols) {
166            self.skip_interaction_slot();
167            self.commands
168                .push(Command::BeginContainer(Box::new(BeginContainerArgs {
169                    direction: Direction::Row,
170                    gap: 0,
171                    align: Align::Start,
172                    align_self: None,
173                    justify: Justify::Start,
174                    border: None,
175                    border_sides: BorderSides::all(),
176                    border_style: Style::new().fg(border),
177                    bg_color: None,
178                    padding: Padding::default(),
179                    margin: Margin::default(),
180                    constraints: Constraints::default(),
181                    title: None,
182                    grow: 0,
183                    group_name: None,
184                })));
185
186            for (col_idx, element) in row.iter().enumerate() {
187                let spec = columns.get(col_idx).copied().unwrap_or(GridColumn::Auto);
188                let (grow, constraints) = match spec {
189                    GridColumn::Auto => (1, Constraints::default()),
190                    GridColumn::Fixed(w) => (0, Constraints::default().w(w)),
191                    GridColumn::Grow(g) => (g, Constraints::default()),
192                    GridColumn::Percent(p) => (0, Constraints::default().w_pct(p)),
193                };
194
195                self.skip_interaction_slot();
196                self.commands
197                    .push(Command::BeginContainer(Box::new(BeginContainerArgs {
198                        direction: Direction::Column,
199                        gap: 0,
200                        align: Align::Start,
201                        align_self: None,
202                        justify: Justify::Start,
203                        border: None,
204                        border_sides: BorderSides::all(),
205                        border_style: Style::new().fg(border),
206                        bg_color: None,
207                        padding: Padding::default(),
208                        margin: Margin::default(),
209                        constraints,
210                        title: None,
211                        grow,
212                        group_name: None,
213                    })));
214                self.commands.extend(element.iter().cloned());
215                self.commands.push(Command::EndContainer);
216            }
217
218            self.commands.push(Command::EndContainer);
219        }
220
221        self.commands.push(Command::EndContainer);
222        self.rollback.last_text_idx = None;
223
224        self.response_for(interaction_id)
225    }
226
227    /// Render a selectable list. Handles Up/Down (and `k`/`j`) navigation when focused.
228    ///
229    /// The selected item is highlighted with the theme's primary color. If the
230    /// list is empty, nothing is rendered.
231    pub fn list(&mut self, state: &mut ListState) -> Response {
232        let colors = self.widget_theme.list;
233        self.list_colored(state, &colors)
234    }
235
236    /// Render a navigable list with custom widget colors.
237    pub fn list_colored(&mut self, state: &mut ListState, colors: &WidgetColors) -> Response {
238        let visible = state.visible_indices().to_vec();
239        if visible.is_empty() && state.items.is_empty() {
240            state.selected = 0;
241            return Response::none();
242        }
243
244        if !visible.is_empty() {
245            state.selected = state.selected.min(visible.len().saturating_sub(1));
246        }
247
248        let old_selected = state.selected;
249        let focused = self.register_focusable();
250        let (interaction_id, mut response) = self.begin_widget_interaction(focused);
251
252        if focused {
253            let mut consumed_indices = Vec::new();
254            for (i, key) in self.available_key_presses() {
255                match key.code {
256                    KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
257                        let _ = handle_vertical_nav(
258                            &mut state.selected,
259                            visible.len().saturating_sub(1),
260                            key.code.clone(),
261                        );
262                        consumed_indices.push(i);
263                    }
264                    _ => {}
265                }
266            }
267            self.consume_indices(consumed_indices);
268        }
269
270        if let Some((rect, clicks)) = self.left_clicks_for_interaction(interaction_id) {
271            let mut consumed = Vec::new();
272            for (i, mouse) in clicks {
273                let clicked_idx = (mouse.y - rect.y) as usize;
274                if clicked_idx < visible.len() {
275                    state.selected = clicked_idx;
276                    consumed.push(i);
277                }
278            }
279            self.consume_indices(consumed);
280        }
281
282        self.commands
283            .push(Command::BeginContainer(Box::new(BeginContainerArgs {
284                direction: Direction::Column,
285                gap: 0,
286                align: Align::Start,
287                align_self: None,
288                justify: Justify::Start,
289                border: None,
290                border_sides: BorderSides::all(),
291                border_style: Style::new().fg(colors.border.unwrap_or(self.theme.border)),
292                bg_color: None,
293                padding: Padding::default(),
294                margin: Margin::default(),
295                constraints: Constraints::default(),
296                title: None,
297                grow: 0,
298                group_name: None,
299            })));
300
301        for (view_idx, &item_idx) in visible.iter().enumerate() {
302            let item = &state.items[item_idx];
303            if view_idx == state.selected {
304                let mut selected_style = Style::new()
305                    .bg(colors.accent.unwrap_or(self.theme.selected_bg))
306                    .fg(colors.fg.unwrap_or(self.theme.selected_fg));
307                if focused {
308                    selected_style = selected_style.bold();
309                }
310                let mut row = String::with_capacity(2 + item.len());
311                row.push_str("▸ ");
312                row.push_str(item);
313                self.styled(row, selected_style);
314            } else {
315                let mut row = String::with_capacity(2 + item.len());
316                row.push_str("  ");
317                row.push_str(item);
318                self.styled(row, Style::new().fg(colors.fg.unwrap_or(self.theme.text)));
319            }
320        }
321
322        self.commands.push(Command::EndContainer);
323        self.rollback.last_text_idx = None;
324
325        response.changed = state.selected != old_selected;
326        response
327    }
328
329    /// Render a calendar date picker with month navigation.
330    ///
331    /// # Keybindings (when focused)
332    ///
333    /// | Key | Action |
334    /// |-----|--------|
335    /// | `Left` / `h` | Previous day |
336    /// | `Right` / `l` | Next day |
337    /// | `Up` | Previous week (−7 days) |
338    /// | `Down` | Next week (+7 days) |
339    /// | `[` | Previous month |
340    /// | `]` | Next month |
341    /// | `Enter` / `Space` | Select cursor day |
342    ///
343    /// `h`/`l` follow vim convention (cursor by one day). Use `[`/`]` for
344    /// month navigation. Mouse clicks on the title row navigate months and
345    /// clicks inside the day grid select that day.
346    pub fn calendar(&mut self, state: &mut CalendarState) -> Response {
347        let focused = self.register_focusable();
348        let (interaction_id, mut response) = self.begin_widget_interaction(focused);
349
350        let month_days = CalendarState::days_in_month(state.year, state.month);
351        state.cursor_day = state.cursor_day.clamp(1, month_days);
352        if let Some(day) = state.selected_day {
353            state.selected_day = Some(day.min(month_days));
354        }
355        let old_selected = state.selected_day;
356
357        if focused {
358            let mut consumed_indices = Vec::new();
359            for (i, key) in self.available_key_presses() {
360                match key.code {
361                    KeyCode::Left => {
362                        calendar_move_cursor_by_days(state, -1);
363                        consumed_indices.push(i);
364                    }
365                    KeyCode::Right => {
366                        calendar_move_cursor_by_days(state, 1);
367                        consumed_indices.push(i);
368                    }
369                    KeyCode::Up => {
370                        calendar_move_cursor_by_days(state, -7);
371                        consumed_indices.push(i);
372                    }
373                    KeyCode::Down => {
374                        calendar_move_cursor_by_days(state, 7);
375                        consumed_indices.push(i);
376                    }
377                    KeyCode::Char('h') => {
378                        calendar_move_cursor_by_days(state, -1);
379                        consumed_indices.push(i);
380                    }
381                    KeyCode::Char('l') => {
382                        calendar_move_cursor_by_days(state, 1);
383                        consumed_indices.push(i);
384                    }
385                    KeyCode::Char('[') => {
386                        state.prev_month();
387                        consumed_indices.push(i);
388                    }
389                    KeyCode::Char(']') => {
390                        state.next_month();
391                        consumed_indices.push(i);
392                    }
393                    KeyCode::Enter | KeyCode::Char(' ') => {
394                        state.selected_day = Some(state.cursor_day);
395                        consumed_indices.push(i);
396                    }
397                    _ => {}
398                }
399            }
400            self.consume_indices(consumed_indices);
401        }
402
403        if let Some((rect, clicks)) = self.left_clicks_for_interaction(interaction_id) {
404            let mut consumed = Vec::new();
405            for (i, mouse) in clicks {
406                let rel_x = mouse.x.saturating_sub(rect.x);
407                let rel_y = mouse.y.saturating_sub(rect.y);
408                if rel_y == 0 {
409                    if rel_x <= 2 {
410                        state.prev_month();
411                        consumed.push(i);
412                        continue;
413                    }
414                    if rel_x + 3 >= rect.width {
415                        state.next_month();
416                        consumed.push(i);
417                        continue;
418                    }
419                }
420
421                if !(2..8).contains(&rel_y) {
422                    continue;
423                }
424                if rel_x >= 21 {
425                    continue;
426                }
427
428                let week = rel_y - 2;
429                let col = rel_x / 3;
430                let day_index = week * 7 + col;
431                let first = CalendarState::first_weekday(state.year, state.month);
432                let days = CalendarState::days_in_month(state.year, state.month);
433                if day_index < first {
434                    continue;
435                }
436                let day = day_index - first + 1;
437                if day == 0 || day > days {
438                    continue;
439                }
440                state.cursor_day = day;
441                state.selected_day = Some(day);
442                consumed.push(i);
443            }
444            self.consume_indices(consumed);
445        }
446
447        let title = {
448            let month_name = calendar_month_name(state.month);
449            let mut s = String::with_capacity(16);
450            s.push_str(&state.year.to_string());
451            s.push(' ');
452            s.push_str(month_name);
453            s
454        };
455
456        self.commands
457            .push(Command::BeginContainer(Box::new(BeginContainerArgs {
458                direction: Direction::Column,
459                gap: 0,
460                align: Align::Start,
461                align_self: None,
462                justify: Justify::Start,
463                border: None,
464                border_sides: BorderSides::all(),
465                border_style: Style::new().fg(self.theme.border),
466                bg_color: None,
467                padding: Padding::default(),
468                margin: Margin::default(),
469                constraints: Constraints::default(),
470                title: None,
471                grow: 0,
472                group_name: None,
473            })));
474
475        let cal_gap = self.theme.spacing.xs();
476        self.commands
477            .push(Command::BeginContainer(Box::new(BeginContainerArgs {
478                direction: Direction::Row,
479                gap: cal_gap,
480                align: Align::Start,
481                align_self: None,
482                justify: Justify::Start,
483                border: None,
484                border_sides: BorderSides::all(),
485                border_style: Style::new().fg(self.theme.border),
486                bg_color: None,
487                padding: Padding::default(),
488                margin: Margin::default(),
489                constraints: Constraints::default(),
490                title: None,
491                grow: 0,
492                group_name: None,
493            })));
494        self.styled("◀", Style::new().fg(self.theme.text));
495        self.styled(title, Style::new().bold().fg(self.theme.text));
496        self.styled("▶", Style::new().fg(self.theme.text));
497        self.commands.push(Command::EndContainer);
498
499        self.commands
500            .push(Command::BeginContainer(Box::new(BeginContainerArgs {
501                direction: Direction::Row,
502                gap: 0,
503                align: Align::Start,
504                align_self: None,
505                justify: Justify::Start,
506                border: None,
507                border_sides: BorderSides::all(),
508                border_style: Style::new().fg(self.theme.border),
509                bg_color: None,
510                padding: Padding::default(),
511                margin: Margin::default(),
512                constraints: Constraints::default(),
513                title: None,
514                grow: 0,
515                group_name: None,
516            })));
517        for wd in ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"] {
518            self.styled(
519                format!("{wd:>2} "),
520                Style::new().fg(self.theme.text_dim).bold(),
521            );
522        }
523        self.commands.push(Command::EndContainer);
524
525        let first = CalendarState::first_weekday(state.year, state.month);
526        let days = CalendarState::days_in_month(state.year, state.month);
527        for week in 0..6_u32 {
528            self.commands
529                .push(Command::BeginContainer(Box::new(BeginContainerArgs {
530                    direction: Direction::Row,
531                    gap: 0,
532                    align: Align::Start,
533                    align_self: None,
534                    justify: Justify::Start,
535                    border: None,
536                    border_sides: BorderSides::all(),
537                    border_style: Style::new().fg(self.theme.border),
538                    bg_color: None,
539                    padding: Padding::default(),
540                    margin: Margin::default(),
541                    constraints: Constraints::default(),
542                    title: None,
543                    grow: 0,
544                    group_name: None,
545                })));
546
547            for col in 0..7_u32 {
548                let idx = week * 7 + col;
549                if idx < first || idx >= first + days {
550                    self.styled("   ", Style::new().fg(self.theme.text_dim));
551                    continue;
552                }
553                let day = idx - first + 1;
554                let text = format!("{day:>2} ");
555                let style = if state.selected_day == Some(day) {
556                    Style::new()
557                        .bg(self.theme.selected_bg)
558                        .fg(self.theme.selected_fg)
559                } else if state.cursor_day == day {
560                    Style::new().fg(self.theme.primary).bold()
561                } else {
562                    Style::new().fg(self.theme.text)
563                };
564                self.styled(text, style);
565            }
566
567            self.commands.push(Command::EndContainer);
568        }
569
570        self.commands.push(Command::EndContainer);
571        self.rollback.last_text_idx = None;
572        response.changed = state.selected_day != old_selected;
573        response
574    }
575
576    /// Render a file system browser with directory navigation.
577    pub fn file_picker(&mut self, state: &mut FilePickerState) -> Response {
578        if state.dirty {
579            state.refresh();
580        }
581        if !state.entries.is_empty() {
582            state.selected = state.selected.min(state.entries.len().saturating_sub(1));
583        }
584
585        let focused = self.register_focusable();
586        let (_interaction_id, mut response) = self.begin_widget_interaction(focused);
587        let mut file_selected = false;
588
589        if focused {
590            let mut consumed_indices = Vec::new();
591            for (i, key) in self.available_key_presses() {
592                match key.code {
593                    KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
594                        if !state.entries.is_empty() {
595                            let _ = handle_vertical_nav(
596                                &mut state.selected,
597                                state.entries.len().saturating_sub(1),
598                                key.code.clone(),
599                            );
600                        }
601                        consumed_indices.push(i);
602                    }
603                    KeyCode::Enter => {
604                        if let Some(entry) = state.entries.get(state.selected).cloned() {
605                            if entry.is_dir {
606                                state.current_dir = entry.path;
607                                state.selected = 0;
608                                state.selected_file = None;
609                                state.dirty = true;
610                            } else {
611                                state.selected_file = Some(entry.path);
612                                file_selected = true;
613                            }
614                        }
615                        consumed_indices.push(i);
616                    }
617                    KeyCode::Backspace => {
618                        if let Some(parent) = state.current_dir.parent().map(|p| p.to_path_buf()) {
619                            state.current_dir = parent;
620                            state.selected = 0;
621                            state.selected_file = None;
622                            state.dirty = true;
623                        }
624                        consumed_indices.push(i);
625                    }
626                    KeyCode::Char('h') => {
627                        state.show_hidden = !state.show_hidden;
628                        state.selected = 0;
629                        state.dirty = true;
630                        consumed_indices.push(i);
631                    }
632                    KeyCode::Esc => {
633                        state.selected_file = None;
634                        consumed_indices.push(i);
635                    }
636                    _ => {}
637                }
638            }
639            self.consume_indices(consumed_indices);
640        }
641
642        if state.dirty {
643            state.refresh();
644        }
645
646        self.commands
647            .push(Command::BeginContainer(Box::new(BeginContainerArgs {
648                direction: Direction::Column,
649                gap: 0,
650                align: Align::Start,
651                align_self: None,
652                justify: Justify::Start,
653                border: None,
654                border_sides: BorderSides::all(),
655                border_style: Style::new().fg(self.theme.border),
656                bg_color: None,
657                padding: Padding::default(),
658                margin: Margin::default(),
659                constraints: Constraints::default(),
660                title: None,
661                grow: 0,
662                group_name: None,
663            })));
664
665        let dir_text = {
666            let dir = state.current_dir.display().to_string();
667            let mut text = String::with_capacity(5 + dir.len());
668            text.push_str("Dir: ");
669            text.push_str(&dir);
670            text
671        };
672        self.styled(dir_text, Style::new().fg(self.theme.text_dim).dim());
673
674        if state.entries.is_empty() {
675            self.styled("(empty)", Style::new().fg(self.theme.text_dim).dim());
676        } else {
677            for (idx, entry) in state.entries.iter().enumerate() {
678                let icon = if entry.is_dir { "▸ " } else { "  " };
679                let row = if entry.is_dir {
680                    let mut row = String::with_capacity(icon.len() + entry.name.len());
681                    row.push_str(icon);
682                    row.push_str(&entry.name);
683                    row
684                } else {
685                    let size_text = entry.size.to_string();
686                    let mut row =
687                        String::with_capacity(icon.len() + entry.name.len() + size_text.len() + 4);
688                    row.push_str(icon);
689                    row.push_str(&entry.name);
690                    row.push_str("  ");
691                    row.push_str(&size_text);
692                    row.push_str(" B");
693                    row
694                };
695
696                let style = if idx == state.selected {
697                    if focused {
698                        Style::new().bold().fg(self.theme.primary)
699                    } else {
700                        Style::new().fg(self.theme.primary)
701                    }
702                } else {
703                    Style::new().fg(self.theme.text)
704                };
705                self.styled(row, style);
706            }
707        }
708
709        self.commands.push(Command::EndContainer);
710        self.rollback.last_text_idx = None;
711
712        response.changed = file_selected;
713        response
714    }
715}
716
717/// Group `child_commands` into per-cell command vectors for `grid()` / `grid_with()`.
718///
719/// Each container subtree (`BeginContainer`/`BeginScrollable` … matching `EndContainer`)
720/// becomes one element. Bare `InteractionMarker`s are flushed onto the next element so
721/// hit-testing slots stay attached to the cell that owns them. Trailing markers with
722/// no following command form their own (empty-content) element.
723fn collect_grid_elements(child_commands: Vec<Command>) -> Vec<Vec<Command>> {
724    let mut elements: Vec<Vec<Command>> = Vec::new();
725    let mut iter = child_commands.into_iter().peekable();
726    let mut pending_markers: Vec<Command> = Vec::new();
727    while let Some(cmd) = iter.next() {
728        match cmd {
729            Command::InteractionMarker(_) => {
730                pending_markers.push(cmd);
731            }
732            Command::BeginContainer(_) | Command::BeginScrollable(_) => {
733                let mut depth = 1_u32;
734                let mut element: Vec<Command> = std::mem::take(&mut pending_markers);
735                element.push(cmd);
736                for next in iter.by_ref() {
737                    match next {
738                        Command::BeginContainer(_) | Command::BeginScrollable(_) => {
739                            depth += 1;
740                        }
741                        Command::EndContainer => {
742                            depth = depth.saturating_sub(1);
743                        }
744                        _ => {}
745                    }
746                    let at_end = matches!(next, Command::EndContainer) && depth == 0;
747                    element.push(next);
748                    if at_end {
749                        break;
750                    }
751                }
752                elements.push(element);
753            }
754            Command::EndContainer => {}
755            _ => {
756                let mut element = std::mem::take(&mut pending_markers);
757                element.push(cmd);
758                elements.push(element);
759            }
760        }
761    }
762    // Flush any trailing markers (edge case: marker with no following command)
763    if !pending_markers.is_empty() {
764        elements.push(pending_markers);
765    }
766    elements
767}