1#![forbid(unsafe_code)]
2
3pub mod adaptive_radix;
129pub mod align;
130pub mod badge;
132pub mod block;
134pub mod borders;
135pub mod cached;
136pub mod choreography;
137pub mod columns;
138pub mod command_palette;
139pub mod constraint_overlay;
140#[cfg(feature = "debug-overlay")]
141pub mod debug_overlay;
142pub mod decision_card;
144pub mod diagnostics;
146pub mod drag;
148pub mod drift_visualization;
150pub mod elias_fano;
152pub mod emoji;
153pub mod error_boundary;
154pub mod fenwick;
156pub mod file_picker;
157pub mod focus;
159pub mod group;
160pub mod height_predictor;
162pub mod help;
163pub mod help_registry;
164pub mod hint_ranker;
166pub mod history_panel;
168pub mod input;
169pub mod inspector;
171pub mod json_view;
172pub mod keyboard_drag;
173pub mod layout;
174pub mod layout_debugger;
175pub mod list;
176pub mod log_ring;
177pub mod log_viewer;
178pub mod louds;
179pub mod measurable;
181pub mod measure_cache;
183pub mod modal;
184pub mod mouse;
186pub mod notification_queue;
188pub mod padding;
189pub mod paginator;
190pub mod panel;
191pub mod paragraph;
193pub mod popover;
194pub mod pretty;
195pub mod progress;
196pub mod rule;
197pub mod scrollbar;
198pub mod sparkline;
199pub mod spinner;
200pub mod stateful;
202pub mod status_line;
203pub mod stopwatch;
204pub mod table;
206pub mod tabs;
207pub mod textarea;
208pub mod timer;
209pub mod toast;
211pub mod tree;
212pub mod undo_support;
214pub mod validation_error;
216pub mod virtualized;
217pub mod voi_debug_overlay;
218
219#[cfg(all(test, feature = "tracing"))]
220pub(crate) mod tracing_test_support {
221 use std::sync::{Mutex, MutexGuard, OnceLock};
222
223 pub(crate) struct TraceTestGuard {
226 _lock: MutexGuard<'static, ()>,
227 }
228
229 impl Drop for TraceTestGuard {
230 fn drop(&mut self) {
231 tracing::callsite::rebuild_interest_cache();
232 }
233 }
234
235 pub(crate) fn acquire() -> TraceTestGuard {
236 static TRACE_TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
237
238 let lock = TRACE_TEST_LOCK
240 .get_or_init(|| Mutex::new(()))
241 .lock()
242 .unwrap_or_else(|e| e.into_inner());
243 tracing::callsite::rebuild_interest_cache();
244 TraceTestGuard { _lock: lock }
245 }
246}
247
248pub use align::{Align, VerticalAlignment};
249pub use badge::Badge;
250pub use cached::{CacheKey, CachedWidget, CachedWidgetState, FnKey, HashKey, NoCacheKey};
251pub use columns::{Column, Columns};
252pub use constraint_overlay::{ConstraintOverlay, ConstraintOverlayStyle};
253#[cfg(feature = "debug-overlay")]
254pub use debug_overlay::{
255 DebugOverlay, DebugOverlayOptions, DebugOverlayState, DebugOverlayStateful,
256 DebugOverlayStatefulState,
257};
258pub use decision_card::DecisionCard;
259pub use group::Group;
260pub use help_registry::{HelpContent, HelpId, HelpRegistry, Keybinding};
261pub use history_panel::{HistoryEntry, HistoryPanel, HistoryPanelMode};
262pub use layout_debugger::{LayoutConstraints, LayoutDebugger, LayoutRecord};
263pub use log_ring::LogRing;
264pub use log_viewer::{LogViewer, LogViewerState, LogWrapMode, SearchConfig, SearchMode};
265pub use paginator::{Paginator, PaginatorMode};
266pub use panel::Panel;
267pub use sparkline::Sparkline;
268pub use status_line::{StatusItem, StatusLine};
269pub use tabs::{Tab, Tabs, TabsState};
270pub use virtualized::{
271 HeightCache, ItemHeight, RenderItem, Virtualized, VirtualizedList, VirtualizedListState,
272 VirtualizedStorage,
273};
274pub use voi_debug_overlay::{
275 VoiDebugOverlay, VoiDecisionSummary, VoiLedgerEntry, VoiObservationSummary, VoiOverlayData,
276 VoiOverlayStyle, VoiPosteriorSummary,
277};
278
279pub use toast::{
281 KeyEvent as ToastKeyEvent, Toast, ToastAction, ToastAnimationConfig, ToastAnimationPhase,
282 ToastAnimationState, ToastConfig, ToastContent, ToastEasing, ToastEntranceAnimation,
283 ToastEvent, ToastExitAnimation, ToastIcon, ToastId, ToastPosition, ToastState, ToastStyle,
284};
285
286pub use notification_queue::{
288 NotificationPriority, NotificationQueue, QueueAction, QueueConfig, QueueStats,
289};
290
291pub use ftui_a11y::Accessible;
293
294pub use mouse::MouseResult;
296
297pub use measurable::{MeasurableWidget, SizeConstraints};
299
300pub use measure_cache::{CacheStats, MeasureCache, WidgetId};
302pub use modal::{
303 BackdropConfig, MODAL_HIT_BACKDROP, MODAL_HIT_CONTENT, Modal, ModalAction, ModalConfig,
304 ModalPosition, ModalSizeConstraints, ModalState,
305};
306
307pub use inspector::{
309 DiagnosticEntry, DiagnosticEventKind, DiagnosticLog, HitInfo, InspectorMode, InspectorOverlay,
310 InspectorState, InspectorStyle, TelemetryCallback, TelemetryHooks, WidgetInfo,
311 diagnostics_enabled, init_diagnostics, is_deterministic_mode, reset_event_counter,
312 set_diagnostics_enabled,
313};
314
315pub use focus::{
317 FocusEvent, FocusGraph, FocusGroup, FocusId, FocusIndicator, FocusIndicatorKind, FocusManager,
318 FocusNode, FocusTrap, NavDirection,
319};
320
321pub use drag::{
323 DragConfig, DragPayload, DragState, Draggable, DropPosition, DropResult, DropTarget,
324};
325
326pub use stateful::{StateKey, Stateful, VersionedState};
328
329pub use list::ListPersistState;
331pub use table::TablePersistState;
332pub use tree::TreePersistState;
333pub use virtualized::VirtualizedListPersistState;
334
335pub use undo_support::{
337 ListOperation, ListUndoExt, SelectionOperation, TableOperation, TableUndoExt,
338 TextEditOperation, TextInputUndoExt, TreeOperation, TreeUndoExt, UndoSupport, UndoWidgetId,
339 WidgetTextEditCmd,
340};
341
342pub use validation_error::{
344 ANIMATION_DURATION_MS, ERROR_BG_DEFAULT, ERROR_FG_DEFAULT, ERROR_ICON_DEFAULT,
345 ValidationErrorDisplay, ValidationErrorState,
346};
347
348use ftui_core::geometry::Rect;
349use ftui_render::buffer::Buffer;
350use ftui_render::cell::Cell;
351use ftui_render::frame::{Frame, WidgetSignal};
352use ftui_style::Style;
353use ftui_text::grapheme_width;
354
355#[must_use]
360pub(crate) fn a11y_node_id(area: Rect) -> u64 {
361 const FNV_OFFSET: u64 = 14_695_981_039_346_656_037;
363 const FNV_PRIME: u64 = 1_099_511_628_211;
364 let mut h = FNV_OFFSET;
365 for byte in area
366 .x
367 .to_le_bytes()
368 .iter()
369 .chain(&area.y.to_le_bytes())
370 .chain(&area.width.to_le_bytes())
371 .chain(&area.height.to_le_bytes())
372 {
373 h ^= u64::from(*byte);
374 h = h.wrapping_mul(FNV_PRIME);
375 }
376 h
377}
378
379pub trait Widget {
419 fn render(&self, area: Rect, frame: &mut Frame);
425
426 fn is_essential(&self) -> bool {
437 false
438 }
439}
440
441pub struct Budgeted<W> {
443 widget_id: u64,
444 signal: WidgetSignal,
445 inner: W,
446}
447
448impl<W> Budgeted<W> {
449 #[must_use]
451 pub fn new(widget_id: u64, inner: W) -> Self {
452 Self {
453 widget_id,
454 signal: WidgetSignal::new(widget_id),
455 inner,
456 }
457 }
458
459 #[must_use]
461 pub fn with_signal(mut self, mut signal: WidgetSignal) -> Self {
462 signal.widget_id = self.widget_id;
463 self.signal = signal;
464 self
465 }
466
467 #[must_use]
469 pub fn inner(&self) -> &W {
470 &self.inner
471 }
472}
473
474impl<W: Widget> Widget for Budgeted<W> {
475 fn render(&self, area: Rect, frame: &mut Frame) {
476 let mut signal = self.signal.clone();
477 signal.widget_id = self.widget_id;
478 signal.essential = self.inner.is_essential();
479 signal.area_cells = area.width as u32 * area.height as u32;
480 frame.register_widget_signal(signal);
481
482 if frame.should_render_widget(self.widget_id, self.inner.is_essential()) {
483 self.inner.render(area, frame);
484 }
485 }
486
487 fn is_essential(&self) -> bool {
488 self.inner.is_essential()
489 }
490}
491
492impl<W: StatefulWidget + Widget> StatefulWidget for Budgeted<W> {
493 type State = W::State;
494
495 fn render(&self, area: Rect, frame: &mut Frame, state: &mut Self::State) {
496 let mut signal = self.signal.clone();
497 signal.widget_id = self.widget_id;
498 signal.essential = self.inner.is_essential();
499 signal.area_cells = area.width as u32 * area.height as u32;
500 frame.register_widget_signal(signal);
501
502 if frame.should_render_widget(self.widget_id, self.inner.is_essential()) {
503 StatefulWidget::render(&self.inner, area, frame, state);
504 }
505 }
506}
507
508pub trait StatefulWidget {
545 type State;
547
548 fn render(&self, area: Rect, frame: &mut Frame, state: &mut Self::State);
555}
556
557pub(crate) fn apply_style(cell: &mut Cell, style: Style) {
564 if let Some(fg) = style.fg {
565 cell.fg = fg;
566 }
567 if let Some(bg) = style.bg {
568 match bg.a() {
569 0 => {} 255 => cell.bg = bg, _ => cell.bg = bg.over(cell.bg), }
573 }
574 if let Some(attrs) = style.attrs {
575 let cell_flags: ftui_render::cell::StyleFlags = attrs.into();
576 cell.attrs = cell.attrs.merged_flags(cell_flags);
577 }
578}
579
580pub(crate) fn set_style_area(buf: &mut Buffer, area: Rect, style: Style) {
590 if style.is_empty() {
591 return;
592 }
593 let clipped = area.intersection(&buf.current_scissor());
594 if clipped.is_empty() {
595 return;
596 }
597
598 let opacity = buf.current_opacity();
599 let fg = style.fg.map(|fg| fg.with_opacity(opacity));
600 let bg = style.bg.map(|bg| bg.with_opacity(opacity));
601 let attrs = style.attrs.map(ftui_render::cell::StyleFlags::from);
602 for y in clipped.y..clipped.bottom() {
603 let Some(row) = buf.row_cells_mut_span(y, clipped.x, clipped.right()) else {
604 continue;
605 };
606 for cell in row {
607 if let Some(fg) = fg {
608 cell.fg = fg;
609 }
610 if let Some(bg) = bg {
611 match bg.a() {
612 0 => {} 255 => cell.bg = bg, _ => cell.bg = bg.over(cell.bg), }
616 }
617 if let Some(attrs) = attrs {
618 cell.attrs = cell.attrs.merged_flags(attrs);
619 }
620 }
621 }
622}
623
624pub(crate) fn clear_text_area(frame: &mut Frame, area: Rect, style: Style) {
626 if area.width == 0 || area.height == 0 {
627 return;
628 }
629
630 let mut cell = Cell::from_char(' ');
631 apply_style(&mut cell, style);
632 frame.buffer.fill(area, cell);
633}
634
635pub(crate) fn clear_text_row(frame: &mut Frame, area: Rect, style: Style) {
637 clear_text_area(frame, Rect::new(area.x, area.y, area.width, 1), style);
638}
639
640fn inherited_text_cell(
646 frame: &Frame,
647 x: u16,
648 y: u16,
649 content: ftui_render::cell::CellContent,
650) -> Cell {
651 let mut cell = frame.buffer.get(x, y).copied().unwrap_or_default();
652 cell.content = content;
653 cell.attrs = ftui_render::cell::CellAttrs::new(cell.attrs.flags(), 0);
654 cell
655}
656
657pub(crate) fn draw_text_span(
662 frame: &mut Frame,
663 mut x: u16,
664 y: u16,
665 content: &str,
666 style: Style,
667 max_x: u16,
668) -> u16 {
669 use unicode_segmentation::UnicodeSegmentation;
670
671 for grapheme in content.graphemes(true) {
672 if x >= max_x {
673 break;
674 }
675 let w = grapheme_width(grapheme);
676 if w == 0 {
677 continue;
678 }
679 if x.saturating_add(w as u16) > max_x {
680 break;
681 }
682
683 let cell_content = if w > 1 || grapheme.chars().count() > 1 {
685 let id = frame.intern_with_width(grapheme, w as u8);
686 ftui_render::cell::CellContent::from_grapheme(id)
687 } else if let Some(c) = grapheme.chars().next() {
688 ftui_render::cell::CellContent::from_char(c)
689 } else {
690 continue;
691 };
692
693 let mut cell = inherited_text_cell(frame, x, y, cell_content);
694 apply_style(&mut cell, style);
695
696 frame.buffer.set_fast(x, y, cell);
699
700 x = x.saturating_add(w as u16);
701 }
702 x
703}
704
705#[allow(dead_code)]
707pub(crate) fn draw_text_span_with_link(
708 frame: &mut Frame,
709 x: u16,
710 y: u16,
711 content: &str,
712 style: Style,
713 max_x: u16,
714 link_url: Option<&str>,
715) -> u16 {
716 draw_text_span_scrolled(frame, x, y, content, style, max_x, 0, link_url)
717}
718
719#[allow(dead_code, clippy::too_many_arguments)]
721pub(crate) fn draw_text_span_scrolled(
722 frame: &mut Frame,
723 mut x: u16,
724 y: u16,
725 content: &str,
726 style: Style,
727 max_x: u16,
728 scroll_x: u16,
729 link_url: Option<&str>,
730) -> u16 {
731 use unicode_segmentation::UnicodeSegmentation;
732
733 let link_id = if let Some(url) = link_url {
735 frame.register_link(url)
736 } else {
737 0
738 };
739
740 let mut visual_pos: u16 = 0;
741
742 for grapheme in content.graphemes(true) {
743 if x >= max_x {
744 break;
745 }
746 let w = grapheme_width(grapheme);
747 if w == 0 {
748 continue;
749 }
750
751 let next_visual_pos = visual_pos.saturating_add(w as u16);
752
753 if next_visual_pos <= scroll_x {
755 visual_pos = next_visual_pos;
757 continue;
758 }
759
760 if visual_pos < scroll_x {
761 visual_pos = next_visual_pos;
764 continue;
765 }
766
767 if x.saturating_add(w as u16) > max_x {
768 break;
769 }
770
771 let cell_content = if w > 1 || grapheme.chars().count() > 1 {
773 let id = frame.intern_with_width(grapheme, w as u8);
774 ftui_render::cell::CellContent::from_grapheme(id)
775 } else if let Some(c) = grapheme.chars().next() {
776 ftui_render::cell::CellContent::from_char(c)
777 } else {
778 continue;
779 };
780
781 let mut cell = inherited_text_cell(frame, x, y, cell_content);
782 apply_style(&mut cell, style);
783
784 if link_id != 0 {
786 cell.attrs = cell.attrs.with_link(link_id);
787 }
788
789 frame.buffer.set_fast(x, y, cell);
790
791 x = x.saturating_add(w as u16);
792 visual_pos = next_visual_pos;
793 }
794 x
795}
796
797pub(crate) fn contains_ignore_case(haystack: &str, needle_lower: &str) -> bool {
799 if needle_lower.is_empty() {
800 return true;
801 }
802 if haystack.is_ascii() && needle_lower.is_ascii() {
804 let haystack_bytes = haystack.as_bytes();
805 let needle_bytes = needle_lower.as_bytes();
806 if needle_bytes.len() > haystack_bytes.len() {
807 return false;
808 }
809 for i in 0..=haystack_bytes.len() - needle_bytes.len() {
811 let mut match_found = true;
812 for (j, &b) in needle_bytes.iter().enumerate() {
813 if haystack_bytes[i + j].to_ascii_lowercase() != b {
814 match_found = false;
815 break;
816 }
817 }
818 if match_found {
819 return true;
820 }
821 }
822 return false;
823 }
824 haystack.to_lowercase().contains(needle_lower)
826}
827
828#[cfg(test)]
829mod tests {
830 use super::*;
831 use ftui_render::cell::PackedRgba;
832 use ftui_render::grapheme_pool::GraphemePool;
833
834 #[test]
835 fn apply_style_sets_fg() {
836 let mut cell = Cell::default();
837 let style = Style::new().fg(PackedRgba::rgb(255, 0, 0));
838 apply_style(&mut cell, style);
839 assert_eq!(cell.fg, PackedRgba::rgb(255, 0, 0));
840 }
841
842 #[test]
843 fn apply_style_sets_bg() {
844 let mut cell = Cell::default();
845 let style = Style::new().bg(PackedRgba::rgb(0, 255, 0));
846 apply_style(&mut cell, style);
847 assert_eq!(cell.bg, PackedRgba::rgb(0, 255, 0));
848 }
849
850 #[test]
851 fn apply_style_preserves_content() {
852 let mut cell = Cell::from_char('Z');
853 let style = Style::new().fg(PackedRgba::rgb(1, 2, 3));
854 apply_style(&mut cell, style);
855 assert_eq!(cell.content.as_char(), Some('Z'));
856 }
857
858 #[test]
859 fn apply_style_empty_is_noop() {
860 let original = Cell::default();
861 let mut cell = Cell::default();
862 apply_style(&mut cell, Style::default());
863 assert_eq!(cell.fg, original.fg);
864 assert_eq!(cell.bg, original.bg);
865 }
866
867 #[test]
868 fn apply_style_bg_only_preserves_fg() {
869 let mut cell = Cell::from_char('x').with_fg(PackedRgba::rgb(0, 200, 0));
871 let selection = Style::new().bg(PackedRgba::rgb(0, 0, 180));
872 apply_style(&mut cell, selection);
873 assert_eq!(cell.fg, PackedRgba::rgb(0, 200, 0));
875 assert_eq!(cell.bg, PackedRgba::rgb(0, 0, 180));
876 }
877
878 #[test]
879 fn apply_style_composites_alpha_bg() {
880 let base_bg = PackedRgba::rgb(200, 0, 0);
881 let mut cell = Cell::default().with_bg(base_bg);
882 let overlay = PackedRgba::rgba(0, 0, 200, 128);
883 apply_style(&mut cell, Style::new().bg(overlay));
884 assert_eq!(cell.bg, overlay.over(base_bg));
885 }
886
887 #[test]
888 fn apply_style_transparent_bg_is_noop() {
889 let base_bg = PackedRgba::rgb(100, 100, 100);
890 let mut cell = Cell::default().with_bg(base_bg);
891 apply_style(&mut cell, Style::new().bg(PackedRgba::rgba(255, 0, 0, 0)));
892 assert_eq!(cell.bg, base_bg);
893 }
894
895 #[test]
896 fn apply_style_merges_attrs_not_replaces() {
897 use ftui_render::cell::StyleFlags as CellFlags;
898 let mut cell = Cell::default();
900 cell.attrs = cell.attrs.with_flags(CellFlags::BOLD);
901 let overlay = Style::new().italic();
903 apply_style(&mut cell, overlay);
904 assert!(cell.attrs.has_flag(CellFlags::BOLD), "BOLD must survive");
905 assert!(
906 cell.attrs.has_flag(CellFlags::ITALIC),
907 "ITALIC must be added"
908 );
909 }
910
911 #[test]
912 fn set_style_area_bg_only_preserves_per_cell_fg() {
913 let mut buf = Buffer::new(3, 1);
915 buf.set(0, 0, Cell::from_char('R').with_fg(PackedRgba::RED));
916 buf.set(1, 0, Cell::from_char('G').with_fg(PackedRgba::GREEN));
917 buf.set(2, 0, Cell::from_char('B').with_fg(PackedRgba::BLUE));
918
919 let highlight = Style::new().bg(PackedRgba::rgb(40, 40, 40));
921 set_style_area(&mut buf, Rect::new(0, 0, 3, 1), highlight);
922
923 assert_eq!(buf.get(0, 0).unwrap().fg, PackedRgba::RED);
925 assert_eq!(buf.get(1, 0).unwrap().fg, PackedRgba::GREEN);
926 assert_eq!(buf.get(2, 0).unwrap().fg, PackedRgba::BLUE);
927 assert_eq!(buf.get(0, 0).unwrap().bg, PackedRgba::rgb(40, 40, 40));
929 }
930
931 #[test]
932 fn set_style_area_merges_attrs_not_replaces() {
933 use ftui_render::cell::StyleFlags as CellFlags;
934 let mut buf = Buffer::new(1, 1);
935 let mut cell = Cell::from_char('X');
936 cell.attrs = cell.attrs.with_flags(CellFlags::BOLD);
937 buf.set(0, 0, cell);
938
939 set_style_area(&mut buf, Rect::new(0, 0, 1, 1), Style::new().italic());
940
941 let result = buf.get(0, 0).unwrap();
942 assert!(result.attrs.has_flag(CellFlags::BOLD), "BOLD must survive");
943 assert!(
944 result.attrs.has_flag(CellFlags::ITALIC),
945 "ITALIC must be added"
946 );
947 }
948
949 #[test]
950 fn set_style_area_applies_to_all_cells() {
951 let mut buf = Buffer::new(3, 2);
952 let area = Rect::new(0, 0, 3, 2);
953 let style = Style::new().bg(PackedRgba::rgb(10, 20, 30));
954 set_style_area(&mut buf, area, style);
955
956 for y in 0..2 {
957 for x in 0..3 {
958 assert_eq!(
959 buf.get(x, y).unwrap().bg,
960 PackedRgba::rgb(10, 20, 30),
961 "cell ({x},{y}) should have style applied"
962 );
963 }
964 }
965 }
966
967 #[test]
968 fn set_style_area_composites_alpha_bg_over_existing_bg() {
969 let mut buf = Buffer::new(1, 1);
970 let base = PackedRgba::rgb(200, 0, 0);
971 buf.set(0, 0, Cell::default().with_bg(base));
972
973 let overlay = PackedRgba::rgba(0, 0, 200, 128);
974 set_style_area(&mut buf, Rect::new(0, 0, 1, 1), Style::new().bg(overlay));
975
976 let expected = overlay.over(base);
977 assert_eq!(buf.get(0, 0).unwrap().bg, expected);
978 }
979
980 #[test]
981 fn set_style_area_partial_rect() {
982 let mut buf = Buffer::new(5, 5);
983 let area = Rect::new(1, 1, 2, 2);
984 let style = Style::new().fg(PackedRgba::rgb(99, 99, 99));
985 set_style_area(&mut buf, area, style);
986
987 assert_eq!(buf.get(1, 1).unwrap().fg, PackedRgba::rgb(99, 99, 99));
989 assert_eq!(buf.get(2, 2).unwrap().fg, PackedRgba::rgb(99, 99, 99));
990
991 assert_ne!(buf.get(0, 0).unwrap().fg, PackedRgba::rgb(99, 99, 99));
993 }
994
995 #[test]
996 fn set_style_area_empty_style_is_noop() {
997 let mut buf = Buffer::new(3, 3);
998 buf.set(0, 0, Cell::from_char('A'));
999 let original_fg = buf.get(0, 0).unwrap().fg;
1000
1001 set_style_area(&mut buf, Rect::new(0, 0, 3, 3), Style::default());
1002
1003 assert_eq!(buf.get(0, 0).unwrap().fg, original_fg);
1005 assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('A'));
1006 }
1007
1008 #[test]
1009 fn set_style_area_respects_scissor() {
1010 let mut buf = Buffer::new(3, 3);
1011 let style = Style::new().bg(PackedRgba::rgb(10, 20, 30));
1012
1013 buf.push_scissor(Rect::new(1, 1, 1, 1));
1014 set_style_area(&mut buf, Rect::new(0, 0, 3, 3), style);
1015
1016 assert_eq!(buf.get(1, 1).unwrap().bg, PackedRgba::rgb(10, 20, 30));
1017 assert_ne!(buf.get(0, 1).unwrap().bg, PackedRgba::rgb(10, 20, 30));
1018 assert_ne!(buf.get(1, 0).unwrap().bg, PackedRgba::rgb(10, 20, 30));
1019 assert_ne!(buf.get(2, 2).unwrap().bg, PackedRgba::rgb(10, 20, 30));
1020 }
1021
1022 #[test]
1023 fn set_style_area_respects_opacity_stack() {
1024 let mut buf = Buffer::new(1, 1);
1025 let base_fg = PackedRgba::rgb(20, 30, 40);
1026 let base_bg = PackedRgba::rgb(50, 60, 70);
1027 buf.set(0, 0, Cell::from_char('X').with_fg(base_fg).with_bg(base_bg));
1028
1029 let overlay_fg = PackedRgba::rgb(200, 100, 0);
1030 let overlay_bg = PackedRgba::rgb(0, 0, 200);
1031 buf.push_opacity(0.5);
1032 set_style_area(
1033 &mut buf,
1034 Rect::new(0, 0, 1, 1),
1035 Style::new().fg(overlay_fg).bg(overlay_bg),
1036 );
1037
1038 let cell = buf.get(0, 0).unwrap();
1039 assert_eq!(cell.fg, overlay_fg.with_opacity(0.5));
1040 assert_eq!(cell.bg, overlay_bg.with_opacity(0.5).over(base_bg));
1041 }
1042
1043 #[test]
1044 fn draw_text_span_basic() {
1045 let mut pool = GraphemePool::new();
1046 let mut frame = Frame::new(10, 1, &mut pool);
1047 let end_x = draw_text_span(&mut frame, 0, 0, "ABC", Style::default(), 10);
1048
1049 assert_eq!(end_x, 3);
1050 assert_eq!(frame.buffer.get(0, 0).unwrap().content.as_char(), Some('A'));
1051 assert_eq!(frame.buffer.get(1, 0).unwrap().content.as_char(), Some('B'));
1052 assert_eq!(frame.buffer.get(2, 0).unwrap().content.as_char(), Some('C'));
1053 }
1054
1055 #[test]
1056 fn draw_text_span_clipped_at_max_x() {
1057 let mut pool = GraphemePool::new();
1058 let mut frame = Frame::new(10, 1, &mut pool);
1059 let end_x = draw_text_span(&mut frame, 0, 0, "ABCDEF", Style::default(), 3);
1060
1061 assert_eq!(end_x, 3);
1062 assert_eq!(frame.buffer.get(0, 0).unwrap().content.as_char(), Some('A'));
1063 assert_eq!(frame.buffer.get(2, 0).unwrap().content.as_char(), Some('C'));
1064 assert!(frame.buffer.get(3, 0).unwrap().is_empty());
1066 }
1067
1068 #[test]
1069 fn draw_text_span_starts_at_offset() {
1070 let mut pool = GraphemePool::new();
1071 let mut frame = Frame::new(10, 1, &mut pool);
1072 let end_x = draw_text_span(&mut frame, 5, 0, "XY", Style::default(), 10);
1073
1074 assert_eq!(end_x, 7);
1075 assert_eq!(frame.buffer.get(5, 0).unwrap().content.as_char(), Some('X'));
1076 assert_eq!(frame.buffer.get(6, 0).unwrap().content.as_char(), Some('Y'));
1077 assert!(frame.buffer.get(4, 0).unwrap().is_empty());
1078 }
1079
1080 #[test]
1081 fn draw_text_span_empty_string() {
1082 let mut pool = GraphemePool::new();
1083 let mut frame = Frame::new(5, 1, &mut pool);
1084 let end_x = draw_text_span(&mut frame, 0, 0, "", Style::default(), 5);
1085 assert_eq!(end_x, 0);
1086 }
1087
1088 #[test]
1089 fn draw_text_span_applies_style() {
1090 let mut pool = GraphemePool::new();
1091 let mut frame = Frame::new(5, 1, &mut pool);
1092 let style = Style::new().fg(PackedRgba::rgb(255, 128, 0));
1093 draw_text_span(&mut frame, 0, 0, "A", style, 5);
1094
1095 assert_eq!(
1096 frame.buffer.get(0, 0).unwrap().fg,
1097 PackedRgba::rgb(255, 128, 0)
1098 );
1099 }
1100
1101 #[test]
1102 fn draw_text_span_preserves_existing_overlay_fg_and_bg() {
1103 let mut pool = GraphemePool::new();
1104 let mut frame = Frame::new(3, 1, &mut pool);
1105 frame.buffer.set(
1106 0,
1107 0,
1108 Cell::from_char('x').with_fg(PackedRgba::rgb(200, 40, 10)),
1109 );
1110 set_style_area(
1111 &mut frame.buffer,
1112 Rect::new(0, 0, 1, 1),
1113 Style::new().bg(PackedRgba::rgb(20, 30, 40)),
1114 );
1115
1116 draw_text_span(&mut frame, 0, 0, "A", Style::default(), 1);
1117
1118 let cell = frame.buffer.get(0, 0).unwrap();
1119 assert_eq!(cell.content.as_char(), Some('A'));
1120 assert_eq!(cell.fg, PackedRgba::rgb(200, 40, 10));
1121 assert_eq!(cell.bg, PackedRgba::rgb(20, 30, 40));
1122 }
1123
1124 #[test]
1125 fn draw_text_span_drops_stale_link_id_but_keeps_style_flags() {
1126 use ftui_render::cell::{CellAttrs, StyleFlags as CellFlags};
1127
1128 let mut pool = GraphemePool::new();
1129 let mut frame = Frame::new(3, 1, &mut pool);
1130 frame.buffer.set(
1131 0,
1132 0,
1133 Cell::from_char('x').with_attrs(CellAttrs::new(CellFlags::UNDERLINE, 42)),
1134 );
1135
1136 draw_text_span(&mut frame, 0, 0, "A", Style::default(), 1);
1137
1138 let cell = frame.buffer.get(0, 0).unwrap();
1139 assert_eq!(cell.content.as_char(), Some('A'));
1140 assert!(cell.attrs.has_flag(CellFlags::UNDERLINE));
1141 assert_eq!(cell.attrs.link_id(), 0);
1142 }
1143
1144 #[test]
1145 fn draw_text_span_max_x_at_start_draws_nothing() {
1146 let mut pool = GraphemePool::new();
1147 let mut frame = Frame::new(5, 1, &mut pool);
1148 let end_x = draw_text_span(&mut frame, 3, 0, "ABC", Style::default(), 3);
1149 assert_eq!(end_x, 3);
1150 assert!(frame.buffer.get(3, 0).unwrap().is_empty());
1151 }
1152
1153 #[test]
1154 fn widget_is_essential_default_false() {
1155 struct DummyWidget;
1156 impl Widget for DummyWidget {
1157 fn render(&self, _: Rect, _: &mut Frame) {}
1158 }
1159 assert!(!DummyWidget.is_essential());
1160 }
1161
1162 #[test]
1163 fn budgeted_new_and_inner() {
1164 struct TestW;
1165 impl Widget for TestW {
1166 fn render(&self, _: Rect, _: &mut Frame) {}
1167 }
1168 let b = Budgeted::new(42, TestW);
1169 assert_eq!(b.widget_id, 42);
1170 let _ = b.inner(); }
1172
1173 #[test]
1174 fn budgeted_with_signal() {
1175 struct TestW;
1176 impl Widget for TestW {
1177 fn render(&self, _: Rect, _: &mut Frame) {}
1178 }
1179 let sig = WidgetSignal::new(99);
1180 let b = Budgeted::new(42, TestW).with_signal(sig);
1181 assert_eq!(b.signal.widget_id, 42);
1183 }
1184
1185 #[test]
1186 fn set_style_area_transparent_bg_is_noop() {
1187 let mut buf = Buffer::new(1, 1);
1188 let base = PackedRgba::rgb(100, 100, 100);
1189 buf.set(0, 0, Cell::default().with_bg(base));
1190
1191 let transparent = PackedRgba::rgba(255, 0, 0, 0);
1193 set_style_area(
1194 &mut buf,
1195 Rect::new(0, 0, 1, 1),
1196 Style::new().bg(transparent),
1197 );
1198 assert_eq!(buf.get(0, 0).unwrap().bg, base);
1199 }
1200
1201 #[test]
1202 fn set_style_area_opaque_bg_replaces() {
1203 let mut buf = Buffer::new(1, 1);
1204 buf.set(
1205 0,
1206 0,
1207 Cell::default().with_bg(PackedRgba::rgb(100, 100, 100)),
1208 );
1209
1210 let opaque = PackedRgba::rgba(0, 255, 0, 255);
1211 set_style_area(&mut buf, Rect::new(0, 0, 1, 1), Style::new().bg(opaque));
1212 assert_eq!(buf.get(0, 0).unwrap().bg, opaque);
1213 }
1214
1215 #[test]
1216 fn draw_text_span_scrolled_skips_chars() {
1217 let mut pool = GraphemePool::new();
1218 let mut frame = Frame::new(10, 1, &mut pool);
1219 let end_x =
1221 draw_text_span_scrolled(&mut frame, 0, 0, "ABCDE", Style::default(), 10, 2, None);
1222
1223 assert_eq!(end_x, 3);
1224 assert_eq!(frame.buffer.get(0, 0).unwrap().content.as_char(), Some('C'));
1225 assert_eq!(frame.buffer.get(1, 0).unwrap().content.as_char(), Some('D'));
1226 assert_eq!(frame.buffer.get(2, 0).unwrap().content.as_char(), Some('E'));
1227 }
1228}