Skip to main content

pito_tui/
chrome.rs

1use std::time::Duration;
2
3use pito_footer::{ConfirmBar, Footer, Hint, Notice, Words as Choice};
4use pito_header::{Breadcrumb, Drill, Header};
5use ratatui::{
6    Frame,
7    buffer::Buffer,
8    layout::{Alignment, Rect},
9    style::{Modifier, Style},
10    text::{Line, Text},
11    widgets::{Block, BorderType, Clear, Paragraph, Widget, Wrap},
12};
13
14use crate::head::{self, Focus, Item, Kind, Labels, Numbers, Row, Rule};
15use crate::label::Span;
16use crate::layout::{Edge, PAD, Spot};
17use crate::screen::{Phase, Screen};
18use crate::shell::{Role, Tui};
19use crate::text::cells;
20use crate::words::Words;
21
22const SIDES: u16 = 6;
23const LEAD: &str = " ยท ";
24const TOAST: u16 = 3;
25
26fn width(text: &str) -> u16 {
27    u16::try_from(cells(text)).unwrap_or(u16::MAX)
28}
29
30fn room(line: u16, title: &str) -> u16 {
31    let wide = width(title).saturating_add(2).min(line);
32    let side = ((line - wide) / 2).saturating_sub(2);
33    if side < SIDES { 0 } else { side - 2 }
34}
35
36pub fn message<'a>(frame: &mut Frame, area: Rect, text: impl Into<Text<'a>>) {
37    message_at(frame, area, text, Spot::THIRD);
38}
39
40pub fn message_at<'a>(frame: &mut Frame, area: Rect, text: impl Into<Text<'a>>, spot: Spot) {
41    let text = text.into();
42    let inner = area.width.saturating_sub(2 * PAD);
43    let x = area.x.saturating_add(PAD.min(area.width));
44    let lines: usize = text
45        .lines
46        .iter()
47        .map(|line| line.width().div_ceil(usize::from(inner.max(1))).max(1))
48        .sum();
49    let lines = u16::try_from(lines).unwrap_or(u16::MAX).min(area.height);
50    let top = match spot.y {
51        Edge::Start => 0,
52        Edge::Third => area.height / 3,
53        Edge::Middle => (area.height - lines) / 2,
54        Edge::End => area.height - lines,
55    };
56    let alignment = match spot.x {
57        Edge::Start => Alignment::Left,
58        Edge::End => Alignment::Right,
59        Edge::Third | Edge::Middle => Alignment::Center,
60    };
61    let place = Rect::new(x, area.y + top, inner, area.height - top);
62    let paragraph = Paragraph::new(text)
63        .alignment(alignment)
64        .wrap(Wrap { trim: true });
65    frame.render_widget(paragraph, place);
66}
67
68fn choice<'a>(words: &'a Words, hint: &'a str) -> Choice<'a> {
69    let choice = Choice::new(&words.yes, &words.no);
70    if hint.is_empty() {
71        choice
72    } else {
73        choice.hint(hint)
74    }
75}
76
77fn light(buffer: &mut Buffer, plain: &Buffer, accent: Style) {
78    let lit = accent.add_modifier(Modifier::BOLD);
79    let area = plain.area;
80    for y in area.top()..area.bottom() {
81        let changed = |x: &u16| {
82            let symbol = buffer[(*x, y)].symbol();
83            symbol != " " && symbol != plain[(*x, y)].symbol()
84        };
85        let first = (area.left()..area.right()).find(changed);
86        let last = (area.left()..area.right()).rev().find(changed);
87        let (Some(first), Some(last)) = (first, last) else {
88            continue;
89        };
90        for x in first..=last {
91            let cell = &mut buffer[(x, y)];
92            cell.modifier = Modifier::empty();
93            cell.set_style(lit);
94        }
95    }
96}
97
98fn spans(parts: &[(String, Style)]) -> Vec<Span> {
99    parts
100        .iter()
101        .map(|(text, style)| (text.clone().into(), *style))
102        .collect()
103}
104
105impl<E: Send + 'static> Tui<E> {
106    pub(crate) fn title_text(&self) -> String {
107        self.title.iter().map(|(text, _)| text.as_ref()).collect()
108    }
109
110    fn footer<'a>(
111        &'a self,
112        hints: &'a [Hint<'a>],
113        screen: &'a dyn Screen<E>,
114        again: Option<&'a str>,
115        width: u16,
116    ) -> Footer<'a> {
117        let hints_open = self.help.is_none_or(|help| help.open());
118        let notice = again
119            .map(|text| Notice::new(text, self.quit_tone))
120            .or_else(|| {
121                self.out
122                    .said
123                    .as_ref()
124                    .map(|(text, tone)| Notice::new(text, *tone))
125            })
126            .or_else(|| screen.notice())
127            .or_else(|| screen.legend().filter(|_| hints_open).map(Notice::legend));
128        let words = &self.words;
129        let leave = if words.leave.is_empty() {
130            &words.choose
131        } else {
132            &words.leave
133        };
134        let mut asked = choice(words, &words.choose);
135        let mut leaving = choice(words, leave);
136        if let Some(text) = again.filter(|_| self.lit_again) {
137            asked = asked.hint(text);
138            leaving = leaving.hint(text);
139        }
140        let confirm = self.quit.bar(leaving).or_else(|| {
141            self.out
142                .asking
143                .as_ref()
144                .map(|asking| ConfirmBar::new(&asking.question, &asking.confirm, asked))
145        });
146        let input = screen
147            .input()
148            .map(|bar| again.map_or(bar, |text| bar.hint(text)));
149        let mut footer = Footer::new(hints)
150            .styles(self.drawn.footer())
151            .version(&self.name, &self.version)
152            .notice(notice)
153            .confirm(confirm)
154            .input(input);
155        if let Some(help) = &self.help
156            && !self.gated
157        {
158            footer = footer.help(help);
159        }
160        if self.version_fit {
161            let bare = footer.version("", "");
162            if again.is_some() || bare.height(width) < footer.height(width) {
163                footer = bare;
164            }
165        }
166        match self.footer_look {
167            Some(look) => look(footer, width),
168            None => footer,
169        }
170    }
171
172    fn sides(&self, index: usize) -> (&dyn Screen<E>, &dyn Screen<E>) {
173        let screen = self.screens[index].screen.as_ref();
174        let Some(app) = self.app_slot().map(|app| self.screens[app].screen.as_ref()) else {
175            return (screen, screen);
176        };
177        (app, screen)
178    }
179
180    fn left(&self, index: usize, room: u16) -> Vec<(String, Style)> {
181        let help = self
182            .help
183            .filter(|help| !help.open() && !self.gated)
184            .map(|_| self.words.help.as_ref())
185            .filter(|text| !text.is_empty());
186        let (app, screen) = self.sides(index);
187        for source in [app, screen] {
188            if let Some(left) = source.left(room, help) {
189                return left;
190            }
191        }
192        let taken = help.map_or(0, |text| width(text).saturating_add(width(LEAD)));
193        let mut lead = app.lead(room.saturating_sub(taken));
194        if lead.iter().all(|(text, _)| text.is_empty()) {
195            lead = screen.lead(room.saturating_sub(taken));
196        }
197        let mut left = Vec::new();
198        if let Some(text) = help {
199            left.push((text.to_string(), self.drawn.muted));
200            if lead.iter().any(|(text, _)| !text.is_empty()) {
201                left.push((LEAD.to_string(), self.drawn.muted));
202            }
203        }
204        left.extend(lead);
205        left
206    }
207
208    fn right(&self, index: usize, room: u16) -> Vec<(String, Style)> {
209        let (app, screen) = self.sides(index);
210        let parts = app.status_parts(room);
211        if parts.iter().any(|(text, _)| !text.is_empty()) {
212            return parts;
213        }
214        screen.status_parts(room)
215    }
216
217    pub(crate) fn header<'a>(
218        &'a self,
219        title: &'a str,
220        facts: &'a [(String, Style)],
221        left: &'a [(String, Style)],
222        status: &'a [(String, Style)],
223    ) -> Header<'a> {
224        let header = Header::new(&self.model.nav)
225            .styles(self.drawn.header())
226            .title(Some(title))
227            .left_pairs(left)
228            .right_pairs(status)
229            .facts_pairs(facts);
230        let header = match self.header_look {
231            Some(look) => look(header),
232            None => header,
233        };
234        if self.open.is_some() {
235            header.lit(false).drill(Drill::Rows).crumbs(false)
236        } else {
237            header
238        }
239    }
240
241    fn facts_row<'a>(&'a self, facts: &'a [(String, Style)]) -> Header<'a> {
242        self.header("", facts, &[], &[])
243            .title(None)
244            .tabs(false)
245            .crumbs(false)
246            .closing(false)
247            .notice(None)
248    }
249
250    pub(crate) fn tail(&self) -> Vec<(String, Style)> {
251        self.current()
252            .and_then(|index| self.screens.get(index))
253            .map(|slot| slot.screen.after_tabs())
254            .unwrap_or_default()
255    }
256
257    pub(crate) fn group_labels(&self) -> Labels<'_> {
258        let place = self.model.nav.place();
259        let palette = &self.drawn;
260        let items = self
261            .model
262            .groups
263            .iter()
264            .enumerate()
265            .map(|(group, heading)| Item {
266                label: &self.model.headings[*heading].label,
267                number: None,
268                on: group == place.group,
269            })
270            .collect();
271        let on = if self.focus == Focus::Groups {
272            palette.focus
273        } else {
274            palette.accent.add_modifier(Modifier::BOLD)
275        };
276        Labels {
277            items,
278            kind: Kind::Groups,
279            on,
280            off: palette.inactive,
281            tail: &[],
282            tail_style: Style::new(),
283        }
284    }
285
286    pub(crate) fn section_labels<'a>(&'a self, tail: &'a [(String, Style)]) -> Labels<'a> {
287        let palette = &self.drawn;
288        let place = self.model.nav.place();
289        let tabs = self.model.group_tabs();
290        let numbers = self
291            .head
292            .as_ref()
293            .map_or(Numbers::Across, |head| head.numbers);
294        let lone = self.head.as_ref().is_some_and(|head| head.lone) && tabs.len() == 1;
295        let offset = place.number.saturating_sub(place.section + 1);
296        let items = tabs
297            .iter()
298            .enumerate()
299            .map(|(at, tab)| Item {
300                label: &self.model.tabs[*tab].label,
301                number: match numbers {
302                    _ if lone => None,
303                    Numbers::Across => Some(offset + at + 1),
304                    Numbers::InGroup => Some(at + 1),
305                    Numbers::Off => None,
306                },
307                on: at == place.section,
308            })
309            .collect();
310        let lit = self.open.is_none();
311        let on = match (lit, self.focus) {
312            (false, _) => palette.inactive,
313            (true, Focus::Sections) => palette.focus,
314            (true, _) => palette.accent.add_modifier(Modifier::BOLD),
315        };
316        let tail_style = if self.focus == Focus::Slot {
317            palette.focus
318        } else {
319            Style::new()
320        };
321        Labels {
322            items,
323            kind: Kind::Sections,
324            on,
325            off: palette.inactive,
326            tail,
327            tail_style,
328        }
329    }
330
331    fn title_rule(
332        &self,
333        buf: &mut Buffer,
334        line: Rect,
335        title: &[Span],
336        left: &[(String, Style)],
337        right: &[(String, Style)],
338    ) {
339        Rule {
340            rule: self.drawn.rule,
341            base: self.drawn.title,
342            middle: title,
343            left,
344            right,
345        }
346        .draw(buf, line);
347    }
348
349    #[allow(clippy::too_many_arguments)]
350    fn stack(
351        &self,
352        buf: &mut Buffer,
353        line: Rect,
354        bottom: u16,
355        index: usize,
356        facts: &[(String, Style)],
357        left: &[(String, Style)],
358        status: &[(String, Style)],
359    ) -> (u16, Vec<(Row, Rect)>) {
360        let Some(head) = self.head.as_ref() else {
361            return (line.y, Vec::new());
362        };
363        let palette = &self.drawn;
364        let overlay = self
365            .open
366            .and_then(|open| match &self.screens.get(open)?.role {
367                Role::Overlay(nav, _) => Some(nav),
368                _ => None,
369            });
370        let nav = &self.model.nav;
371        let mut y = line.y;
372        let mut rows = Vec::new();
373        for row in &head.rows {
374            if y >= bottom {
375                break;
376            }
377            let at = Rect { y, ..line };
378            match row {
379                Row::Title => {
380                    self.title_rule(buf, at, &self.title, left, status);
381                }
382                Row::Groups => {
383                    if self.model.groups.len() < 2 {
384                        continue;
385                    }
386                    self.group_labels().draw(buf, at);
387                    rows.push((Row::Groups, at));
388                }
389                Row::Caption => {
390                    let caption = self.screens[index]
391                        .screen
392                        .caption()
393                        .map(|parts| spans(&parts));
394                    let group = self.model.groups.get(nav.place().group).map(|heading| {
395                        let name = self.model.headings[*heading].label.text().to_string();
396                        vec![(name.into(), Style::new())]
397                    });
398                    let middle = caption.or(group).unwrap_or_default();
399                    Rule {
400                        rule: palette.rule,
401                        base: palette.accent.add_modifier(Modifier::BOLD),
402                        middle: &middle,
403                        left: &[],
404                        right: &[],
405                    }
406                    .draw(buf, at);
407                }
408                Row::Sections => {
409                    if nav.count() == 0 {
410                        continue;
411                    }
412                    if overlay.is_none() && nav.depth() > 0 {
413                        Breadcrumb::new(nav)
414                            .styles(palette.header())
415                            .centred(true)
416                            .render(at, buf);
417                    } else {
418                        let tail = self.screens[index].screen.after_tabs();
419                        self.section_labels(&tail).draw(buf, at);
420                        rows.push((Row::Sections, at));
421                    }
422                }
423                Row::Facts => {
424                    if facts.iter().all(|(text, _)| text.is_empty()) {
425                        continue;
426                    }
427                    self.facts_row(facts).render(at, buf);
428                }
429                Row::Crumbs => {
430                    if overlay.is_none() && nav.depth() > 0 {
431                        continue;
432                    }
433                    Breadcrumb::new(overlay.unwrap_or(nav))
434                        .styles(palette.header())
435                        .render(at, buf);
436                }
437                Row::Blank => head::blank(buf, at, palette.base),
438            }
439            y += 1;
440        }
441        (y, rows)
442    }
443
444    fn time_glass(&mut self, index: usize) {
445        let moment = self.moment;
446        let (delay, least) = (self.delay, self.least);
447        let slot = &mut self.screens[index];
448        let label = match slot.screen.phase() {
449            Phase::Loading(label) => Some(label.into_owned()),
450            _ => None,
451        };
452        match label {
453            Some(label) => {
454                let since = *slot.since.get_or_insert(moment);
455                if slot.glass.is_none() && moment.saturating_duration_since(since) >= delay {
456                    slot.glass = Some(moment);
457                }
458                slot.label = label;
459            }
460            None => {
461                slot.since = None;
462                if slot
463                    .glass
464                    .is_some_and(|shown| moment.saturating_duration_since(shown) >= least)
465                {
466                    slot.glass = None;
467                }
468            }
469        }
470    }
471
472    fn draw_takeover(&self, frame: &mut Frame, area: Rect) {
473        let layout = self.layout;
474        let palette = self.drawn;
475        let width = area.width.saturating_sub(2 * layout.side);
476        let x = area.x + layout.side;
477        let bottom = area.bottom().saturating_sub(layout.bottom);
478        let top = (area.y + layout.top).min(bottom);
479        let Some((_, takeover)) = self.out.takeover.as_ref() else {
480            return;
481        };
482        if top < bottom {
483            Rule {
484                rule: palette.rule,
485                base: palette.title,
486                middle: &takeover.title,
487                left: &[],
488                right: &[],
489            }
490            .draw(frame.buffer_mut(), Rect::new(x, top, width, 1));
491        }
492        let again = self.quit.notice().filter(|text| !text.is_empty());
493        let mut floor = bottom;
494        let words = &self.words;
495        let leave = if words.leave.is_empty() {
496            &words.choose
497        } else {
498            &words.leave
499        };
500        let confirm = self.quit.bar(choice(words, leave));
501        if again.is_some() || confirm.is_some() {
502            let footer = Footer::new(&[])
503                .styles(palette.footer())
504                .notice(again.map(|text| Notice::new(text, self.quit_tone)))
505                .confirm(confirm)
506                .rule(false);
507            let height = footer.height(width).min(bottom.saturating_sub(top + 1));
508            floor = bottom - height;
509            frame.render_widget(footer, Rect::new(x, floor, width, height));
510        }
511        let elapsed = self.taken.map_or(Duration::ZERO, |since| {
512            self.moment.saturating_duration_since(since)
513        });
514        let below = top.saturating_add(1).min(floor);
515        let inner = area.width.saturating_sub(2 * layout.pad);
516        let place = Rect::new(area.x + layout.pad, below, inner, floor - below);
517        let hourglass = palette.hourglass(elapsed, &takeover.label, None);
518        frame.render_widget(hourglass, place);
519    }
520
521    fn draw_toasts(&self, frame: &mut Frame, area: Rect) {
522        let mut stacks: Vec<(Spot, u16)> = Vec::new();
523        for (until, toast) in &self.toasts {
524            if *until <= self.moment || area.is_empty() {
525                continue;
526            }
527            let wide = width(&toast.text).saturating_add(4).min(area.width);
528            let shift = match stacks.iter_mut().find(|(spot, _)| *spot == toast.spot) {
529                Some((_, count)) => {
530                    *count += 1;
531                    *count - 1
532                }
533                None => {
534                    stacks.push((toast.spot, 1));
535                    0
536                }
537            };
538            let mut place = toast.spot.place(area, wide, TOAST);
539            let by = shift.saturating_mul(TOAST);
540            place.y = match toast.spot.y {
541                Edge::End => place.y.saturating_sub(by).max(area.y),
542                _ => place.y.saturating_add(by),
543            };
544            if place.bottom() > area.bottom() || place.y < area.y {
545                continue;
546            }
547            let style = self.drawn.tone(toast.tone);
548            frame.render_widget(Clear, place);
549            let block = Block::bordered()
550                .border_type(BorderType::Rounded)
551                .border_style(style)
552                .style(self.drawn.base);
553            let inner = block.inner(place);
554            frame.render_widget(block, place);
555            let text = Line::styled(toast.text.to_string(), style).alignment(Alignment::Center);
556            frame.render_widget(Paragraph::new(text), inner);
557        }
558    }
559
560    pub fn draw(&mut self, frame: &mut Frame) {
561        let area = frame.area();
562        let palette = self.drawn;
563        frame.render_widget(Block::new().style(palette.base), area);
564        if area.width < self.min.0 || area.height < self.min.1 {
565            let line = Line::styled(self.words.too_small.as_ref(), palette.small);
566            message_at(frame, area, line, self.layout.small);
567            return;
568        }
569        self.follow();
570        self.word_quit();
571        self.rows.clear();
572        if self.out.takeover.is_some() {
573            self.draw_takeover(frame, area);
574            return;
575        }
576        let Some(index) = self.current() else {
577            return;
578        };
579        let moment = self.moment;
580        self.screens[index].screen.moment(moment);
581        self.time_glass(index);
582        let layout = self.layout;
583        let width = area.width.saturating_sub(2 * layout.side);
584        let bottom = area.bottom().saturating_sub(layout.bottom);
585        let title = self.title_text();
586        let screen = &self.screens[index].screen;
587        let facts = screen.facts();
588        let room = room(width, &title);
589        let left = self.left(index, room);
590        let status = self.right(index, room);
591        let line = Rect::new(area.x + layout.side, area.y + layout.top, width, 1);
592        let mut below;
593        if self.gated || self.head.is_some() {
594            let top = line.y.min(bottom);
595            if self.gated {
596                if top < bottom {
597                    self.title_rule(frame.buffer_mut(), line, &self.title, &left, &status);
598                }
599                below = (top + 1).min(bottom);
600            } else {
601                let (y, rows) = self.stack(
602                    frame.buffer_mut(),
603                    line,
604                    bottom,
605                    index,
606                    &facts,
607                    &left,
608                    &status,
609                );
610                below = y;
611                self.rows = rows;
612            }
613            self.top = Rect::default();
614        } else {
615            let header = self.header(&title, &facts, &left, &status);
616            let top = Rect::new(
617                line.x,
618                line.y,
619                width,
620                header.height().min(bottom.saturating_sub(line.y)),
621            );
622            let dropped = header.height() == header.facts_pairs(&[]).height();
623            frame.render_widget(header, top);
624            self.top = top;
625            below = top.bottom();
626            let overlay = self
627                .open
628                .and_then(|open| match &self.screens.get(open)?.role {
629                    Role::Overlay(nav, _) => Some(nav),
630                    _ => None,
631                });
632            if below < bottom && (overlay.is_some() || self.model.nav.depth() == 0) {
633                let nav = overlay.unwrap_or(&self.model.nav);
634                let rule = Breadcrumb::new(nav).styles(palette.header());
635                frame.render_widget(rule, Rect::new(top.x, below, width, 1));
636                below += 1;
637            } else if self.keep_facts && dropped && below < bottom {
638                let row = self.facts_row(&facts);
639                let height = row.height().min(bottom - below);
640                frame.render_widget(row, Rect::new(top.x, below, width, height));
641                below += height;
642            }
643        }
644        below = below.saturating_add(layout.head_gap).min(bottom);
645        let screen = &self.screens[index].screen;
646        let again = self.quit.notice().filter(|text| !text.is_empty());
647        let (mut hints, prompted) = match (self.out.modals.last(), &self.out.picking) {
648            (Some(top), _) => (top.modal.hints.clone(), true),
649            (None, Some(picking)) => (picking.pick.hints.clone(), true),
650            (None, None) => (screen.hints(), false),
651        };
652        if !self.gated {
653            let shown = self.hints.iter().filter(|hint| !prompted || hint.pinned);
654            hints.extend(shown.copied());
655        }
656        if again.is_some() && !self.quit_hints {
657            hints.clear();
658        }
659        let footer = self.footer(&hints, screen.as_ref(), again, width);
660        let height = footer.height(width).min(bottom.saturating_sub(below));
661        let floor_line = bottom.saturating_sub(height);
662        let place = Rect::new(line.x, floor_line, width, height);
663        frame.render_widget(footer, place);
664        if again.is_some()
665            && self.lit_again
666            && (screen.input().is_some() || self.out.asking.is_some())
667        {
668            let mut plain = Buffer::empty(place);
669            self.footer(&hints, screen.as_ref(), Some(""), width)
670                .render(place, &mut plain);
671            light(frame.buffer_mut(), &plain, palette.accent);
672        }
673        let inner = area.width.saturating_sub(2 * layout.pad);
674        let x = area.x + layout.pad;
675        let mut floor = floor_line;
676        let room = floor_line.saturating_sub(below);
677        let band = if self.gated { None } else { self.band.as_mut() };
678        if let Some(band) = band {
679            let rows = band
680                .height(room, moment)
681                .min(room.saturating_sub(layout.gap));
682            if rows > 0 {
683                floor = floor_line - rows;
684                band.draw(frame, Rect::new(x, floor, inner, rows), &palette, moment);
685            }
686        } else if let Some(board) = self.board().filter(|_| self.open.is_none() && !self.gated) {
687            let shown = board.banded(moment).len();
688            let rows = u16::try_from(shown).unwrap_or(u16::MAX).min(room / 2);
689            if rows > 0 {
690                floor = floor_line - rows;
691                board.band(frame, Rect::new(x, floor, inner, rows), &palette, moment);
692            }
693        }
694        if !self.gated
695            && let Some(band) = self.top_band.as_mut()
696        {
697            let room = floor.saturating_sub(below);
698            let rows = band
699                .height(room, moment)
700                .min(room.saturating_sub(layout.gap + 1));
701            if rows > 0 {
702                band.draw(frame, Rect::new(x, below, inner, rows), &palette, moment);
703                below += rows;
704            }
705        }
706        let content = Rect::new(x, below, inner, floor.saturating_sub(below + layout.gap));
707        self.content = content;
708        let slot = &self.screens[index];
709        let shown = slot
710            .glass
711            .map(|since| moment.saturating_duration_since(since));
712        let phase = slot.screen.phase();
713        match (shown, phase) {
714            (Some(elapsed), _) => {
715                let waiting = Some(self.words.waiting.as_ref());
716                let hourglass = palette.hourglass(elapsed, &slot.label, waiting);
717                frame.render_widget(hourglass, content);
718            }
719            (None, Phase::Loading(_)) => {}
720            (None, Phase::Trouble(lines)) => message(frame, content, lines.into_owned()),
721            (None, _) => self.screens[index].screen.draw(frame, content, &palette),
722        }
723        if let Some(picking) = self.out.picking.as_mut() {
724            picking.pick.draw(frame, content, &palette);
725        }
726        for opened in &mut self.out.modals {
727            opened.modal.draw(frame, content, &palette);
728        }
729        self.draw_toasts(frame, content);
730    }
731}