Skip to main content

ansiq_core/
element.rs

1use std::{
2    fmt,
3    ops::{BitOr, BitOrAssign},
4    rc::Rc,
5};
6
7use crate::{HistoryBlock, Line, Rect, Row, ScopeId, Span, Style, Text};
8use unicode_width::UnicodeWidthChar;
9
10pub type ChangeHandler = std::boxed::Box<dyn FnMut(String)>;
11pub type SubmitHandler<Message> = std::boxed::Box<dyn FnMut(String) -> Option<Message>>;
12pub type SelectHandler<Message> = std::boxed::Box<dyn FnMut(usize) -> Option<Message>>;
13pub type ScrollHandler<Message> = std::boxed::Box<dyn FnMut(usize) -> Option<Message>>;
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum RuntimeWidgetState {
17    InputCursor(usize),
18    List(ListState),
19    Tabs(Option<usize>),
20    Table(TableState),
21    ScrollView(Option<usize>),
22    Scrollbar(ScrollbarState),
23}
24
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum WidgetKey {
27    Up,
28    Down,
29    Left,
30    Right,
31    Escape,
32    Enter,
33    Backspace,
34    Char(char),
35}
36
37#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
38pub struct WidgetRouteContext {
39    pub viewport_height: usize,
40    pub scroll_view_max_offset: Option<usize>,
41}
42
43#[derive(Debug, PartialEq, Eq)]
44pub struct WidgetRouteEffect<Message> {
45    pub dirty: bool,
46    pub message: Option<Message>,
47}
48
49impl<Message> Default for WidgetRouteEffect<Message> {
50    fn default() -> Self {
51        Self {
52            dirty: false,
53            message: None,
54        }
55    }
56}
57
58#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
59pub enum Direction {
60    Row,
61    #[default]
62    Column,
63}
64
65#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
66pub enum Length {
67    #[default]
68    Auto,
69    Fill,
70    Fixed(u16),
71}
72
73#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
74pub enum Alignment {
75    #[default]
76    Left,
77    Center,
78    Right,
79}
80
81#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
82pub enum Constraint {
83    Length(u16),
84    Percentage(u16),
85    Fill(u16),
86    Min(u16),
87    Max(u16),
88}
89
90#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
91pub enum Flex {
92    Legacy,
93    #[default]
94    Start,
95    End,
96    Center,
97    SpaceBetween,
98    SpaceAround,
99    SpaceEvenly,
100}
101
102impl From<u16> for Constraint {
103    fn from(value: u16) -> Self {
104        Self::Length(value)
105    }
106}
107
108#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
109pub struct Wrap {
110    pub trim: bool,
111}
112
113#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
114pub struct Padding {
115    pub top: u16,
116    pub right: u16,
117    pub bottom: u16,
118    pub left: u16,
119}
120
121impl Padding {
122    pub const fn zero() -> Self {
123        Self {
124            top: 0,
125            right: 0,
126            bottom: 0,
127            left: 0,
128        }
129    }
130
131    pub const fn all(value: u16) -> Self {
132        Self {
133            top: value,
134            right: value,
135            bottom: value,
136            left: value,
137        }
138    }
139
140    pub const fn symmetric(horizontal: u16, vertical: u16) -> Self {
141        Self {
142            top: vertical,
143            right: horizontal,
144            bottom: vertical,
145            left: horizontal,
146        }
147    }
148}
149
150#[derive(Clone, Copy, Debug, PartialEq, Eq)]
151pub struct Layout {
152    pub width: Length,
153    pub height: Length,
154}
155
156impl Default for Layout {
157    fn default() -> Self {
158        Self {
159            width: Length::Fill,
160            height: Length::Auto,
161        }
162    }
163}
164
165#[derive(Clone, Copy, Debug, PartialEq, Eq)]
166pub enum ChildLayoutKind {
167    Fill,
168    Stack { direction: Direction, gap: u16 },
169    Shell,
170}
171
172#[derive(Clone, Copy, Debug, PartialEq, Eq)]
173pub struct ChildLayoutSpec {
174    pub bounds: Rect,
175    pub kind: ChildLayoutKind,
176}
177
178#[derive(Clone, Copy, Debug, PartialEq, Eq)]
179pub struct BoxProps {
180    pub direction: Direction,
181    pub gap: u16,
182}
183
184#[derive(Clone, Debug, PartialEq, Eq)]
185pub struct TextProps {
186    pub content: String,
187}
188
189#[derive(Clone, Debug, PartialEq, Eq)]
190pub struct PaneProps {
191    pub title: Option<String>,
192}
193
194#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
195pub struct Borders(u8);
196
197impl Borders {
198    pub const NONE: Self = Self(0);
199    pub const TOP: Self = Self(0b0001);
200    pub const RIGHT: Self = Self(0b0010);
201    pub const BOTTOM: Self = Self(0b0100);
202    pub const LEFT: Self = Self(0b1000);
203    pub const ALL: Self = Self(Self::TOP.0 | Self::RIGHT.0 | Self::BOTTOM.0 | Self::LEFT.0);
204
205    pub const fn contains(self, other: Self) -> bool {
206        self.0 & other.0 == other.0
207    }
208
209    pub const fn intersects(self, other: Self) -> bool {
210        self.0 & other.0 != 0
211    }
212
213    pub const fn is_empty(self) -> bool {
214        self.0 == 0
215    }
216}
217
218impl BitOr for Borders {
219    type Output = Self;
220
221    fn bitor(self, rhs: Self) -> Self::Output {
222        Self(self.0 | rhs.0)
223    }
224}
225
226impl BitOrAssign for Borders {
227    fn bitor_assign(&mut self, rhs: Self) {
228        self.0 |= rhs.0;
229    }
230}
231
232#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
233pub enum BorderType {
234    #[default]
235    Plain,
236    Rounded,
237    Double,
238    Thick,
239}
240
241#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
242pub enum TitlePosition {
243    #[default]
244    Top,
245    Bottom,
246}
247
248#[derive(Clone, Debug, PartialEq, Eq, Hash)]
249pub struct BlockTitle {
250    pub position: Option<TitlePosition>,
251    pub content: Line,
252}
253
254impl BlockTitle {
255    pub fn new<T>(content: T) -> Self
256    where
257        T: Into<Line>,
258    {
259        Self {
260            position: None,
261            content: content.into(),
262        }
263    }
264
265    pub fn top<T>(content: T) -> Self
266    where
267        T: Into<Line>,
268    {
269        Self {
270            position: Some(TitlePosition::Top),
271            content: content.into(),
272        }
273    }
274
275    pub fn bottom<T>(content: T) -> Self
276    where
277        T: Into<Line>,
278    {
279        Self {
280            position: Some(TitlePosition::Bottom),
281            content: content.into(),
282        }
283    }
284}
285
286#[derive(Clone, Debug, PartialEq, Eq)]
287pub struct BlockProps {
288    pub titles: Vec<BlockTitle>,
289    pub title_alignment: Alignment,
290    pub title_position: TitlePosition,
291    pub borders: Borders,
292    pub border_type: BorderType,
293    pub border_set: Option<crate::symbols::border::Set>,
294    pub padding: Padding,
295    pub border_style: Style,
296    pub title_style: Style,
297}
298
299impl BlockProps {
300    pub fn has_title_at_position(&self, position: TitlePosition) -> bool {
301        self.titles
302            .iter()
303            .any(|title| title.position.unwrap_or(self.title_position) == position)
304    }
305
306    pub fn inner(&self, rect: Rect) -> Rect {
307        let left = u16::from(self.borders.contains(Borders::LEFT));
308        let right = u16::from(self.borders.contains(Borders::RIGHT));
309        let top = u16::from(
310            self.borders.contains(Borders::TOP) || self.has_title_at_position(TitlePosition::Top),
311        );
312        let bottom = u16::from(
313            self.borders.contains(Borders::BOTTOM)
314                || self.has_title_at_position(TitlePosition::Bottom),
315        );
316
317        Rect::new(
318            rect.x.saturating_add(left),
319            rect.y.saturating_add(top),
320            rect.width.saturating_sub(left.saturating_add(right)),
321            rect.height.saturating_sub(top.saturating_add(bottom)),
322        )
323        .inset(self.padding)
324    }
325}
326
327#[derive(Clone, Debug, PartialEq, Eq)]
328pub struct BlockFrame {
329    pub props: BlockProps,
330    pub style: Style,
331}
332
333impl BlockFrame {
334    pub fn inner(&self, rect: Rect) -> Rect {
335        self.props.inner(rect)
336    }
337}
338
339#[derive(Clone, Debug, PartialEq, Eq)]
340pub struct ParagraphProps {
341    pub content: Text,
342    pub block: Option<BlockFrame>,
343    pub alignment: Alignment,
344    pub wrap: Option<Wrap>,
345    pub scroll_x: u16,
346    pub scroll_y: u16,
347}
348
349#[derive(Clone, Debug, PartialEq, Eq)]
350pub struct RichTextProps {
351    pub block: HistoryBlock,
352}
353
354#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
355pub enum HighlightSpacing {
356    Always,
357    #[default]
358    WhenSelected,
359    Never,
360}
361
362impl HighlightSpacing {
363    pub const fn should_add(&self, has_selection: bool) -> bool {
364        match self {
365            Self::Always => true,
366            Self::WhenSelected => has_selection,
367            Self::Never => false,
368        }
369    }
370}
371
372#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
373pub enum ListDirection {
374    #[default]
375    TopToBottom,
376    BottomToTop,
377}
378
379#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
380pub struct ListItem {
381    pub content: Text,
382    pub style: Style,
383}
384
385impl ListItem {
386    pub fn new<T>(content: T) -> Self
387    where
388        T: Into<Text>,
389    {
390        Self {
391            content: content.into(),
392            style: Style::default(),
393        }
394    }
395
396    pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
397        self.style = style.into();
398        self
399    }
400
401    pub fn height(&self) -> usize {
402        self.content.height()
403    }
404
405    pub fn width(&self) -> usize {
406        self.content.width()
407    }
408}
409
410impl<T> From<T> for ListItem
411where
412    T: Into<Text>,
413{
414    fn from(value: T) -> Self {
415        Self::new(value)
416    }
417}
418
419#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
420pub struct ListState {
421    offset: usize,
422    selected: Option<usize>,
423}
424
425impl ListState {
426    pub const fn with_offset(mut self, offset: usize) -> Self {
427        self.offset = offset;
428        self
429    }
430
431    pub const fn with_selected(mut self, selected: Option<usize>) -> Self {
432        self.selected = selected;
433        self
434    }
435
436    pub const fn offset(&self) -> usize {
437        self.offset
438    }
439
440    pub const fn offset_mut(&mut self) -> &mut usize {
441        &mut self.offset
442    }
443
444    pub const fn selected(&self) -> Option<usize> {
445        self.selected
446    }
447
448    pub const fn selected_mut(&mut self) -> &mut Option<usize> {
449        &mut self.selected
450    }
451
452    pub const fn select(&mut self, index: Option<usize>) {
453        self.selected = index;
454        if index.is_none() {
455            self.offset = 0;
456        }
457    }
458
459    pub fn select_next(&mut self) {
460        let next = self.selected.map_or(0, |i| i.saturating_add(1));
461        self.select(Some(next));
462    }
463
464    pub fn select_previous(&mut self) {
465        let previous = self.selected.map_or(usize::MAX, |i| i.saturating_sub(1));
466        self.select(Some(previous));
467    }
468
469    pub const fn select_first(&mut self) {
470        self.select(Some(0));
471    }
472
473    pub const fn select_last(&mut self) {
474        self.select(Some(usize::MAX));
475    }
476
477    pub fn scroll_down_by(&mut self, amount: u16) {
478        let selected = self.selected.unwrap_or_default();
479        self.select(Some(selected.saturating_add(amount as usize)));
480    }
481
482    pub fn scroll_up_by(&mut self, amount: u16) {
483        let selected = self.selected.unwrap_or_default();
484        self.select(Some(selected.saturating_sub(amount as usize)));
485    }
486}
487
488pub struct ListProps<Message> {
489    pub block: Option<BlockFrame>,
490    pub items: Vec<ListItem>,
491    pub state: ListState,
492    pub highlight_symbol: Option<Line>,
493    pub highlight_style: Style,
494    pub highlight_spacing: HighlightSpacing,
495    pub repeat_highlight_symbol: bool,
496    pub direction: ListDirection,
497    pub scroll_padding: usize,
498    pub on_select: Option<SelectHandler<Message>>,
499}
500
501impl<Message> fmt::Debug for ListProps<Message> {
502    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
503        f.debug_struct("ListProps")
504            .field("block", &self.block)
505            .field("items", &self.items)
506            .field("state", &self.state)
507            .field("highlight_symbol", &self.highlight_symbol)
508            .field("highlight_style", &self.highlight_style)
509            .field("highlight_spacing", &self.highlight_spacing)
510            .field("repeat_highlight_symbol", &self.repeat_highlight_symbol)
511            .field("direction", &self.direction)
512            .field("scroll_padding", &self.scroll_padding)
513            .finish()
514    }
515}
516
517pub struct TabsProps<Message> {
518    pub block: Option<BlockFrame>,
519    pub titles: Vec<Line>,
520    pub selected: Option<usize>,
521    pub selection_explicit: bool,
522    pub highlight_style: Style,
523    pub divider: Span,
524    pub padding_left: Line,
525    pub padding_right: Line,
526    pub on_select: Option<SelectHandler<Message>>,
527}
528
529impl<Message> fmt::Debug for TabsProps<Message> {
530    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
531        f.debug_struct("TabsProps")
532            .field("block", &self.block)
533            .field("titles", &self.titles)
534            .field("selected", &self.selected)
535            .field("selection_explicit", &self.selection_explicit)
536            .field("highlight_style", &self.highlight_style)
537            .field("divider", &self.divider)
538            .field("padding_left", &self.padding_left)
539            .field("padding_right", &self.padding_right)
540            .finish()
541    }
542}
543
544#[derive(Clone, Debug, PartialEq)]
545pub struct GaugeProps {
546    pub block: Option<BlockFrame>,
547    pub ratio: f64,
548    pub label: Option<Span>,
549    pub use_unicode: bool,
550    pub gauge_style: Style,
551}
552
553#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
554pub struct ClearProps;
555
556#[derive(Clone, Debug, PartialEq)]
557pub struct LineGaugeProps {
558    pub block: Option<BlockFrame>,
559    pub ratio: f64,
560    pub label: Option<Line>,
561    pub filled_symbol: String,
562    pub unfilled_symbol: String,
563    pub filled_style: Style,
564    pub unfilled_style: Style,
565}
566
567#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
568pub enum TableAlignment {
569    #[default]
570    Left,
571    Center,
572    Right,
573}
574
575#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
576pub struct TableState {
577    offset: usize,
578    selected: Option<usize>,
579    selected_column: Option<usize>,
580}
581
582impl TableState {
583    pub const fn new() -> Self {
584        Self {
585            offset: 0,
586            selected: None,
587            selected_column: None,
588        }
589    }
590
591    pub const fn with_offset(mut self, offset: usize) -> Self {
592        self.offset = offset;
593        self
594    }
595
596    pub fn with_selected(mut self, selected: impl Into<Option<usize>>) -> Self {
597        self.selected = selected.into();
598        self
599    }
600
601    pub fn with_selected_column(mut self, selected: impl Into<Option<usize>>) -> Self {
602        self.selected_column = selected.into();
603        self
604    }
605
606    pub fn with_selected_cell(mut self, selected: impl Into<Option<(usize, usize)>>) -> Self {
607        if let Some((row, column)) = selected.into() {
608            self.selected = Some(row);
609            self.selected_column = Some(column);
610        } else {
611            self.selected = None;
612            self.selected_column = None;
613        }
614        self
615    }
616
617    pub const fn offset(&self) -> usize {
618        self.offset
619    }
620
621    pub const fn offset_mut(&mut self) -> &mut usize {
622        &mut self.offset
623    }
624
625    pub const fn selected(&self) -> Option<usize> {
626        self.selected
627    }
628
629    pub const fn selected_column(&self) -> Option<usize> {
630        self.selected_column
631    }
632
633    pub const fn selected_cell(&self) -> Option<(usize, usize)> {
634        if let (Some(row), Some(column)) = (self.selected, self.selected_column) {
635            Some((row, column))
636        } else {
637            None
638        }
639    }
640
641    pub const fn selected_mut(&mut self) -> &mut Option<usize> {
642        &mut self.selected
643    }
644
645    pub const fn selected_column_mut(&mut self) -> &mut Option<usize> {
646        &mut self.selected_column
647    }
648
649    pub const fn select(&mut self, index: Option<usize>) {
650        self.selected = index;
651        if index.is_none() {
652            self.offset = 0;
653        }
654    }
655
656    pub const fn select_column(&mut self, index: Option<usize>) {
657        self.selected_column = index;
658    }
659
660    pub const fn select_cell(&mut self, indexes: Option<(usize, usize)>) {
661        if let Some((row, column)) = indexes {
662            self.selected = Some(row);
663            self.selected_column = Some(column);
664        } else {
665            self.offset = 0;
666            self.selected = None;
667            self.selected_column = None;
668        }
669    }
670
671    pub fn select_next(&mut self) {
672        let next = self.selected.map_or(0, |i| i.saturating_add(1));
673        self.select(Some(next));
674    }
675
676    pub fn select_next_column(&mut self) {
677        let next = self.selected_column.map_or(0, |i| i.saturating_add(1));
678        self.select_column(Some(next));
679    }
680
681    pub fn select_previous(&mut self) {
682        let previous = self.selected.map_or(usize::MAX, |i| i.saturating_sub(1));
683        self.select(Some(previous));
684    }
685
686    pub fn select_previous_column(&mut self) {
687        let previous = self
688            .selected_column
689            .map_or(usize::MAX, |i| i.saturating_sub(1));
690        self.select_column(Some(previous));
691    }
692
693    pub const fn select_first(&mut self) {
694        self.select(Some(0));
695    }
696
697    pub const fn select_first_column(&mut self) {
698        self.select_column(Some(0));
699    }
700
701    pub const fn select_last(&mut self) {
702        self.select(Some(usize::MAX));
703    }
704
705    pub const fn select_last_column(&mut self) {
706        self.select_column(Some(usize::MAX));
707    }
708
709    pub fn scroll_down_by(&mut self, amount: u16) {
710        let selected = self.selected.unwrap_or_default();
711        self.select(Some(selected.saturating_add(amount as usize)));
712    }
713
714    pub fn scroll_up_by(&mut self, amount: u16) {
715        let selected = self.selected.unwrap_or_default();
716        self.select(Some(selected.saturating_sub(amount as usize)));
717    }
718
719    pub fn scroll_right_by(&mut self, amount: u16) {
720        let selected = self.selected_column.unwrap_or_default();
721        self.select_column(Some(selected.saturating_add(amount as usize)));
722    }
723
724    pub fn scroll_left_by(&mut self, amount: u16) {
725        let selected = self.selected_column.unwrap_or_default();
726        self.select_column(Some(selected.saturating_sub(amount as usize)));
727    }
728}
729
730pub struct TableProps<Message> {
731    pub block: Option<BlockFrame>,
732    pub header: Option<Row>,
733    pub footer: Option<Row>,
734    pub rows: Vec<Row>,
735    pub widths: Vec<Constraint>,
736    pub column_spacing: u16,
737    pub flex: Flex,
738    pub alignments: Vec<TableAlignment>,
739    pub state: TableState,
740    pub highlight_symbol: Option<Text>,
741    pub row_highlight_style: Style,
742    pub column_highlight_style: Style,
743    pub cell_highlight_style: Style,
744    pub highlight_spacing: HighlightSpacing,
745    pub on_select: Option<SelectHandler<Message>>,
746}
747
748impl<Message> fmt::Debug for TableProps<Message> {
749    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
750        f.debug_struct("TableProps")
751            .field("block", &self.block)
752            .field("header", &self.header)
753            .field("footer", &self.footer)
754            .field("rows", &self.rows)
755            .field("widths", &self.widths)
756            .field("column_spacing", &self.column_spacing)
757            .field("flex", &self.flex)
758            .field("alignments", &self.alignments)
759            .field("state", &self.state)
760            .field("highlight_symbol", &self.highlight_symbol)
761            .field("row_highlight_style", &self.row_highlight_style)
762            .field("column_highlight_style", &self.column_highlight_style)
763            .field("cell_highlight_style", &self.cell_highlight_style)
764            .field("highlight_spacing", &self.highlight_spacing)
765            .finish()
766    }
767}
768
769#[derive(Clone, Debug, PartialEq, Eq)]
770pub enum SparklineDirection {
771    LeftToRight,
772    RightToLeft,
773}
774
775#[derive(Clone, Debug, PartialEq, Eq)]
776pub struct SparklineProps {
777    pub values: Vec<Option<u64>>,
778    pub max: Option<u64>,
779    pub direction: SparklineDirection,
780    pub absent_value_symbol: char,
781    pub absent_value_style: Style,
782}
783
784#[derive(Clone, Debug, PartialEq, Eq)]
785pub struct Bar {
786    pub label: String,
787    pub value: u64,
788}
789
790#[derive(Clone, Debug, PartialEq, Eq)]
791pub struct BarChartProps {
792    pub bars: Vec<Bar>,
793    pub max: Option<u64>,
794    pub bar_width: u16,
795}
796
797#[derive(Clone, Debug, PartialEq, Eq)]
798pub struct ChartDataset {
799    pub label: Option<String>,
800    pub points: Vec<(i64, i64)>,
801}
802
803#[derive(Clone, Debug, PartialEq, Eq)]
804pub struct ChartProps {
805    pub datasets: Vec<ChartDataset>,
806    pub min_y: Option<i64>,
807    pub max_y: Option<i64>,
808}
809
810#[derive(Clone, Debug, PartialEq, Eq)]
811pub struct CanvasCell {
812    pub x: u16,
813    pub y: u16,
814    pub symbol: char,
815    pub style: Style,
816}
817
818#[derive(Clone, Debug, PartialEq, Eq)]
819pub struct CanvasProps {
820    pub width: u16,
821    pub height: u16,
822    pub cells: Vec<CanvasCell>,
823}
824
825#[derive(Clone, Debug, PartialEq, Eq)]
826pub struct MonthlyProps {
827    pub year: i32,
828    pub month: u8,
829    pub selected_day: Option<u8>,
830}
831
832#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
833pub enum ScrollbarOrientation {
834    #[default]
835    VerticalRight,
836    VerticalLeft,
837    HorizontalBottom,
838    HorizontalTop,
839}
840
841impl ScrollbarOrientation {
842    pub const fn is_vertical(self) -> bool {
843        matches!(self, Self::VerticalRight | Self::VerticalLeft)
844    }
845
846    pub const fn is_horizontal(self) -> bool {
847        matches!(self, Self::HorizontalBottom | Self::HorizontalTop)
848    }
849}
850
851#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
852pub enum ScrollDirection {
853    #[default]
854    Forward,
855    Backward,
856}
857
858#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
859pub struct ScrollbarState {
860    content_length: usize,
861    position: usize,
862    viewport_content_length: usize,
863}
864
865impl ScrollbarState {
866    pub const fn new(content_length: usize) -> Self {
867        Self {
868            content_length,
869            position: 0,
870            viewport_content_length: 0,
871        }
872    }
873
874    pub const fn content_length(mut self, content_length: usize) -> Self {
875        self.content_length = content_length;
876        self
877    }
878
879    pub const fn content_length_value(&self) -> usize {
880        self.content_length
881    }
882
883    pub const fn get_position(&self) -> usize {
884        self.position
885    }
886
887    pub const fn viewport_content_length_value(&self) -> usize {
888        self.viewport_content_length
889    }
890
891    pub const fn with_content_length(mut self, content_length: usize) -> Self {
892        self.content_length = content_length;
893        self
894    }
895
896    pub const fn with_position(mut self, position: usize) -> Self {
897        self.position = position;
898        self
899    }
900
901    pub const fn with_viewport_content_length(mut self, viewport_content_length: usize) -> Self {
902        self.viewport_content_length = viewport_content_length;
903        self
904    }
905
906    pub const fn position(mut self, position: usize) -> Self {
907        self.position = position;
908        self
909    }
910
911    pub const fn viewport_content_length(mut self, viewport_content_length: usize) -> Self {
912        self.viewport_content_length = viewport_content_length;
913        self
914    }
915
916    pub const fn content_length_mut(&mut self) -> &mut usize {
917        &mut self.content_length
918    }
919
920    pub const fn position_mut(&mut self) -> &mut usize {
921        &mut self.position
922    }
923
924    pub const fn viewport_content_length_mut(&mut self) -> &mut usize {
925        &mut self.viewport_content_length
926    }
927
928    pub const fn prev(&mut self) {
929        self.position = self.position.saturating_sub(1);
930    }
931
932    pub fn next(&mut self) {
933        self.position = self
934            .position
935            .saturating_add(1)
936            .min(self.content_length.saturating_sub(1));
937    }
938
939    pub const fn first(&mut self) {
940        self.position = 0;
941    }
942
943    pub const fn last(&mut self) {
944        self.position = self.content_length.saturating_sub(1);
945    }
946
947    pub fn scroll(&mut self, direction: ScrollDirection) {
948        match direction {
949            ScrollDirection::Forward => self.next(),
950            ScrollDirection::Backward => self.prev(),
951        }
952    }
953}
954
955pub struct ScrollViewProps<Message> {
956    pub follow_bottom: bool,
957    pub offset: Option<usize>,
958    pub on_scroll: Option<ScrollHandler<Message>>,
959}
960
961impl<Message> fmt::Debug for ScrollViewProps<Message> {
962    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
963        f.debug_struct("ScrollViewProps")
964            .field("follow_bottom", &self.follow_bottom)
965            .field("offset", &self.offset)
966            .finish()
967    }
968}
969
970pub struct ScrollbarProps<Message> {
971    pub orientation: ScrollbarOrientation,
972    pub state: ScrollbarState,
973    pub thumb_symbol: String,
974    pub thumb_style: Style,
975    pub track_symbol: Option<String>,
976    pub track_style: Style,
977    pub begin_symbol: Option<String>,
978    pub begin_style: Style,
979    pub end_symbol: Option<String>,
980    pub end_style: Style,
981    pub on_scroll: Option<ScrollHandler<Message>>,
982}
983
984impl<Message> fmt::Debug for ScrollbarProps<Message> {
985    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
986        f.debug_struct("ScrollbarProps")
987            .field("orientation", &self.orientation)
988            .field("state", &self.state)
989            .field("thumb_symbol", &self.thumb_symbol)
990            .field("track_symbol", &self.track_symbol)
991            .field("begin_symbol", &self.begin_symbol)
992            .field("end_symbol", &self.end_symbol)
993            .finish()
994    }
995}
996
997#[derive(Clone, Debug, PartialEq, Eq)]
998pub struct StreamingTextProps {
999    pub content: String,
1000}
1001
1002pub struct InputProps<Message> {
1003    pub value: String,
1004    pub placeholder: String,
1005    pub on_change: Option<ChangeHandler>,
1006    pub on_submit: Option<SubmitHandler<Message>>,
1007    pub cursor: usize,
1008}
1009
1010impl<Message> fmt::Debug for InputProps<Message> {
1011    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1012        f.debug_struct("InputProps")
1013            .field("value", &self.value)
1014            .field("placeholder", &self.placeholder)
1015            .field("cursor", &self.cursor)
1016            .finish()
1017    }
1018}
1019
1020#[derive(Clone, Debug, PartialEq, Eq)]
1021pub struct StatusBarProps {
1022    pub content: String,
1023}
1024
1025#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1026pub struct ShellProps;
1027
1028pub struct ComponentProps<Message> {
1029    pub name: String,
1030    pub scope: Option<ScopeId>,
1031    pub renderer: ComponentRenderer<Message>,
1032}
1033
1034impl<Message> fmt::Debug for ComponentProps<Message> {
1035    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1036        f.debug_struct("ComponentProps")
1037            .field("name", &self.name)
1038            .field("scope", &self.scope)
1039            .finish()
1040    }
1041}
1042
1043pub enum ComponentRenderer<Message> {
1044    Static(Rc<dyn Fn() -> Element<Message>>),
1045    WithCx(Rc<dyn for<'a> Fn(&mut crate::ViewCtx<'a, Message>) -> Element<Message>>),
1046}
1047
1048impl<Message> Clone for ComponentRenderer<Message> {
1049    fn clone(&self) -> Self {
1050        match self {
1051            Self::Static(renderer) => Self::Static(renderer.clone()),
1052            Self::WithCx(renderer) => Self::WithCx(renderer.clone()),
1053        }
1054    }
1055}
1056
1057pub enum ElementKind<Message> {
1058    Box(BoxProps),
1059    Text(TextProps),
1060    Pane(PaneProps),
1061    Block(BlockProps),
1062    Paragraph(ParagraphProps),
1063    RichText(RichTextProps),
1064    List(ListProps<Message>),
1065    Tabs(TabsProps<Message>),
1066    Gauge(GaugeProps),
1067    Clear(ClearProps),
1068    LineGauge(LineGaugeProps),
1069    Table(TableProps<Message>),
1070    Sparkline(SparklineProps),
1071    BarChart(BarChartProps),
1072    Chart(ChartProps),
1073    Canvas(CanvasProps),
1074    Monthly(MonthlyProps),
1075    ScrollView(ScrollViewProps<Message>),
1076    Scrollbar(ScrollbarProps<Message>),
1077    StreamingText(StreamingTextProps),
1078    Input(InputProps<Message>),
1079    StatusBar(StatusBarProps),
1080    Shell(ShellProps),
1081    Component(ComponentProps<Message>),
1082}
1083
1084impl<Message> ElementKind<Message> {
1085    pub fn child_layout_spec(&self, bounds: Rect) -> ChildLayoutSpec {
1086        match self {
1087            Self::Box(props) => ChildLayoutSpec {
1088                bounds,
1089                kind: ChildLayoutKind::Stack {
1090                    direction: props.direction,
1091                    gap: props.gap,
1092                },
1093            },
1094            Self::Shell(_) => ChildLayoutSpec {
1095                bounds,
1096                kind: ChildLayoutKind::Shell,
1097            },
1098            Self::Pane(_) => ChildLayoutSpec {
1099                bounds: bounds.shrink(1),
1100                kind: ChildLayoutKind::Fill,
1101            },
1102            Self::Block(props) => ChildLayoutSpec {
1103                bounds: props.inner(bounds),
1104                kind: ChildLayoutKind::Stack {
1105                    direction: Direction::Column,
1106                    gap: 0,
1107                },
1108            },
1109            Self::ScrollView(_) | Self::Component(_) => ChildLayoutSpec {
1110                bounds,
1111                kind: ChildLayoutKind::Fill,
1112            },
1113            _ => ChildLayoutSpec {
1114                bounds,
1115                kind: ChildLayoutKind::Fill,
1116            },
1117        }
1118    }
1119
1120    pub fn capture_runtime_state(&self) -> Option<RuntimeWidgetState> {
1121        match self {
1122            Self::Input(props) => Some(RuntimeWidgetState::InputCursor(
1123                props.cursor.min(props.value.chars().count()),
1124            )),
1125            Self::List(props) => Some(RuntimeWidgetState::List(props.state)),
1126            Self::Tabs(props) => Some(RuntimeWidgetState::Tabs(props.selected)),
1127            Self::Table(props) => Some(RuntimeWidgetState::Table(props.state)),
1128            Self::ScrollView(props) => Some(RuntimeWidgetState::ScrollView(props.offset)),
1129            Self::Scrollbar(props) => Some(RuntimeWidgetState::Scrollbar(props.state)),
1130            _ => None,
1131        }
1132    }
1133
1134    pub fn restore_runtime_state(&mut self, state: &RuntimeWidgetState) -> bool {
1135        match (self, state) {
1136            (Self::Input(props), RuntimeWidgetState::InputCursor(cursor)) => {
1137                let max = props.value.chars().count();
1138                props.cursor = (*cursor).min(max);
1139                true
1140            }
1141            (Self::List(props), RuntimeWidgetState::List(previous))
1142                if props.state == ListState::default() && *previous != ListState::default() =>
1143            {
1144                props.state = *previous;
1145                true
1146            }
1147            (Self::Tabs(props), RuntimeWidgetState::Tabs(previous))
1148                if !props.selection_explicit && props.selected != *previous =>
1149            {
1150                props.selected = *previous;
1151                true
1152            }
1153            (Self::Table(props), RuntimeWidgetState::Table(previous))
1154                if props.state == TableState::default() && *previous != TableState::default() =>
1155            {
1156                props.state = *previous;
1157                true
1158            }
1159            (Self::ScrollView(props), RuntimeWidgetState::ScrollView(previous))
1160                if props.offset.is_none() && previous.is_some() =>
1161            {
1162                props.offset = *previous;
1163                true
1164            }
1165            (Self::Scrollbar(props), RuntimeWidgetState::Scrollbar(previous))
1166                if props.state.get_position() == 0 && previous.get_position() > 0 =>
1167            {
1168                props.state = *previous;
1169                true
1170            }
1171            _ => false,
1172        }
1173    }
1174
1175    pub fn initialize_runtime_state(&mut self) {
1176        match self {
1177            Self::Input(props) => {
1178                props.cursor = props.cursor.min(props.value.chars().count());
1179                if props.cursor == 0 {
1180                    props.cursor = props.value.chars().count();
1181                }
1182            }
1183            Self::Tabs(props) => {
1184                if props.selected.is_none() && !props.titles.is_empty() {
1185                    props.selected = Some(0);
1186                }
1187            }
1188            _ => {}
1189        }
1190    }
1191
1192    pub fn intrinsic_height(&self, width: u16, child_heights: &[u16]) -> u16 {
1193        match self {
1194            Self::Box(props) => match props.direction {
1195                Direction::Column => {
1196                    let gaps = props
1197                        .gap
1198                        .saturating_mul(child_heights.len().saturating_sub(1) as u16);
1199                    child_heights
1200                        .iter()
1201                        .copied()
1202                        .fold(0u16, u16::saturating_add)
1203                        .saturating_add(gaps)
1204                }
1205                Direction::Row => child_heights.iter().copied().max().unwrap_or(1),
1206            },
1207            Self::Pane(_) => child_heights
1208                .iter()
1209                .copied()
1210                .max()
1211                .unwrap_or(0)
1212                .saturating_add(2),
1213            Self::Block(props) => child_heights
1214                .iter()
1215                .copied()
1216                .fold(0u16, u16::saturating_add)
1217                .saturating_add(props.padding.top)
1218                .saturating_add(props.padding.bottom)
1219                .saturating_add(block_vertical_inset(props))
1220                .max(1),
1221            Self::Shell(_) => child_heights
1222                .iter()
1223                .copied()
1224                .fold(0u16, u16::saturating_add)
1225                .max(1),
1226            Self::ScrollView(_) | Self::Component(_) => child_heights.first().copied().unwrap_or(1),
1227            Self::Text(props) => wrapped_line_count(&props.content, width),
1228            Self::Paragraph(props) => paragraph_height(props, width),
1229            Self::RichText(props) => props.block.lines.len().max(1) as u16,
1230            Self::StreamingText(props) => wrapped_line_count(&props.content, width),
1231            Self::List(props) => list_height(props),
1232            Self::Tabs(props) => tabs_height(props),
1233            Self::Gauge(props) => block_height(props.block.as_ref(), 1),
1234            Self::LineGauge(props) => block_height(props.block.as_ref(), 1),
1235            Self::Sparkline(_) => 1,
1236            Self::BarChart(_) => 6,
1237            Self::Chart(_) | Self::Canvas(_) | Self::Monthly(_) => 8,
1238            Self::Clear(_) => 1,
1239            Self::Table(props) => table_height(props),
1240            Self::Scrollbar(_) => 1,
1241            Self::StatusBar(props) => wrapped_line_count(&props.content, width),
1242            Self::Input(_) => 3,
1243        }
1244    }
1245
1246    pub fn intrinsic_width(&self, child_widths: &[u16]) -> u16 {
1247        match self {
1248            Self::Box(props) => match props.direction {
1249                Direction::Column => child_widths.iter().copied().max().unwrap_or(1),
1250                Direction::Row => {
1251                    let gaps = props
1252                        .gap
1253                        .saturating_mul(child_widths.len().saturating_sub(1) as u16);
1254                    child_widths
1255                        .iter()
1256                        .copied()
1257                        .fold(0u16, u16::saturating_add)
1258                        .saturating_add(gaps)
1259                        .max(1)
1260                }
1261            },
1262            Self::Pane(props) => child_widths
1263                .iter()
1264                .copied()
1265                .max()
1266                .unwrap_or(0)
1267                .max(title_width(props.title.as_deref()))
1268                .saturating_add(2)
1269                .max(1),
1270            Self::Block(props) => child_widths
1271                .iter()
1272                .copied()
1273                .max()
1274                .unwrap_or(0)
1275                .max(block_title_width(props))
1276                .saturating_add(props.padding.left)
1277                .saturating_add(props.padding.right)
1278                .saturating_add(border_horizontal_inset(props.borders))
1279                .max(1),
1280            Self::Shell(_) => child_widths.iter().copied().max().unwrap_or(1),
1281            Self::ScrollView(_) | Self::Component(_) => child_widths.first().copied().unwrap_or(1),
1282            Self::Text(props) => plain_text_width(&props.content).max(1),
1283            Self::Paragraph(props) => paragraph_width(props).max(1),
1284            Self::RichText(props) => rich_text_width(&props.block).max(1),
1285            Self::StreamingText(props) => plain_text_width(&props.content).max(1),
1286            Self::List(props) => list_width(props).max(1),
1287            Self::Tabs(props) => tabs_width(props).max(1),
1288            Self::Gauge(props) => block_width(
1289                props.block.as_ref(),
1290                props.label.as_ref().map_or(1, |label| label.width() as u16),
1291            ),
1292            Self::LineGauge(props) => block_width(
1293                props.block.as_ref(),
1294                props.label.as_ref().map_or(1, |label| label.width() as u16),
1295            ),
1296            Self::Sparkline(props) => props.values.len().max(1) as u16,
1297            Self::BarChart(props) => {
1298                let bars = props.bars.len() as u16;
1299                bars.saturating_mul(props.bar_width.max(1))
1300                    .saturating_add(bars.saturating_sub(1))
1301                    .max(1)
1302            }
1303            Self::Chart(_) | Self::Canvas(_) | Self::Monthly(_) => 8,
1304            Self::Clear(_) => 1,
1305            Self::Table(props) => table_width(props).max(1),
1306            Self::Scrollbar(props) => {
1307                if props.orientation.is_vertical() {
1308                    1
1309                } else {
1310                    props
1311                        .state
1312                        .viewport_content_length_value()
1313                        .max(1)
1314                        .try_into()
1315                        .unwrap_or(u16::MAX)
1316                }
1317            }
1318            Self::StatusBar(props) => plain_text_width(&props.content).max(1),
1319            Self::Input(props) => plain_text_width(if props.value.is_empty() {
1320                &props.placeholder
1321            } else {
1322                &props.value
1323            })
1324            .saturating_add(2)
1325            .max(1),
1326        }
1327    }
1328
1329    pub fn invalidates_self_on_layout_change(&self, style: Style) -> bool {
1330        match self {
1331            Self::Box(_) | Self::Shell(_) | Self::ScrollView(_) => style != Style::default(),
1332            Self::Component(_) => false,
1333            _ => true,
1334        }
1335    }
1336
1337    pub fn route_widget_key(
1338        &mut self,
1339        key: WidgetKey,
1340        context: WidgetRouteContext,
1341    ) -> Option<WidgetRouteEffect<Message>> {
1342        match self {
1343            Self::List(props) => {
1344                match key {
1345                    WidgetKey::Up => step_list_selection(&mut props.state, props.items.len(), -1)
1346                        .map(|index| {
1347                            sync_list_offset(props, context.viewport_height);
1348                            WidgetRouteEffect {
1349                                dirty: true,
1350                                message: props
1351                                    .on_select
1352                                    .as_mut()
1353                                    .and_then(|on_select| on_select(index)),
1354                            }
1355                        }),
1356                    WidgetKey::Down => step_list_selection(&mut props.state, props.items.len(), 1)
1357                        .map(|index| {
1358                            sync_list_offset(props, context.viewport_height);
1359                            WidgetRouteEffect {
1360                                dirty: true,
1361                                message: props
1362                                    .on_select
1363                                    .as_mut()
1364                                    .and_then(|on_select| on_select(index)),
1365                            }
1366                        }),
1367                    WidgetKey::Enter => Some(WidgetRouteEffect {
1368                        message: props.state.selected().and_then(|index| {
1369                            props
1370                                .on_select
1371                                .as_mut()
1372                                .and_then(|on_select| on_select(index))
1373                        }),
1374                        ..WidgetRouteEffect::default()
1375                    }),
1376                    _ => None,
1377                }
1378            }
1379            Self::Tabs(props) => match key {
1380                WidgetKey::Left => step_selected_index(&mut props.selected, props.titles.len(), -1)
1381                    .map(|index| WidgetRouteEffect {
1382                        dirty: true,
1383                        message: props
1384                            .on_select
1385                            .as_mut()
1386                            .and_then(|on_select| on_select(index)),
1387                    }),
1388                WidgetKey::Right => step_selected_index(&mut props.selected, props.titles.len(), 1)
1389                    .map(|index| WidgetRouteEffect {
1390                        dirty: true,
1391                        message: props
1392                            .on_select
1393                            .as_mut()
1394                            .and_then(|on_select| on_select(index)),
1395                    }),
1396                WidgetKey::Enter => Some(WidgetRouteEffect {
1397                    message: props.selected.and_then(|selected| {
1398                        props
1399                            .on_select
1400                            .as_mut()
1401                            .and_then(|on_select| on_select(selected))
1402                    }),
1403                    ..WidgetRouteEffect::default()
1404                }),
1405                _ => None,
1406            },
1407            Self::Table(props) => {
1408                match key {
1409                    WidgetKey::Up => step_table_selection(&mut props.state, props.rows.len(), -1)
1410                        .map(|index| {
1411                            sync_table_offset(props, context.viewport_height);
1412                            WidgetRouteEffect {
1413                                dirty: true,
1414                                message: props
1415                                    .on_select
1416                                    .as_mut()
1417                                    .and_then(|on_select| on_select(index)),
1418                            }
1419                        }),
1420                    WidgetKey::Down => step_table_selection(&mut props.state, props.rows.len(), 1)
1421                        .map(|index| {
1422                            sync_table_offset(props, context.viewport_height);
1423                            WidgetRouteEffect {
1424                                dirty: true,
1425                                message: props
1426                                    .on_select
1427                                    .as_mut()
1428                                    .and_then(|on_select| on_select(index)),
1429                            }
1430                        }),
1431                    WidgetKey::Enter => Some(WidgetRouteEffect {
1432                        message: props.state.selected().and_then(|index| {
1433                            props
1434                                .on_select
1435                                .as_mut()
1436                                .and_then(|on_select| on_select(index))
1437                        }),
1438                        ..WidgetRouteEffect::default()
1439                    }),
1440                    _ => None,
1441                }
1442            }
1443            Self::ScrollView(props) => {
1444                let max_offset = context.scroll_view_max_offset?;
1445                let current =
1446                    props
1447                        .offset
1448                        .unwrap_or(if props.follow_bottom { max_offset } else { 0 });
1449                let next = match key {
1450                    WidgetKey::Up => current.saturating_sub(1),
1451                    WidgetKey::Down => current.saturating_add(1).min(max_offset),
1452                    WidgetKey::Enter => current,
1453                    _ => return None,
1454                };
1455
1456                if matches!(key, WidgetKey::Enter) {
1457                    return Some(WidgetRouteEffect {
1458                        message: props
1459                            .on_scroll
1460                            .as_mut()
1461                            .and_then(|on_scroll| on_scroll(current)),
1462                        ..WidgetRouteEffect::default()
1463                    });
1464                }
1465
1466                if next == current {
1467                    return Some(WidgetRouteEffect::default());
1468                }
1469
1470                props.offset = if props.follow_bottom && next == max_offset {
1471                    None
1472                } else {
1473                    Some(next)
1474                };
1475
1476                Some(WidgetRouteEffect {
1477                    dirty: true,
1478                    message: props
1479                        .on_scroll
1480                        .as_mut()
1481                        .and_then(|on_scroll| on_scroll(next)),
1482                })
1483            }
1484            Self::Scrollbar(props) => {
1485                let max_position = props
1486                    .state
1487                    .content_length_value()
1488                    .saturating_sub(props.state.viewport_content_length_value());
1489                let current = props.state.get_position().min(max_position);
1490                let next = match key {
1491                    WidgetKey::Up
1492                        if matches!(
1493                            props.orientation,
1494                            ScrollbarOrientation::VerticalRight
1495                                | ScrollbarOrientation::VerticalLeft
1496                        ) =>
1497                    {
1498                        current.saturating_sub(1)
1499                    }
1500                    WidgetKey::Down
1501                        if matches!(
1502                            props.orientation,
1503                            ScrollbarOrientation::VerticalRight
1504                                | ScrollbarOrientation::VerticalLeft
1505                        ) =>
1506                    {
1507                        current.saturating_add(1).min(max_position)
1508                    }
1509                    WidgetKey::Left
1510                        if matches!(
1511                            props.orientation,
1512                            ScrollbarOrientation::HorizontalBottom
1513                                | ScrollbarOrientation::HorizontalTop
1514                        ) =>
1515                    {
1516                        current.saturating_sub(1)
1517                    }
1518                    WidgetKey::Right
1519                        if matches!(
1520                            props.orientation,
1521                            ScrollbarOrientation::HorizontalBottom
1522                                | ScrollbarOrientation::HorizontalTop
1523                        ) =>
1524                    {
1525                        current.saturating_add(1).min(max_position)
1526                    }
1527                    WidgetKey::Enter => current,
1528                    _ => return None,
1529                };
1530
1531                if matches!(key, WidgetKey::Enter) {
1532                    return Some(WidgetRouteEffect {
1533                        message: props
1534                            .on_scroll
1535                            .as_mut()
1536                            .and_then(|on_scroll| on_scroll(current)),
1537                        ..WidgetRouteEffect::default()
1538                    });
1539                }
1540
1541                if next == current {
1542                    return Some(WidgetRouteEffect::default());
1543                }
1544
1545                *props.state.position_mut() = next;
1546                Some(WidgetRouteEffect {
1547                    dirty: true,
1548                    message: props
1549                        .on_scroll
1550                        .as_mut()
1551                        .and_then(|on_scroll| on_scroll(next)),
1552                })
1553            }
1554            Self::Input(props) => match key {
1555                WidgetKey::Char(ch) => {
1556                    insert_char_at_cursor(&mut props.value, props.cursor, ch);
1557                    props.cursor = props.cursor.saturating_add(1);
1558                    if let Some(on_change) = props.on_change.as_mut() {
1559                        on_change(props.value.clone());
1560                    }
1561
1562                    Some(WidgetRouteEffect {
1563                        dirty: true,
1564                        ..WidgetRouteEffect::default()
1565                    })
1566                }
1567                WidgetKey::Backspace => {
1568                    if props.cursor == 0 {
1569                        return Some(WidgetRouteEffect::default());
1570                    }
1571
1572                    remove_char_before_cursor(&mut props.value, props.cursor);
1573                    props.cursor = props.cursor.saturating_sub(1);
1574                    if let Some(on_change) = props.on_change.as_mut() {
1575                        on_change(props.value.clone());
1576                    }
1577
1578                    Some(WidgetRouteEffect {
1579                        dirty: true,
1580                        ..WidgetRouteEffect::default()
1581                    })
1582                }
1583                WidgetKey::Left => {
1584                    let next = props.cursor.saturating_sub(1);
1585                    if next == props.cursor {
1586                        Some(WidgetRouteEffect::default())
1587                    } else {
1588                        props.cursor = next;
1589                        Some(WidgetRouteEffect {
1590                            dirty: true,
1591                            ..WidgetRouteEffect::default()
1592                        })
1593                    }
1594                }
1595                WidgetKey::Right => {
1596                    let max = props.value.chars().count();
1597                    if props.cursor >= max {
1598                        Some(WidgetRouteEffect::default())
1599                    } else {
1600                        props.cursor += 1;
1601                        Some(WidgetRouteEffect {
1602                            dirty: true,
1603                            ..WidgetRouteEffect::default()
1604                        })
1605                    }
1606                }
1607                WidgetKey::Enter => Some(WidgetRouteEffect {
1608                    message: props
1609                        .on_submit
1610                        .as_mut()
1611                        .and_then(|on_submit| on_submit(props.value.clone())),
1612                    ..WidgetRouteEffect::default()
1613                }),
1614                _ => None,
1615            },
1616            _ => None,
1617        }
1618    }
1619}
1620
1621impl<Message> fmt::Debug for ElementKind<Message> {
1622    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1623        match self {
1624            Self::Box(props) => f.debug_tuple("Box").field(props).finish(),
1625            Self::Text(props) => f.debug_tuple("Text").field(props).finish(),
1626            Self::Pane(props) => f.debug_tuple("Pane").field(props).finish(),
1627            Self::Block(props) => f.debug_tuple("Block").field(props).finish(),
1628            Self::Paragraph(props) => f.debug_tuple("Paragraph").field(props).finish(),
1629            Self::RichText(props) => f.debug_tuple("RichText").field(props).finish(),
1630            Self::List(props) => f.debug_tuple("List").field(props).finish(),
1631            Self::Tabs(props) => f.debug_tuple("Tabs").field(props).finish(),
1632            Self::Gauge(props) => f.debug_tuple("Gauge").field(props).finish(),
1633            Self::Clear(props) => f.debug_tuple("Clear").field(props).finish(),
1634            Self::LineGauge(props) => f.debug_tuple("LineGauge").field(props).finish(),
1635            Self::Table(props) => f.debug_tuple("Table").field(props).finish(),
1636            Self::Sparkline(props) => f.debug_tuple("Sparkline").field(props).finish(),
1637            Self::BarChart(props) => f.debug_tuple("BarChart").field(props).finish(),
1638            Self::Chart(props) => f.debug_tuple("Chart").field(props).finish(),
1639            Self::Canvas(props) => f.debug_tuple("Canvas").field(props).finish(),
1640            Self::Monthly(props) => f.debug_tuple("Monthly").field(props).finish(),
1641            Self::ScrollView(props) => f.debug_tuple("ScrollView").field(props).finish(),
1642            Self::Scrollbar(props) => f.debug_tuple("Scrollbar").field(props).finish(),
1643            Self::StreamingText(props) => f.debug_tuple("StreamingText").field(props).finish(),
1644            Self::Input(props) => f.debug_tuple("Input").field(props).finish(),
1645            Self::StatusBar(props) => f.debug_tuple("StatusBar").field(props).finish(),
1646            Self::Shell(props) => f.debug_tuple("Shell").field(props).finish(),
1647            Self::Component(props) => f.debug_tuple("Component").field(props).finish(),
1648        }
1649    }
1650}
1651
1652pub struct Element<Message> {
1653    pub kind: ElementKind<Message>,
1654    pub layout: Layout,
1655    pub style: Style,
1656    pub focusable: bool,
1657    pub continuity_key: Option<String>,
1658    pub children: Vec<Element<Message>>,
1659}
1660
1661pub trait IntoElement<Message> {
1662    fn into_element(self) -> Element<Message>;
1663}
1664
1665impl<Message> IntoElement<Message> for Element<Message> {
1666    fn into_element(self) -> Element<Message> {
1667        self
1668    }
1669}
1670
1671pub fn component<Message, F>(name: impl Into<String>, renderer: F) -> Element<Message>
1672where
1673    F: Fn() -> Element<Message> + 'static,
1674{
1675    Element::new(ElementKind::Component(ComponentProps {
1676        name: name.into(),
1677        scope: None,
1678        renderer: ComponentRenderer::Static(Rc::new(renderer)),
1679    }))
1680}
1681
1682pub fn component_with_cx<Message, F>(name: impl Into<String>, renderer: F) -> Element<Message>
1683where
1684    F: for<'a> Fn(&mut crate::ViewCtx<'a, Message>) -> Element<Message> + 'static,
1685{
1686    Element::new(ElementKind::Component(ComponentProps {
1687        name: name.into(),
1688        scope: None,
1689        renderer: ComponentRenderer::WithCx(Rc::new(renderer)),
1690    }))
1691}
1692
1693impl<Message> Element<Message> {
1694    pub fn new(kind: ElementKind<Message>) -> Self {
1695        Self {
1696            kind,
1697            layout: Layout::default(),
1698            style: Style::default(),
1699            focusable: false,
1700            continuity_key: None,
1701            children: Vec::new(),
1702        }
1703    }
1704
1705    pub fn new_text(content: impl Into<String>) -> Self {
1706        Self::new(ElementKind::Text(TextProps {
1707            content: content.into(),
1708        }))
1709    }
1710
1711    pub fn with_layout(mut self, layout: Layout) -> Self {
1712        self.layout = layout;
1713        self
1714    }
1715
1716    pub fn with_style(mut self, style: Style) -> Self {
1717        self.style = style;
1718        self
1719    }
1720
1721    pub fn with_focusable(mut self, focusable: bool) -> Self {
1722        self.focusable = focusable;
1723        self
1724    }
1725
1726    pub fn with_continuity_key(mut self, key: impl Into<String>) -> Self {
1727        self.continuity_key = Some(key.into());
1728        self
1729    }
1730
1731    pub fn with_children(mut self, children: Vec<Element<Message>>) -> Self {
1732        self.children = children;
1733        self
1734    }
1735
1736    pub fn continuity_key(&self) -> Option<&str> {
1737        self.continuity_key.as_deref()
1738    }
1739
1740    pub fn child_layout_spec(&self, bounds: Rect) -> ChildLayoutSpec {
1741        self.kind.child_layout_spec(bounds)
1742    }
1743
1744    pub fn intrinsic_height(&self, width: u16, child_heights: &[u16]) -> u16 {
1745        self.kind.intrinsic_height(width, child_heights)
1746    }
1747
1748    pub fn intrinsic_width(&self, child_widths: &[u16]) -> u16 {
1749        self.kind.intrinsic_width(child_widths)
1750    }
1751
1752    pub fn invalidates_self_on_layout_change(&self) -> bool {
1753        self.kind.invalidates_self_on_layout_change(self.style)
1754    }
1755
1756    pub fn kind_name(&self) -> &'static str {
1757        match &self.kind {
1758            ElementKind::Box(_) => "Box",
1759            ElementKind::Text(_) => "Text",
1760            ElementKind::Pane(_) => "Pane",
1761            ElementKind::Block(_) => "Block",
1762            ElementKind::Paragraph(_) => "Paragraph",
1763            ElementKind::RichText(_) => "RichText",
1764            ElementKind::List(_) => "List",
1765            ElementKind::Tabs(_) => "Tabs",
1766            ElementKind::Gauge(_) => "Gauge",
1767            ElementKind::Clear(_) => "Clear",
1768            ElementKind::LineGauge(_) => "LineGauge",
1769            ElementKind::Table(_) => "Table",
1770            ElementKind::Sparkline(_) => "Sparkline",
1771            ElementKind::BarChart(_) => "BarChart",
1772            ElementKind::Chart(_) => "Chart",
1773            ElementKind::Canvas(_) => "Canvas",
1774            ElementKind::Monthly(_) => "Monthly",
1775            ElementKind::ScrollView(_) => "ScrollView",
1776            ElementKind::Scrollbar(_) => "Scrollbar",
1777            ElementKind::StreamingText(_) => "StreamingText",
1778            ElementKind::Input(_) => "Input",
1779            ElementKind::StatusBar(_) => "StatusBar",
1780            ElementKind::Shell(_) => "Shell",
1781            ElementKind::Component(_) => "Component",
1782        }
1783    }
1784}
1785
1786impl<Message> fmt::Debug for Element<Message> {
1787    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1788        f.debug_struct("Element")
1789            .field("kind", &self.kind)
1790            .field("layout", &self.layout)
1791            .field("style", &self.style)
1792            .field("focusable", &self.focusable)
1793            .field("children_len", &self.children.len())
1794            .finish()
1795    }
1796}
1797
1798#[derive(Debug)]
1799pub struct Node<Message> {
1800    pub id: usize,
1801    pub rect: Rect,
1802    pub measured_height: u16,
1803    pub element: Element<Message>,
1804    pub children: Vec<Node<Message>>,
1805}
1806
1807impl<Message> Node<Message> {
1808    pub fn child_layout_spec(&self, bounds: Rect) -> ChildLayoutSpec {
1809        self.element.child_layout_spec(bounds)
1810    }
1811}
1812
1813fn wrapped_line_count(content: &str, width: u16) -> u16 {
1814    if width == 0 {
1815        return 0;
1816    }
1817
1818    let mut count = 0u16;
1819
1820    for raw_line in content.split('\n') {
1821        if raw_line.is_empty() {
1822            count = count.saturating_add(1);
1823            continue;
1824        }
1825
1826        let mut current_width = 0u16;
1827        let mut line_count = 1u16;
1828
1829        for ch in raw_line.chars() {
1830            let char_width = UnicodeWidthChar::width(ch).unwrap_or(0) as u16;
1831            let char_width = char_width.max(1);
1832            if current_width.saturating_add(char_width) > width && current_width > 0 {
1833                line_count = line_count.saturating_add(1);
1834                current_width = 0;
1835            }
1836
1837            current_width = current_width.saturating_add(char_width);
1838        }
1839
1840        count = count.saturating_add(line_count);
1841    }
1842
1843    count.max(1)
1844}
1845
1846fn text_wrapped_height(text: &Text, width: u16, wrap: bool) -> u16 {
1847    if width == 0 {
1848        return 0;
1849    }
1850
1851    if !wrap {
1852        return text.height() as u16;
1853    }
1854
1855    text.lines
1856        .iter()
1857        .map(|line| wrapped_line_count(&line.plain(), width))
1858        .fold(0u16, u16::saturating_add)
1859        .max(1)
1860}
1861
1862fn paragraph_height(props: &ParagraphProps, width: u16) -> u16 {
1863    let (content_width, chrome_height) = if let Some(block) = &props.block {
1864        let inner_width = width.saturating_sub(border_horizontal_inset(block.props.borders));
1865        let content_width = inner_width
1866            .saturating_sub(block.props.padding.left)
1867            .saturating_sub(block.props.padding.right);
1868        let chrome_height = block
1869            .props
1870            .padding
1871            .top
1872            .saturating_add(block.props.padding.bottom)
1873            .saturating_add(block_vertical_inset(&block.props));
1874        (content_width, chrome_height)
1875    } else {
1876        (width, 0)
1877    };
1878
1879    let text_height = text_wrapped_height(&props.content, content_width, props.wrap.is_some());
1880
1881    text_height.saturating_add(chrome_height).max(1)
1882}
1883
1884fn paragraph_width(props: &ParagraphProps) -> u16 {
1885    let text_width = props.content.width() as u16;
1886    if let Some(block) = &props.block {
1887        text_width
1888            .saturating_add(block.props.padding.left)
1889            .saturating_add(block.props.padding.right)
1890            .saturating_add(border_horizontal_inset(block.props.borders))
1891            .max(block_title_width(&block.props))
1892    } else {
1893        text_width
1894    }
1895}
1896
1897fn plain_text_width(content: &str) -> u16 {
1898    content
1899        .split('\n')
1900        .map(|line| {
1901            line.chars()
1902                .map(|ch| UnicodeWidthChar::width(ch).unwrap_or(0) as u16)
1903                .sum::<u16>()
1904        })
1905        .max()
1906        .unwrap_or(0)
1907}
1908
1909fn rich_text_width(block: &HistoryBlock) -> u16 {
1910    block
1911        .lines
1912        .iter()
1913        .map(|line| {
1914            line.runs
1915                .iter()
1916                .map(|run| plain_text_width(&run.text))
1917                .fold(0u16, u16::saturating_add)
1918        })
1919        .max()
1920        .unwrap_or(0)
1921}
1922
1923fn list_height<Message>(props: &ListProps<Message>) -> u16 {
1924    let content_height = props
1925        .items
1926        .iter()
1927        .map(ListItem::height)
1928        .sum::<usize>()
1929        .max(1) as u16;
1930    block_height(props.block.as_ref(), content_height)
1931}
1932
1933fn list_width<Message>(props: &ListProps<Message>) -> u16 {
1934    let item_width = props.items.iter().map(ListItem::width).max().unwrap_or(0) as u16;
1935    let highlight_width = props.highlight_symbol.as_ref().map_or(0, |symbol| {
1936        if props
1937            .highlight_spacing
1938            .should_add(props.state.selected().is_some())
1939        {
1940            symbol.width() as u16
1941        } else {
1942            0
1943        }
1944    });
1945    block_width(
1946        props.block.as_ref(),
1947        item_width.saturating_add(highlight_width),
1948    )
1949}
1950
1951fn tabs_height<Message>(props: &TabsProps<Message>) -> u16 {
1952    block_height(props.block.as_ref(), 1)
1953}
1954
1955fn tabs_width<Message>(props: &TabsProps<Message>) -> u16 {
1956    let titles = props
1957        .titles
1958        .iter()
1959        .map(|title| {
1960            props.padding_left.width() as u16
1961                + title.width() as u16
1962                + props.padding_right.width() as u16
1963        })
1964        .fold(0u16, u16::saturating_add);
1965    let dividers =
1966        (props.titles.len().saturating_sub(1) as u16).saturating_mul(props.divider.width() as u16);
1967    block_width(props.block.as_ref(), titles.saturating_add(dividers).max(1))
1968}
1969
1970fn block_height(block: Option<&BlockFrame>, content_height: u16) -> u16 {
1971    let Some(block) = block else {
1972        return content_height.max(1);
1973    };
1974
1975    content_height
1976        .saturating_add(block.props.padding.top)
1977        .saturating_add(block.props.padding.bottom)
1978        .saturating_add(block_vertical_inset(&block.props))
1979        .max(1)
1980}
1981
1982fn block_width(block: Option<&BlockFrame>, content_width: u16) -> u16 {
1983    let Some(block) = block else {
1984        return content_width.max(1);
1985    };
1986
1987    content_width
1988        .max(block_title_width(&block.props))
1989        .saturating_add(block.props.padding.left)
1990        .saturating_add(block.props.padding.right)
1991        .saturating_add(border_horizontal_inset(block.props.borders))
1992        .max(1)
1993}
1994
1995fn table_height<Message>(props: &TableProps<Message>) -> u16 {
1996    let header_rows = props
1997        .header
1998        .as_ref()
1999        .map(Row::height_with_margin)
2000        .unwrap_or(0);
2001    let footer_rows = props
2002        .footer
2003        .as_ref()
2004        .map(Row::height_with_margin)
2005        .unwrap_or(0);
2006    let body_rows = props
2007        .rows
2008        .iter()
2009        .map(Row::height_with_margin)
2010        .fold(0u16, u16::saturating_add);
2011    let content = header_rows
2012        .saturating_add(body_rows)
2013        .saturating_add(footer_rows)
2014        .max(1);
2015
2016    if let Some(block) = &props.block {
2017        content
2018            .saturating_add(block_vertical_inset(&block.props))
2019            .saturating_add(block.props.padding.top)
2020            .saturating_add(block.props.padding.bottom)
2021    } else {
2022        content
2023    }
2024}
2025
2026fn table_width<Message>(props: &TableProps<Message>) -> u16 {
2027    let row_width = |row: &Row| -> u16 {
2028        let cells = row.cells_ref();
2029        let spacing = cells.len().saturating_sub(1) as u16 * props.column_spacing;
2030        let content = cells
2031            .iter()
2032            .map(|cell| cell.width() as u16)
2033            .fold(0u16, u16::saturating_add);
2034        content.saturating_add(spacing)
2035    };
2036
2037    let mut content_width = props.header.as_ref().map_or(0, row_width);
2038    content_width = content_width.max(props.footer.as_ref().map_or(0, row_width));
2039    content_width = content_width.max(props.rows.iter().map(row_width).max().unwrap_or(0));
2040
2041    let highlight_width = props.highlight_symbol.as_ref().map_or(0, |symbol| {
2042        if props
2043            .highlight_spacing
2044            .should_add(props.state.selected().is_some())
2045        {
2046            symbol.width() as u16
2047        } else {
2048            0
2049        }
2050    });
2051
2052    block_width(
2053        props.block.as_ref(),
2054        content_width.saturating_add(highlight_width).max(1),
2055    )
2056}
2057
2058fn block_title_width(props: &BlockProps) -> u16 {
2059    props
2060        .titles
2061        .iter()
2062        .map(|title| title.content.width() as u16)
2063        .max()
2064        .unwrap_or(0)
2065}
2066
2067fn title_width(title: Option<&str>) -> u16 {
2068    title.map(plain_text_width).unwrap_or(0)
2069}
2070
2071fn border_horizontal_inset(borders: Borders) -> u16 {
2072    u16::from(borders.contains(Borders::LEFT)) + u16::from(borders.contains(Borders::RIGHT))
2073}
2074
2075fn block_vertical_inset(props: &BlockProps) -> u16 {
2076    let top = u16::from(
2077        props.borders.contains(Borders::TOP) || props.has_title_at_position(TitlePosition::Top),
2078    );
2079    let bottom = u16::from(
2080        props.borders.contains(Borders::BOTTOM)
2081            || props.has_title_at_position(TitlePosition::Bottom),
2082    );
2083    top.saturating_add(bottom)
2084}
2085
2086fn step_list_selection(state: &mut ListState, len: usize, delta: isize) -> Option<usize> {
2087    if len == 0 {
2088        return None;
2089    }
2090
2091    let mut next = state.selected().unwrap_or(0);
2092    if delta.is_negative() {
2093        next = next.saturating_sub(delta.unsigned_abs());
2094    } else {
2095        next = next
2096            .saturating_add(delta as usize)
2097            .min(len.saturating_sub(1));
2098    }
2099
2100    if Some(next) == state.selected() {
2101        None
2102    } else {
2103        state.select(Some(next));
2104        Some(next)
2105    }
2106}
2107
2108fn sync_list_offset<Message>(props: &mut ListProps<Message>, viewport_height: usize) {
2109    let Some(selected) = props.state.selected() else {
2110        return;
2111    };
2112
2113    if viewport_height == 0 {
2114        props.state = props.state.with_offset(0);
2115        return;
2116    }
2117
2118    let item_heights = props.items.iter().map(ListItem::height).collect::<Vec<_>>();
2119    let selected_top = item_heights[..selected].iter().sum::<usize>();
2120    let selected_height = item_heights.get(selected).copied().unwrap_or(1).max(1);
2121    let selected_bottom = selected_top.saturating_add(selected_height);
2122    let mut offset = props.state.offset();
2123    let padding = props.scroll_padding;
2124
2125    let upper_bound = offset.saturating_add(padding);
2126    if selected_top < upper_bound {
2127        offset = selected_top.saturating_sub(padding);
2128    } else {
2129        let viewport_end = offset.saturating_add(viewport_height);
2130        let lower_bound = viewport_end.saturating_sub(padding);
2131        if selected_bottom > lower_bound {
2132            offset = selected_bottom
2133                .saturating_add(padding)
2134                .saturating_sub(viewport_height);
2135        }
2136    }
2137
2138    props.state = props.state.with_offset(offset);
2139}
2140
2141fn step_table_selection(state: &mut TableState, len: usize, delta: isize) -> Option<usize> {
2142    if len == 0 {
2143        return None;
2144    }
2145
2146    let mut next = state.selected().unwrap_or(0);
2147    if delta.is_negative() {
2148        next = next.saturating_sub(delta.unsigned_abs());
2149    } else {
2150        next = next
2151            .saturating_add(delta as usize)
2152            .min(len.saturating_sub(1));
2153    }
2154
2155    if Some(next) == state.selected() {
2156        None
2157    } else {
2158        state.select(Some(next));
2159        Some(next)
2160    }
2161}
2162
2163fn sync_table_offset<Message>(props: &mut TableProps<Message>, viewport_height: usize) {
2164    let Some(selected) = props.state.selected() else {
2165        return;
2166    };
2167
2168    if viewport_height == 0 {
2169        props.state = props.state.with_offset(0);
2170        return;
2171    }
2172
2173    let header_rows = props
2174        .header
2175        .as_ref()
2176        .map(Row::height_with_margin)
2177        .unwrap_or(0) as usize;
2178    let visible_height = viewport_height.saturating_sub(header_rows).max(1);
2179    let row_heights = props
2180        .rows
2181        .iter()
2182        .map(Row::height_with_margin)
2183        .map(usize::from)
2184        .collect::<Vec<_>>();
2185    let selected_top = row_heights[..selected].iter().sum::<usize>();
2186    let selected_height = row_heights.get(selected).copied().unwrap_or(1).max(1);
2187    let selected_bottom = selected_top.saturating_add(selected_height);
2188    let mut offset = props.state.offset();
2189
2190    if selected_top < offset {
2191        offset = selected_top;
2192    } else if selected_bottom > offset.saturating_add(visible_height) {
2193        offset = selected_bottom.saturating_sub(visible_height);
2194    }
2195
2196    props.state = props.state.with_offset(offset);
2197}
2198
2199fn step_selected_index(selected: &mut Option<usize>, len: usize, delta: isize) -> Option<usize> {
2200    if len == 0 {
2201        return None;
2202    }
2203
2204    let mut next = selected.unwrap_or(0);
2205    if delta.is_negative() {
2206        next = next.saturating_sub(delta.unsigned_abs());
2207    } else {
2208        next = next
2209            .saturating_add(delta as usize)
2210            .min(len.saturating_sub(1));
2211    }
2212
2213    if Some(next) == *selected {
2214        None
2215    } else {
2216        *selected = Some(next);
2217        Some(next)
2218    }
2219}
2220
2221fn insert_char_at_cursor(value: &mut String, cursor: usize, ch: char) {
2222    let byte_index = byte_index_for_char(value, cursor);
2223    value.insert(byte_index, ch);
2224}
2225
2226fn remove_char_before_cursor(value: &mut String, cursor: usize) {
2227    let end = byte_index_for_char(value, cursor);
2228    let start = byte_index_for_char(value, cursor.saturating_sub(1));
2229    value.replace_range(start..end, "");
2230}
2231
2232fn byte_index_for_char(value: &str, cursor: usize) -> usize {
2233    if cursor == 0 {
2234        return 0;
2235    }
2236
2237    value
2238        .char_indices()
2239        .map(|(index, _)| index)
2240        .nth(cursor)
2241        .unwrap_or(value.len())
2242}