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}