Skip to main content

rmux_core/
grid.rs

1//! Safe grid and scrollback storage for pane screen contents.
2
3use rmux_proto::TerminalSize;
4use std::collections::VecDeque;
5
6use crate::hyperlinks::Hyperlinks;
7use crate::input::{Colour, COLOUR_DEFAULT};
8use crate::style::Style;
9
10#[path = "grid/cell.rs"]
11mod cell;
12#[path = "grid/history_bytes.rs"]
13mod history_bytes;
14#[path = "grid/render.rs"]
15mod render;
16
17pub(crate) use cell::{GridCell, GridCellFlags, GridLine, GridLineFlags};
18use render::{append_cell_text, append_grid_string_code, append_hyperlink};
19
20const HISTORY_STAMP_REFRESH_LINES: u16 = 256;
21
22/// Captured grid content rendered as logical lines.
23#[derive(Debug, Clone, PartialEq, Eq, Default)]
24#[cfg_attr(not(test), allow(dead_code))]
25pub(crate) struct GridCapture {
26    /// Captured lines ordered from oldest to newest.
27    pub lines: Vec<String>,
28}
29
30/// Rendering flags for tmux-style grid capture.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct GridRenderOptions {
33    /// Whether wrapped rows should omit separating newlines.
34    pub join_wrapped: bool,
35    /// Whether to emit ANSI SGR and OSC sequences inline.
36    pub with_sequences: bool,
37    /// Whether control sequences should be octal-escaped.
38    pub escape_sequences: bool,
39    /// Whether trailing empty cells should be included.
40    pub include_empty_cells: bool,
41    /// Whether included empty cells should stop at tmux's allocation bucket.
42    pub use_tmux_cell_capacity: bool,
43    /// Whether trailing spaces should be trimmed from the rendered line.
44    pub trim_spaces: bool,
45}
46
47impl Default for GridRenderOptions {
48    fn default() -> Self {
49        Self {
50            join_wrapped: false,
51            with_sequences: false,
52            escape_sequences: false,
53            include_empty_cells: true,
54            use_tmux_cell_capacity: false,
55            trim_spaces: true,
56        }
57    }
58}
59
60/// Per-capture ANSI state matching tmux's carried `lastgc`.
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct GridStringState {
63    last_cell: GridCell,
64}
65
66impl Default for GridStringState {
67    fn default() -> Self {
68        Self {
69            last_cell: GridCell::blank_with_bg(COLOUR_DEFAULT),
70        }
71    }
72}
73
74impl GridStringState {
75    pub(crate) fn reset_to_default_line_style(
76        &mut self,
77        options: GridRenderOptions,
78        hyperlinks: Option<&Hyperlinks>,
79        output: &mut Vec<u8>,
80    ) {
81        if !options.with_sequences {
82            return;
83        }
84
85        let default_cell = GridCell::blank_with_bg(COLOUR_DEFAULT);
86        let mut rendered = String::new();
87        let mut has_link = false;
88        append_grid_string_code(
89            &self.last_cell,
90            &default_cell,
91            &mut rendered,
92            options.escape_sequences,
93            hyperlinks,
94            &mut has_link,
95        );
96        if has_link {
97            append_hyperlink(&mut rendered, "", "", options.escape_sequences);
98        }
99        output.extend_from_slice(rendered.as_bytes());
100        self.last_cell = default_cell;
101    }
102}
103
104/// Absolute grid storage split into history and visible rows.
105#[derive(Debug, Clone, PartialEq, Eq)]
106pub(crate) struct Grid {
107    sx: u32,
108    sy: u32,
109    hlimit: usize,
110    hscrolled: usize,
111    history_enabled: bool,
112    history_stamp: i64,
113    history_stamp_remaining: u16,
114    history: VecDeque<GridLine>,
115    visible: VecDeque<GridLine>,
116}
117
118impl Grid {
119    /// Creates a new grid with the given geometry and history limit.
120    #[must_use]
121    pub fn new(size: TerminalSize, hlimit: usize) -> Self {
122        let sx = u32::from(size.cols.max(1));
123        let sy = u32::from(size.rows.max(1));
124        Self {
125            sx,
126            sy,
127            hlimit,
128            hscrolled: 0,
129            history_enabled: true,
130            history_stamp: 0,
131            history_stamp_remaining: 0,
132            history: VecDeque::new(),
133            visible: (0..sy).map(|_| GridLine::new(sx)).collect(),
134        }
135    }
136
137    /// Returns the grid size.
138    #[must_use]
139    pub fn size(&self) -> TerminalSize {
140        TerminalSize {
141            cols: u16::try_from(self.sx).unwrap_or(u16::MAX),
142            rows: u16::try_from(self.sy).unwrap_or(u16::MAX),
143        }
144    }
145
146    /// Returns the visible width in columns.
147    #[must_use]
148    pub const fn sx(&self) -> u32 {
149        self.sx
150    }
151
152    /// Returns the visible height in rows.
153    #[must_use]
154    pub const fn sy(&self) -> u32 {
155        self.sy
156    }
157
158    /// Returns the history size in rows.
159    #[must_use]
160    pub fn hsize(&self) -> usize {
161        self.history.len()
162    }
163
164    /// Returns the configured history limit.
165    #[must_use]
166    pub const fn hlimit(&self) -> usize {
167        self.hlimit
168    }
169
170    /// Returns whether history collection is enabled.
171    #[must_use]
172    pub const fn history_enabled(&self) -> bool {
173        self.history_enabled
174    }
175
176    /// Updates the history limit and evicts old rows if needed.
177    pub fn set_hlimit(&mut self, hlimit: usize) {
178        self.hlimit = hlimit;
179        while self.history.len() > self.hlimit {
180            let _ = self.history.pop_front();
181        }
182        self.hscrolled = self.hscrolled.min(self.history.len());
183    }
184
185    /// Enables or disables scrollback collection.
186    pub fn set_history_enabled(&mut self, enabled: bool) {
187        self.history_enabled = enabled;
188    }
189
190    /// Returns the number of history rows that can be pulled back by growth.
191    #[allow(dead_code)]
192    #[must_use]
193    pub const fn hscrolled(&self) -> usize {
194        self.hscrolled
195    }
196
197    /// Returns one visible line by row.
198    #[must_use]
199    pub fn visible_line(&self, y: u32) -> Option<&GridLine> {
200        self.visible.get(y as usize)
201    }
202
203    pub(crate) fn visible_line_mut(&mut self, y: u32) -> Option<&mut GridLine> {
204        self.visible.get_mut(y as usize)
205    }
206
207    /// Returns one absolute line where rows `0..hsize` are history and
208    /// `hsize..hsize+sy` are the visible screen.
209    #[allow(dead_code)]
210    #[must_use]
211    pub fn absolute_line(&self, absolute_y: usize) -> Option<&GridLine> {
212        if absolute_y < self.history.len() {
213            self.history.get(absolute_y)
214        } else {
215            self.visible.get(absolute_y - self.history.len())
216        }
217    }
218
219    /// Removes one absolute line from history or the visible viewport.
220    ///
221    /// Visible removals keep the viewport height stable by pushing a blank row
222    /// at the bottom.
223    pub fn remove_absolute_line(&mut self, absolute_y: usize) -> bool {
224        if absolute_y < self.history.len() {
225            let _ = self.history.remove(absolute_y);
226            self.hscrolled = self.hscrolled.min(self.history.len());
227            return true;
228        }
229
230        let visible_index = absolute_y.saturating_sub(self.history.len());
231        if visible_index >= self.visible.len() {
232            return false;
233        }
234
235        let _ = self.visible.remove(visible_index);
236        self.visible.push_back(GridLine::new(self.sx));
237        true
238    }
239
240    /// Drops all lines after the addressed absolute row and recomposes the viewport.
241    pub(crate) fn truncate_after_absolute_line(&mut self, absolute_y: usize) -> bool {
242        let total = self.history.len() + self.visible.len();
243        if absolute_y >= total {
244            return false;
245        }
246
247        let keep = absolute_y.saturating_add(1);
248        let mut lines = self
249            .history
250            .iter()
251            .chain(self.visible.iter())
252            .take(keep)
253            .cloned()
254            .collect::<Vec<_>>();
255        let visible_rows = self.sy as usize;
256        while lines.len() < visible_rows {
257            lines.push(GridLine::new(self.sx));
258        }
259
260        let visible_start = lines.len().saturating_sub(visible_rows);
261        let mut visible = lines.split_off(visible_start);
262        for line in &mut visible {
263            line.resize_width_preserving_wrap(self.sx, COLOUR_DEFAULT);
264        }
265        self.history = compacted_history(lines);
266        while self.history.len() > self.hlimit {
267            let _ = self.history.pop_front();
268        }
269        self.visible = visible.into();
270        self.hscrolled = self.history.len();
271        true
272    }
273
274    /// Returns whether the absolute line is marked as wrapped.
275    #[must_use]
276    pub fn absolute_line_wrapped(&self, absolute_y: usize) -> Option<bool> {
277        self.absolute_line(absolute_y)
278            .map(|line| line.flags.contains(GridLineFlags::WRAPPED))
279    }
280
281    /// Clears every history row.
282    pub fn clear_history(&mut self) {
283        self.history.clear();
284        self.hscrolled = 0;
285    }
286
287    /// Clears the visible grid.
288    pub fn clear_visible(&mut self, bg: Colour) {
289        for line in &mut self.visible {
290            line.clear(bg);
291        }
292    }
293
294    /// Moves used visible rows to scrollback before clearing the viewport.
295    pub fn clear_visible_to_history(&mut self, bg: Colour) {
296        if self.history_enabled {
297            let last_used = self.visible.iter().rposition(|line| line.used_end() > 0);
298            if let Some(last_used) = last_used {
299                for index in 0..=last_used {
300                    let line = self.visible[index].clone();
301                    self.push_history(line);
302                }
303            }
304        }
305        self.clear_visible(bg);
306    }
307
308    /// Replaces the visible rows with a saved copy.
309    pub fn replace_visible(&mut self, lines: Vec<GridLine>) {
310        self.sy = lines.len() as u32;
311        self.visible = lines.into();
312        for line in &mut self.visible {
313            line.resize_width_preserving_wrap(self.sx, COLOUR_DEFAULT);
314        }
315    }
316
317    pub(crate) fn replace_visible_resized_width_only(
318        &mut self,
319        source_size: TerminalSize,
320        lines: Vec<GridLine>,
321        bg: Colour,
322    ) {
323        debug_assert_eq!(
324            u32::from(source_size.rows.max(1)),
325            self.sy,
326            "width-only visible restore must not change row policy"
327        );
328        let target_width = self.sx;
329        let mut viewport = Grid::new(source_size, 0);
330        viewport.replace_visible(lines);
331        viewport.resize_width(target_width, bg);
332        self.visible = viewport.visible;
333    }
334
335    /// Captures the grid as rendered lines. Wrapped rows are optionally joined.
336    #[cfg_attr(not(test), allow(dead_code))]
337    #[must_use]
338    pub fn capture(&self, join_wrapped: bool) -> GridCapture {
339        let mut lines = Vec::new();
340        let mut pending = String::new();
341
342        for line in self.history.iter().chain(self.visible.iter()) {
343            let rendered = line.render_text();
344            if join_wrapped {
345                pending.push_str(&rendered);
346                if !line.flags.contains(GridLineFlags::WRAPPED) {
347                    lines.push(std::mem::take(&mut pending));
348                }
349                continue;
350            }
351
352            lines.push(rendered);
353        }
354
355        if join_wrapped && !pending.is_empty() {
356            lines.push(pending);
357        }
358
359        GridCapture { lines }
360    }
361
362    /// Renders one absolute line using tmux-style capture options.
363    #[must_use]
364    pub fn render_absolute_line(
365        &self,
366        absolute_y: usize,
367        options: GridRenderOptions,
368        state: &mut GridStringState,
369        hyperlinks: Option<&Hyperlinks>,
370    ) -> Option<String> {
371        self.absolute_line(absolute_y)
372            .map(|line| line.render_with_options(self.sx as usize, options, state, hyperlinks))
373    }
374
375    pub fn append_rendered_absolute_line(
376        &self,
377        absolute_y: usize,
378        options: GridRenderOptions,
379        state: &mut GridStringState,
380        hyperlinks: Option<&Hyperlinks>,
381        output: &mut Vec<u8>,
382    ) -> Option<()> {
383        let line = self.absolute_line(absolute_y)?;
384        if line.render_bytes_with_options(self.sx as usize, options, output) {
385            return Some(());
386        }
387        let rendered = line.render_with_options(self.sx as usize, options, state, hyperlinks);
388        output.extend_from_slice(rendered.as_bytes());
389        Some(())
390    }
391
392    /// Renders one visible line after applying a pane default-style overlay to
393    /// default cells only. This is used by live renderers to avoid cloning the
394    /// full screen and scrollback when only the viewport is needed.
395    #[must_use]
396    pub fn render_visible_line_with_default_style(
397        &self,
398        row: usize,
399        options: GridRenderOptions,
400        state: &mut GridStringState,
401        hyperlinks: Option<&Hyperlinks>,
402        style: &Style,
403    ) -> Option<String> {
404        self.visible_line(u32::try_from(row).ok()?).map(|line| {
405            line.render_with_default_style(self.sx as usize, options, state, hyperlinks, style)
406        })
407    }
408
409    /// Returns the retained history size in bytes including newlines.
410    #[must_use]
411    pub fn history_byte_size(&self) -> usize {
412        self.history
413            .iter()
414            .map(|line| line.render_text().len() + 1)
415            .sum()
416    }
417
418    /// Captures only the visible rows.
419    #[must_use]
420    pub fn visible_lines(&self) -> Vec<GridLine> {
421        self.visible.iter().cloned().collect()
422    }
423
424    pub(crate) fn scroll_region_up(
425        &mut self,
426        upper: u32,
427        lower: u32,
428        bg: Colour,
429        to_history: bool,
430    ) {
431        if !self.valid_region(upper, lower) {
432            return;
433        }
434
435        let upper = upper as usize;
436        let lower = lower as usize;
437        if upper == 0 && lower + 1 == self.visible.len() {
438            let Some(mut removed) = self.visible.pop_front() else {
439                return;
440            };
441            if to_history && self.history_enabled {
442                self.push_history(removed);
443                self.visible.push_back(GridLine::blank_with_bg(self.sx, bg));
444            } else {
445                removed.clear(bg);
446                self.visible.push_back(removed);
447            }
448            return;
449        }
450
451        let removed_for_history = if to_history && self.history_enabled {
452            let blank = GridLine::blank_with_bg(self.sx, bg);
453            let visible = self.visible.make_contiguous();
454            let removed = std::mem::replace(&mut visible[upper], blank);
455            Some(removed)
456        } else {
457            None
458        };
459        if let Some(removed) = removed_for_history {
460            self.push_history(removed);
461        }
462        let visible = self.visible.make_contiguous();
463        visible[upper..=lower].rotate_left(1);
464        let removed = &mut visible[lower];
465        removed.clear(bg);
466    }
467
468    pub(crate) fn scroll_region_down(&mut self, upper: u32, lower: u32, bg: Colour) {
469        if !self.valid_region(upper, lower) {
470            return;
471        }
472
473        let upper = upper as usize;
474        let lower = lower as usize;
475        if upper == 0 && lower + 1 == self.visible.len() {
476            let Some(mut removed) = self.visible.pop_back() else {
477                return;
478            };
479            removed.clear(bg);
480            self.visible.push_front(removed);
481            return;
482        }
483
484        let visible = self.visible.make_contiguous();
485        visible[upper..=lower].rotate_right(1);
486        visible[upper].clear(bg);
487    }
488
489    pub(crate) fn resize_width(&mut self, sx: u32, bg: Colour) {
490        let sx = sx.max(1);
491        if sx == self.sx {
492            return;
493        }
494
495        if self.can_resize_width_without_reflow(sx) {
496            for line in &mut self.history {
497                line.resize_width_preserving_wrap(sx, bg);
498            }
499            for line in &mut self.visible {
500                line.resize_width_preserving_wrap(sx, bg);
501            }
502            self.sx = sx;
503            return;
504        }
505
506        let visible_rows = self.sy as usize;
507        let lines = self
508            .history
509            .iter()
510            .chain(self.visible.iter())
511            .cloned()
512            .collect::<Vec<_>>();
513        let mut reflowed = reflow_wrapped_lines(lines, sx, bg);
514        while reflowed.len() < visible_rows {
515            reflowed.push(GridLine::blank_with_bg(sx, bg));
516        }
517
518        let history_rows = reflowed.len().saturating_sub(visible_rows);
519        let mut visible = reflowed.split_off(history_rows);
520        for line in &mut visible {
521            line.resize_width_preserving_wrap(sx, bg);
522        }
523        self.history = compacted_history(reflowed);
524        while self.history.len() > self.hlimit {
525            let _ = self.history.pop_front();
526        }
527        self.visible = visible.into();
528        self.hscrolled = self.history.len();
529        self.sx = sx;
530    }
531
532    fn can_resize_width_without_reflow(&self, sx: u32) -> bool {
533        self.history.iter().chain(self.visible.iter()).all(|line| {
534            !line.flags.contains(GridLineFlags::WRAPPED) && line.used_end() <= sx as usize
535        })
536    }
537
538    pub(crate) fn resize_height(&mut self, sy: u32, cursor_y: &mut u32, bg: Colour) {
539        let sy = sy.max(1);
540        let oldy = self.sy;
541
542        if sy < oldy {
543            let mut needed = oldy - sy;
544
545            let available_bottom = oldy.saturating_sub(1).saturating_sub(*cursor_y);
546            let remove_bottom = available_bottom.min(needed);
547            for _ in 0..remove_bottom {
548                let _ = self.visible.pop_back();
549            }
550            needed -= remove_bottom;
551
552            if self.history_enabled {
553                for _ in 0..needed {
554                    let Some(line) = self.visible.pop_front() else {
555                        break;
556                    };
557                    self.push_history(line);
558                }
559            } else {
560                let remove_top = (*cursor_y).min(needed);
561                for _ in 0..remove_top {
562                    let _ = self.visible.pop_front();
563                }
564                *cursor_y = cursor_y.saturating_sub(remove_top);
565            }
566        } else if sy > oldy {
567            let mut needed = sy - oldy;
568            let pull = self.hscrolled.min(needed as usize).min(self.history.len()) as u32;
569            if self.history_enabled && pull > 0 {
570                let mut restored = Vec::with_capacity(pull as usize);
571                for _ in 0..pull {
572                    if let Some(line) = self.history.pop_back() {
573                        restored.push(line);
574                    }
575                }
576                restored.reverse();
577                for mut line in restored.into_iter().rev() {
578                    line.resize_width_preserving_wrap(self.sx, bg);
579                    self.visible.push_front(line);
580                }
581                *cursor_y = cursor_y.saturating_add(pull).min(sy.saturating_sub(1));
582                self.hscrolled -= pull as usize;
583                needed -= pull;
584            }
585
586            for _ in 0..needed {
587                self.visible.push_back(GridLine::blank_with_bg(self.sx, bg));
588            }
589        }
590
591        self.sy = sy;
592        while self.visible.len() > self.sy as usize {
593            let _ = self.visible.pop_back();
594        }
595        while self.visible.len() < self.sy as usize {
596            self.visible.push_back(GridLine::blank_with_bg(self.sx, bg));
597        }
598        for line in &mut self.visible {
599            line.resize_width_preserving_wrap(self.sx, bg);
600        }
601        *cursor_y = (*cursor_y).min(self.sy.saturating_sub(1));
602    }
603
604    fn valid_region(&self, upper: u32, lower: u32) -> bool {
605        upper < self.sy && lower < self.sy && upper <= lower
606    }
607
608    fn push_history(&mut self, mut line: GridLine) {
609        if self.hlimit == 0 {
610            return;
611        }
612
613        line.stamp_for_history_at(self.next_history_stamp());
614        line.compact_for_history();
615        if self.history.len() == self.hlimit {
616            let _ = self.history.pop_front();
617        }
618        self.history.push_back(line);
619        self.hscrolled = (self.hscrolled + 1).min(self.history.len());
620    }
621
622    fn next_history_stamp(&mut self) -> i64 {
623        if self.history_stamp_remaining == 0 {
624            self.history_stamp = cell::current_unix_timestamp();
625            self.history_stamp_remaining = HISTORY_STAMP_REFRESH_LINES;
626        }
627        self.history_stamp_remaining = self.history_stamp_remaining.saturating_sub(1);
628        self.history_stamp
629    }
630}
631
632fn compacted_history(lines: Vec<GridLine>) -> VecDeque<GridLine> {
633    lines
634        .into_iter()
635        .map(|mut line| {
636            line.compact_for_history();
637            line
638        })
639        .collect()
640}
641
642fn reflow_wrapped_lines(lines: Vec<GridLine>, width: u32, bg: Colour) -> Vec<GridLine> {
643    let mut output = Vec::new();
644    let mut logical_cells = Vec::new();
645    let mut logical_plain_text: Option<String> = None;
646    let mut logical_flags = None;
647
648    for line in lines {
649        let wrapped = line.flags.contains(GridLineFlags::WRAPPED);
650        if logical_flags.is_none() {
651            let mut flags = line.flags;
652            flags.remove(GridLineFlags::WRAPPED);
653            logical_flags = Some(flags);
654            logical_plain_text = (bg == COLOUR_DEFAULT).then(String::new);
655        }
656
657        let end = if wrapped {
658            self::line_width(&line)
659        } else {
660            line.used_end()
661        };
662        if let (Some(logical_text), Some(text)) = (logical_plain_text.as_mut(), line.plain_text()) {
663            logical_text.extend(
664                text.bytes()
665                    .chain(std::iter::repeat(b' '))
666                    .take(end)
667                    .map(char::from),
668            );
669        } else {
670            if let Some(text) = logical_plain_text.take() {
671                extend_plain_ascii_cells(&mut logical_cells, text.bytes());
672            }
673            if let Some(text) = line.plain_text() {
674                extend_plain_ascii_cells(
675                    &mut logical_cells,
676                    text.bytes().chain(std::iter::repeat(b' ')).take(end),
677                );
678            } else {
679                logical_cells.extend(
680                    line.cells
681                        .iter()
682                        .take(end)
683                        .filter(|cell| !cell.is_padding())
684                        .cloned(),
685                );
686            }
687        }
688
689        if !wrapped {
690            let flags = logical_flags.take().unwrap_or_default();
691            if let Some(text) = logical_plain_text.take() {
692                output.extend(reflow_plain_ascii_line(&text, flags, width, bg));
693            } else {
694                output.extend(reflow_logical_line(&logical_cells, flags, width, bg));
695            }
696            logical_cells.clear();
697        }
698    }
699
700    if logical_flags.is_some() || !logical_cells.is_empty() || logical_plain_text.is_some() {
701        let flags = logical_flags.unwrap_or_default();
702        if let Some(text) = logical_plain_text {
703            output.extend(reflow_plain_ascii_line(&text, flags, width, bg));
704        } else {
705            output.extend(reflow_logical_line(&logical_cells, flags, width, bg));
706        }
707    }
708
709    output
710}
711
712fn extend_plain_ascii_cells(cells: &mut Vec<GridCell>, bytes: impl IntoIterator<Item = u8>) {
713    cells.extend(bytes.into_iter().map(GridCell::from_plain_ascii));
714}
715
716fn reflow_plain_ascii_line(
717    text: &str,
718    first_flags: GridLineFlags,
719    width: u32,
720    bg: Colour,
721) -> Vec<GridLine> {
722    if text.is_empty() || bg != COLOUR_DEFAULT {
723        let mut line = GridLine::blank_with_bg(width, bg);
724        line.flags = first_flags;
725        return vec![line];
726    }
727
728    let width = width.max(1);
729    let width_usize = width as usize;
730    let mut output = Vec::with_capacity(text.len().div_ceil(width_usize));
731    let mut start = 0;
732    let mut flags = first_flags;
733    while start < text.len() {
734        let end = (start + width_usize).min(text.len());
735        let mut line = GridLine::from_plain_ascii_text(width, flags, text[start..end].to_owned());
736        if end < text.len() {
737            line.set_wrapped(true);
738        }
739        output.push(line);
740        flags = GridLineFlags::default();
741        start = end;
742    }
743    output
744}
745
746fn reflow_logical_line(
747    cells: &[GridCell],
748    first_flags: GridLineFlags,
749    width: u32,
750    bg: Colour,
751) -> Vec<GridLine> {
752    if cells.is_empty() {
753        let mut line = GridLine::blank_with_bg(width, bg);
754        line.flags = first_flags;
755        return vec![line];
756    }
757
758    let mut output = Vec::new();
759    let mut current = GridLine::blank_with_bg(width, bg);
760    current.flags = first_flags;
761    let mut x: u32 = 0;
762
763    for cell in cells {
764        let mut cell = cell.clone();
765        let mut cell_width = u32::from(cell.width().max(1));
766        if cell_width > width {
767            cell_width = 1;
768            cell.set_width(1);
769        }
770        if x > 0 && x.saturating_add(cell_width) > width {
771            current.set_wrapped(true);
772            output.push(current);
773            current = GridLine::blank_with_bg(width, bg);
774            x = 0;
775        }
776
777        if let Some(target) = current.cell_mut(x) {
778            *target = cell.clone();
779        }
780        for offset in 1..cell_width {
781            if let Some(padding_cell) = current.cell_mut(x + offset) {
782                let mut padding = cell.clone();
783                padding.set_text(" ".to_owned());
784                padding.set_width(0);
785                padding.set_flags(GridCellFlags::PADDING);
786                *padding_cell = padding;
787            }
788        }
789        current.touch();
790        x += cell_width;
791    }
792
793    output.push(current);
794    output
795}
796
797fn line_width(line: &GridLine) -> usize {
798    line.width() as usize
799}
800
801#[cfg(test)]
802#[path = "grid/tests.rs"]
803mod tests;