#![forbid(unsafe_code)]
pub mod adaptive_radix;
pub mod align;
pub mod badge;
pub mod block;
pub mod borders;
pub mod cached;
pub mod choreography;
pub mod columns;
pub mod command_palette;
pub mod constraint_overlay;
#[cfg(feature = "debug-overlay")]
pub mod debug_overlay;
pub mod decision_card;
pub mod diagnostics;
pub mod drag;
pub mod drift_visualization;
pub mod elias_fano;
pub mod emoji;
pub mod error_boundary;
pub mod fenwick;
pub mod file_picker;
pub mod focus;
pub mod group;
pub mod height_predictor;
pub mod help;
pub mod help_registry;
pub mod hint_ranker;
pub mod history_panel;
pub mod input;
pub mod inspector;
pub mod json_view;
pub mod keyboard_drag;
pub mod layout;
pub mod layout_debugger;
pub mod list;
pub mod log_ring;
pub mod log_viewer;
pub mod louds;
pub mod measurable;
pub mod measure_cache;
pub mod modal;
pub mod mouse;
pub mod notification_queue;
pub mod padding;
pub mod paginator;
pub mod panel;
pub mod paragraph;
pub mod popover;
pub mod pretty;
pub mod progress;
pub mod rule;
pub mod scrollbar;
pub mod sparkline;
pub mod spinner;
pub mod stateful;
pub mod status_line;
pub mod stopwatch;
pub mod table;
pub mod tabs;
pub mod textarea;
pub mod timer;
pub mod toast;
pub mod tree;
pub mod undo_support;
pub mod validation_error;
pub mod virtualized;
pub mod voi_debug_overlay;
#[cfg(all(test, feature = "tracing"))]
pub(crate) mod tracing_test_support {
use std::sync::{Mutex, MutexGuard, OnceLock};
pub(crate) struct TraceTestGuard {
_lock: MutexGuard<'static, ()>,
}
impl Drop for TraceTestGuard {
fn drop(&mut self) {
tracing::callsite::rebuild_interest_cache();
}
}
pub(crate) fn acquire() -> TraceTestGuard {
static TRACE_TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
let lock = TRACE_TEST_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.unwrap_or_else(|e| e.into_inner());
tracing::callsite::rebuild_interest_cache();
TraceTestGuard { _lock: lock }
}
}
pub use align::{Align, VerticalAlignment};
pub use badge::Badge;
pub use cached::{CacheKey, CachedWidget, CachedWidgetState, FnKey, HashKey, NoCacheKey};
pub use columns::{Column, Columns};
pub use constraint_overlay::{ConstraintOverlay, ConstraintOverlayStyle};
#[cfg(feature = "debug-overlay")]
pub use debug_overlay::{
DebugOverlay, DebugOverlayOptions, DebugOverlayState, DebugOverlayStateful,
DebugOverlayStatefulState,
};
pub use decision_card::DecisionCard;
pub use group::Group;
pub use help_registry::{HelpContent, HelpId, HelpRegistry, Keybinding};
pub use history_panel::{HistoryEntry, HistoryPanel, HistoryPanelMode};
pub use layout_debugger::{LayoutConstraints, LayoutDebugger, LayoutRecord};
pub use log_ring::LogRing;
pub use log_viewer::{LogViewer, LogViewerState, LogWrapMode, SearchConfig, SearchMode};
pub use paginator::{Paginator, PaginatorMode};
pub use panel::Panel;
pub use sparkline::Sparkline;
pub use status_line::{StatusItem, StatusLine};
pub use tabs::{Tab, Tabs, TabsState};
pub use virtualized::{
HeightCache, ItemHeight, RenderItem, Virtualized, VirtualizedList, VirtualizedListState,
VirtualizedStorage,
};
pub use voi_debug_overlay::{
VoiDebugOverlay, VoiDecisionSummary, VoiLedgerEntry, VoiObservationSummary, VoiOverlayData,
VoiOverlayStyle, VoiPosteriorSummary,
};
pub use toast::{
KeyEvent as ToastKeyEvent, Toast, ToastAction, ToastAnimationConfig, ToastAnimationPhase,
ToastAnimationState, ToastConfig, ToastContent, ToastEasing, ToastEntranceAnimation,
ToastEvent, ToastExitAnimation, ToastIcon, ToastId, ToastPosition, ToastState, ToastStyle,
};
pub use notification_queue::{
NotificationPriority, NotificationQueue, QueueAction, QueueConfig, QueueStats,
};
pub use ftui_a11y::Accessible;
pub use mouse::MouseResult;
pub use measurable::{MeasurableWidget, SizeConstraints};
pub use measure_cache::{CacheStats, MeasureCache, WidgetId};
pub use modal::{
BackdropConfig, MODAL_HIT_BACKDROP, MODAL_HIT_CONTENT, Modal, ModalAction, ModalConfig,
ModalPosition, ModalSizeConstraints, ModalState,
};
pub use inspector::{
DiagnosticEntry, DiagnosticEventKind, DiagnosticLog, HitInfo, InspectorMode, InspectorOverlay,
InspectorState, InspectorStyle, TelemetryCallback, TelemetryHooks, WidgetInfo,
diagnostics_enabled, init_diagnostics, is_deterministic_mode, reset_event_counter,
set_diagnostics_enabled,
};
pub use focus::{
FocusEvent, FocusGraph, FocusGroup, FocusId, FocusIndicator, FocusIndicatorKind, FocusManager,
FocusNode, FocusTrap, NavDirection,
};
pub use drag::{
DragConfig, DragPayload, DragState, Draggable, DropPosition, DropResult, DropTarget,
};
pub use stateful::{StateKey, Stateful, VersionedState};
pub use list::ListPersistState;
pub use table::TablePersistState;
pub use tree::TreePersistState;
pub use virtualized::VirtualizedListPersistState;
pub use undo_support::{
ListOperation, ListUndoExt, SelectionOperation, TableOperation, TableUndoExt,
TextEditOperation, TextInputUndoExt, TreeOperation, TreeUndoExt, UndoSupport, UndoWidgetId,
WidgetTextEditCmd,
};
pub use validation_error::{
ANIMATION_DURATION_MS, ERROR_BG_DEFAULT, ERROR_FG_DEFAULT, ERROR_ICON_DEFAULT,
ValidationErrorDisplay, ValidationErrorState,
};
use ftui_core::geometry::Rect;
use ftui_render::buffer::Buffer;
use ftui_render::cell::Cell;
use ftui_render::frame::{Frame, WidgetSignal};
use ftui_style::Style;
use ftui_text::grapheme_width;
#[must_use]
pub(crate) fn a11y_node_id(area: Rect) -> u64 {
const FNV_OFFSET: u64 = 14_695_981_039_346_656_037;
const FNV_PRIME: u64 = 1_099_511_628_211;
let mut h = FNV_OFFSET;
for byte in area
.x
.to_le_bytes()
.iter()
.chain(&area.y.to_le_bytes())
.chain(&area.width.to_le_bytes())
.chain(&area.height.to_le_bytes())
{
h ^= u64::from(*byte);
h = h.wrapping_mul(FNV_PRIME);
}
h
}
pub trait Widget {
fn render(&self, area: Rect, frame: &mut Frame);
fn is_essential(&self) -> bool {
false
}
}
pub struct Budgeted<W> {
widget_id: u64,
signal: WidgetSignal,
inner: W,
}
impl<W> Budgeted<W> {
#[must_use]
pub fn new(widget_id: u64, inner: W) -> Self {
Self {
widget_id,
signal: WidgetSignal::new(widget_id),
inner,
}
}
#[must_use]
pub fn with_signal(mut self, mut signal: WidgetSignal) -> Self {
signal.widget_id = self.widget_id;
self.signal = signal;
self
}
#[must_use]
pub fn inner(&self) -> &W {
&self.inner
}
}
impl<W: Widget> Widget for Budgeted<W> {
fn render(&self, area: Rect, frame: &mut Frame) {
let mut signal = self.signal.clone();
signal.widget_id = self.widget_id;
signal.essential = self.inner.is_essential();
signal.area_cells = area.width as u32 * area.height as u32;
frame.register_widget_signal(signal);
if frame.should_render_widget(self.widget_id, self.inner.is_essential()) {
self.inner.render(area, frame);
}
}
fn is_essential(&self) -> bool {
self.inner.is_essential()
}
}
impl<W: StatefulWidget + Widget> StatefulWidget for Budgeted<W> {
type State = W::State;
fn render(&self, area: Rect, frame: &mut Frame, state: &mut Self::State) {
let mut signal = self.signal.clone();
signal.widget_id = self.widget_id;
signal.essential = self.inner.is_essential();
signal.area_cells = area.width as u32 * area.height as u32;
frame.register_widget_signal(signal);
if frame.should_render_widget(self.widget_id, self.inner.is_essential()) {
StatefulWidget::render(&self.inner, area, frame, state);
}
}
}
pub trait StatefulWidget {
type State;
fn render(&self, area: Rect, frame: &mut Frame, state: &mut Self::State);
}
pub(crate) fn apply_style(cell: &mut Cell, style: Style) {
if let Some(fg) = style.fg {
cell.fg = fg;
}
if let Some(bg) = style.bg {
match bg.a() {
0 => {} 255 => cell.bg = bg, _ => cell.bg = bg.over(cell.bg), }
}
if let Some(attrs) = style.attrs {
let cell_flags: ftui_render::cell::StyleFlags = attrs.into();
cell.attrs = cell.attrs.merged_flags(cell_flags);
}
}
pub(crate) fn set_style_area(buf: &mut Buffer, area: Rect, style: Style) {
if style.is_empty() {
return;
}
let clipped = area.intersection(&buf.current_scissor());
if clipped.is_empty() {
return;
}
let opacity = buf.current_opacity();
let fg = style.fg.map(|fg| fg.with_opacity(opacity));
let bg = style.bg.map(|bg| bg.with_opacity(opacity));
let attrs = style.attrs.map(ftui_render::cell::StyleFlags::from);
for y in clipped.y..clipped.bottom() {
let Some(row) = buf.row_cells_mut_span(y, clipped.x, clipped.right()) else {
continue;
};
for cell in row {
if let Some(fg) = fg {
cell.fg = fg;
}
if let Some(bg) = bg {
match bg.a() {
0 => {} 255 => cell.bg = bg, _ => cell.bg = bg.over(cell.bg), }
}
if let Some(attrs) = attrs {
cell.attrs = cell.attrs.merged_flags(attrs);
}
}
}
}
pub(crate) fn clear_text_area(frame: &mut Frame, area: Rect, style: Style) {
if area.width == 0 || area.height == 0 {
return;
}
let mut cell = Cell::from_char(' ');
apply_style(&mut cell, style);
frame.buffer.fill(area, cell);
}
pub(crate) fn clear_text_row(frame: &mut Frame, area: Rect, style: Style) {
clear_text_area(frame, Rect::new(area.x, area.y, area.width, 1), style);
}
fn inherited_text_cell(
frame: &Frame,
x: u16,
y: u16,
content: ftui_render::cell::CellContent,
) -> Cell {
let mut cell = frame.buffer.get(x, y).copied().unwrap_or_default();
cell.content = content;
cell.attrs = ftui_render::cell::CellAttrs::new(cell.attrs.flags(), 0);
cell
}
pub(crate) fn draw_text_span(
frame: &mut Frame,
mut x: u16,
y: u16,
content: &str,
style: Style,
max_x: u16,
) -> u16 {
use unicode_segmentation::UnicodeSegmentation;
for grapheme in content.graphemes(true) {
if x >= max_x {
break;
}
let w = grapheme_width(grapheme);
if w == 0 {
continue;
}
if x.saturating_add(w as u16) > max_x {
break;
}
let cell_content = if w > 1 || grapheme.chars().count() > 1 {
let id = frame.intern_with_width(grapheme, w as u8);
ftui_render::cell::CellContent::from_grapheme(id)
} else if let Some(c) = grapheme.chars().next() {
ftui_render::cell::CellContent::from_char(c)
} else {
continue;
};
let mut cell = inherited_text_cell(frame, x, y, cell_content);
apply_style(&mut cell, style);
frame.buffer.set_fast(x, y, cell);
x = x.saturating_add(w as u16);
}
x
}
#[allow(dead_code)]
pub(crate) fn draw_text_span_with_link(
frame: &mut Frame,
x: u16,
y: u16,
content: &str,
style: Style,
max_x: u16,
link_url: Option<&str>,
) -> u16 {
draw_text_span_scrolled(frame, x, y, content, style, max_x, 0, link_url)
}
#[allow(dead_code, clippy::too_many_arguments)]
pub(crate) fn draw_text_span_scrolled(
frame: &mut Frame,
mut x: u16,
y: u16,
content: &str,
style: Style,
max_x: u16,
scroll_x: u16,
link_url: Option<&str>,
) -> u16 {
use unicode_segmentation::UnicodeSegmentation;
let link_id = if let Some(url) = link_url {
frame.register_link(url)
} else {
0
};
let mut visual_pos: u16 = 0;
for grapheme in content.graphemes(true) {
if x >= max_x {
break;
}
let w = grapheme_width(grapheme);
if w == 0 {
continue;
}
let next_visual_pos = visual_pos.saturating_add(w as u16);
if next_visual_pos <= scroll_x {
visual_pos = next_visual_pos;
continue;
}
if visual_pos < scroll_x {
visual_pos = next_visual_pos;
continue;
}
if x.saturating_add(w as u16) > max_x {
break;
}
let cell_content = if w > 1 || grapheme.chars().count() > 1 {
let id = frame.intern_with_width(grapheme, w as u8);
ftui_render::cell::CellContent::from_grapheme(id)
} else if let Some(c) = grapheme.chars().next() {
ftui_render::cell::CellContent::from_char(c)
} else {
continue;
};
let mut cell = inherited_text_cell(frame, x, y, cell_content);
apply_style(&mut cell, style);
if link_id != 0 {
cell.attrs = cell.attrs.with_link(link_id);
}
frame.buffer.set_fast(x, y, cell);
x = x.saturating_add(w as u16);
visual_pos = next_visual_pos;
}
x
}
pub(crate) fn contains_ignore_case(haystack: &str, needle_lower: &str) -> bool {
if needle_lower.is_empty() {
return true;
}
if haystack.is_ascii() && needle_lower.is_ascii() {
let haystack_bytes = haystack.as_bytes();
let needle_bytes = needle_lower.as_bytes();
if needle_bytes.len() > haystack_bytes.len() {
return false;
}
for i in 0..=haystack_bytes.len() - needle_bytes.len() {
let mut match_found = true;
for (j, &b) in needle_bytes.iter().enumerate() {
if haystack_bytes[i + j].to_ascii_lowercase() != b {
match_found = false;
break;
}
}
if match_found {
return true;
}
}
return false;
}
haystack.to_lowercase().contains(needle_lower)
}
#[cfg(test)]
mod tests {
use super::*;
use ftui_render::cell::PackedRgba;
use ftui_render::grapheme_pool::GraphemePool;
#[test]
fn apply_style_sets_fg() {
let mut cell = Cell::default();
let style = Style::new().fg(PackedRgba::rgb(255, 0, 0));
apply_style(&mut cell, style);
assert_eq!(cell.fg, PackedRgba::rgb(255, 0, 0));
}
#[test]
fn apply_style_sets_bg() {
let mut cell = Cell::default();
let style = Style::new().bg(PackedRgba::rgb(0, 255, 0));
apply_style(&mut cell, style);
assert_eq!(cell.bg, PackedRgba::rgb(0, 255, 0));
}
#[test]
fn apply_style_preserves_content() {
let mut cell = Cell::from_char('Z');
let style = Style::new().fg(PackedRgba::rgb(1, 2, 3));
apply_style(&mut cell, style);
assert_eq!(cell.content.as_char(), Some('Z'));
}
#[test]
fn apply_style_empty_is_noop() {
let original = Cell::default();
let mut cell = Cell::default();
apply_style(&mut cell, Style::default());
assert_eq!(cell.fg, original.fg);
assert_eq!(cell.bg, original.bg);
}
#[test]
fn apply_style_bg_only_preserves_fg() {
let mut cell = Cell::from_char('x').with_fg(PackedRgba::rgb(0, 200, 0));
let selection = Style::new().bg(PackedRgba::rgb(0, 0, 180));
apply_style(&mut cell, selection);
assert_eq!(cell.fg, PackedRgba::rgb(0, 200, 0));
assert_eq!(cell.bg, PackedRgba::rgb(0, 0, 180));
}
#[test]
fn apply_style_composites_alpha_bg() {
let base_bg = PackedRgba::rgb(200, 0, 0);
let mut cell = Cell::default().with_bg(base_bg);
let overlay = PackedRgba::rgba(0, 0, 200, 128);
apply_style(&mut cell, Style::new().bg(overlay));
assert_eq!(cell.bg, overlay.over(base_bg));
}
#[test]
fn apply_style_transparent_bg_is_noop() {
let base_bg = PackedRgba::rgb(100, 100, 100);
let mut cell = Cell::default().with_bg(base_bg);
apply_style(&mut cell, Style::new().bg(PackedRgba::rgba(255, 0, 0, 0)));
assert_eq!(cell.bg, base_bg);
}
#[test]
fn apply_style_merges_attrs_not_replaces() {
use ftui_render::cell::StyleFlags as CellFlags;
let mut cell = Cell::default();
cell.attrs = cell.attrs.with_flags(CellFlags::BOLD);
let overlay = Style::new().italic();
apply_style(&mut cell, overlay);
assert!(cell.attrs.has_flag(CellFlags::BOLD), "BOLD must survive");
assert!(
cell.attrs.has_flag(CellFlags::ITALIC),
"ITALIC must be added"
);
}
#[test]
fn set_style_area_bg_only_preserves_per_cell_fg() {
let mut buf = Buffer::new(3, 1);
buf.set(0, 0, Cell::from_char('R').with_fg(PackedRgba::RED));
buf.set(1, 0, Cell::from_char('G').with_fg(PackedRgba::GREEN));
buf.set(2, 0, Cell::from_char('B').with_fg(PackedRgba::BLUE));
let highlight = Style::new().bg(PackedRgba::rgb(40, 40, 40));
set_style_area(&mut buf, Rect::new(0, 0, 3, 1), highlight);
assert_eq!(buf.get(0, 0).unwrap().fg, PackedRgba::RED);
assert_eq!(buf.get(1, 0).unwrap().fg, PackedRgba::GREEN);
assert_eq!(buf.get(2, 0).unwrap().fg, PackedRgba::BLUE);
assert_eq!(buf.get(0, 0).unwrap().bg, PackedRgba::rgb(40, 40, 40));
}
#[test]
fn set_style_area_merges_attrs_not_replaces() {
use ftui_render::cell::StyleFlags as CellFlags;
let mut buf = Buffer::new(1, 1);
let mut cell = Cell::from_char('X');
cell.attrs = cell.attrs.with_flags(CellFlags::BOLD);
buf.set(0, 0, cell);
set_style_area(&mut buf, Rect::new(0, 0, 1, 1), Style::new().italic());
let result = buf.get(0, 0).unwrap();
assert!(result.attrs.has_flag(CellFlags::BOLD), "BOLD must survive");
assert!(
result.attrs.has_flag(CellFlags::ITALIC),
"ITALIC must be added"
);
}
#[test]
fn set_style_area_applies_to_all_cells() {
let mut buf = Buffer::new(3, 2);
let area = Rect::new(0, 0, 3, 2);
let style = Style::new().bg(PackedRgba::rgb(10, 20, 30));
set_style_area(&mut buf, area, style);
for y in 0..2 {
for x in 0..3 {
assert_eq!(
buf.get(x, y).unwrap().bg,
PackedRgba::rgb(10, 20, 30),
"cell ({x},{y}) should have style applied"
);
}
}
}
#[test]
fn set_style_area_composites_alpha_bg_over_existing_bg() {
let mut buf = Buffer::new(1, 1);
let base = PackedRgba::rgb(200, 0, 0);
buf.set(0, 0, Cell::default().with_bg(base));
let overlay = PackedRgba::rgba(0, 0, 200, 128);
set_style_area(&mut buf, Rect::new(0, 0, 1, 1), Style::new().bg(overlay));
let expected = overlay.over(base);
assert_eq!(buf.get(0, 0).unwrap().bg, expected);
}
#[test]
fn set_style_area_partial_rect() {
let mut buf = Buffer::new(5, 5);
let area = Rect::new(1, 1, 2, 2);
let style = Style::new().fg(PackedRgba::rgb(99, 99, 99));
set_style_area(&mut buf, area, style);
assert_eq!(buf.get(1, 1).unwrap().fg, PackedRgba::rgb(99, 99, 99));
assert_eq!(buf.get(2, 2).unwrap().fg, PackedRgba::rgb(99, 99, 99));
assert_ne!(buf.get(0, 0).unwrap().fg, PackedRgba::rgb(99, 99, 99));
}
#[test]
fn set_style_area_empty_style_is_noop() {
let mut buf = Buffer::new(3, 3);
buf.set(0, 0, Cell::from_char('A'));
let original_fg = buf.get(0, 0).unwrap().fg;
set_style_area(&mut buf, Rect::new(0, 0, 3, 3), Style::default());
assert_eq!(buf.get(0, 0).unwrap().fg, original_fg);
assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('A'));
}
#[test]
fn set_style_area_respects_scissor() {
let mut buf = Buffer::new(3, 3);
let style = Style::new().bg(PackedRgba::rgb(10, 20, 30));
buf.push_scissor(Rect::new(1, 1, 1, 1));
set_style_area(&mut buf, Rect::new(0, 0, 3, 3), style);
assert_eq!(buf.get(1, 1).unwrap().bg, PackedRgba::rgb(10, 20, 30));
assert_ne!(buf.get(0, 1).unwrap().bg, PackedRgba::rgb(10, 20, 30));
assert_ne!(buf.get(1, 0).unwrap().bg, PackedRgba::rgb(10, 20, 30));
assert_ne!(buf.get(2, 2).unwrap().bg, PackedRgba::rgb(10, 20, 30));
}
#[test]
fn set_style_area_respects_opacity_stack() {
let mut buf = Buffer::new(1, 1);
let base_fg = PackedRgba::rgb(20, 30, 40);
let base_bg = PackedRgba::rgb(50, 60, 70);
buf.set(0, 0, Cell::from_char('X').with_fg(base_fg).with_bg(base_bg));
let overlay_fg = PackedRgba::rgb(200, 100, 0);
let overlay_bg = PackedRgba::rgb(0, 0, 200);
buf.push_opacity(0.5);
set_style_area(
&mut buf,
Rect::new(0, 0, 1, 1),
Style::new().fg(overlay_fg).bg(overlay_bg),
);
let cell = buf.get(0, 0).unwrap();
assert_eq!(cell.fg, overlay_fg.with_opacity(0.5));
assert_eq!(cell.bg, overlay_bg.with_opacity(0.5).over(base_bg));
}
#[test]
fn draw_text_span_basic() {
let mut pool = GraphemePool::new();
let mut frame = Frame::new(10, 1, &mut pool);
let end_x = draw_text_span(&mut frame, 0, 0, "ABC", Style::default(), 10);
assert_eq!(end_x, 3);
assert_eq!(frame.buffer.get(0, 0).unwrap().content.as_char(), Some('A'));
assert_eq!(frame.buffer.get(1, 0).unwrap().content.as_char(), Some('B'));
assert_eq!(frame.buffer.get(2, 0).unwrap().content.as_char(), Some('C'));
}
#[test]
fn draw_text_span_clipped_at_max_x() {
let mut pool = GraphemePool::new();
let mut frame = Frame::new(10, 1, &mut pool);
let end_x = draw_text_span(&mut frame, 0, 0, "ABCDEF", Style::default(), 3);
assert_eq!(end_x, 3);
assert_eq!(frame.buffer.get(0, 0).unwrap().content.as_char(), Some('A'));
assert_eq!(frame.buffer.get(2, 0).unwrap().content.as_char(), Some('C'));
assert!(frame.buffer.get(3, 0).unwrap().is_empty());
}
#[test]
fn draw_text_span_starts_at_offset() {
let mut pool = GraphemePool::new();
let mut frame = Frame::new(10, 1, &mut pool);
let end_x = draw_text_span(&mut frame, 5, 0, "XY", Style::default(), 10);
assert_eq!(end_x, 7);
assert_eq!(frame.buffer.get(5, 0).unwrap().content.as_char(), Some('X'));
assert_eq!(frame.buffer.get(6, 0).unwrap().content.as_char(), Some('Y'));
assert!(frame.buffer.get(4, 0).unwrap().is_empty());
}
#[test]
fn draw_text_span_empty_string() {
let mut pool = GraphemePool::new();
let mut frame = Frame::new(5, 1, &mut pool);
let end_x = draw_text_span(&mut frame, 0, 0, "", Style::default(), 5);
assert_eq!(end_x, 0);
}
#[test]
fn draw_text_span_applies_style() {
let mut pool = GraphemePool::new();
let mut frame = Frame::new(5, 1, &mut pool);
let style = Style::new().fg(PackedRgba::rgb(255, 128, 0));
draw_text_span(&mut frame, 0, 0, "A", style, 5);
assert_eq!(
frame.buffer.get(0, 0).unwrap().fg,
PackedRgba::rgb(255, 128, 0)
);
}
#[test]
fn draw_text_span_preserves_existing_overlay_fg_and_bg() {
let mut pool = GraphemePool::new();
let mut frame = Frame::new(3, 1, &mut pool);
frame.buffer.set(
0,
0,
Cell::from_char('x').with_fg(PackedRgba::rgb(200, 40, 10)),
);
set_style_area(
&mut frame.buffer,
Rect::new(0, 0, 1, 1),
Style::new().bg(PackedRgba::rgb(20, 30, 40)),
);
draw_text_span(&mut frame, 0, 0, "A", Style::default(), 1);
let cell = frame.buffer.get(0, 0).unwrap();
assert_eq!(cell.content.as_char(), Some('A'));
assert_eq!(cell.fg, PackedRgba::rgb(200, 40, 10));
assert_eq!(cell.bg, PackedRgba::rgb(20, 30, 40));
}
#[test]
fn draw_text_span_drops_stale_link_id_but_keeps_style_flags() {
use ftui_render::cell::{CellAttrs, StyleFlags as CellFlags};
let mut pool = GraphemePool::new();
let mut frame = Frame::new(3, 1, &mut pool);
frame.buffer.set(
0,
0,
Cell::from_char('x').with_attrs(CellAttrs::new(CellFlags::UNDERLINE, 42)),
);
draw_text_span(&mut frame, 0, 0, "A", Style::default(), 1);
let cell = frame.buffer.get(0, 0).unwrap();
assert_eq!(cell.content.as_char(), Some('A'));
assert!(cell.attrs.has_flag(CellFlags::UNDERLINE));
assert_eq!(cell.attrs.link_id(), 0);
}
#[test]
fn draw_text_span_max_x_at_start_draws_nothing() {
let mut pool = GraphemePool::new();
let mut frame = Frame::new(5, 1, &mut pool);
let end_x = draw_text_span(&mut frame, 3, 0, "ABC", Style::default(), 3);
assert_eq!(end_x, 3);
assert!(frame.buffer.get(3, 0).unwrap().is_empty());
}
#[test]
fn widget_is_essential_default_false() {
struct DummyWidget;
impl Widget for DummyWidget {
fn render(&self, _: Rect, _: &mut Frame) {}
}
assert!(!DummyWidget.is_essential());
}
#[test]
fn budgeted_new_and_inner() {
struct TestW;
impl Widget for TestW {
fn render(&self, _: Rect, _: &mut Frame) {}
}
let b = Budgeted::new(42, TestW);
assert_eq!(b.widget_id, 42);
let _ = b.inner(); }
#[test]
fn budgeted_with_signal() {
struct TestW;
impl Widget for TestW {
fn render(&self, _: Rect, _: &mut Frame) {}
}
let sig = WidgetSignal::new(99);
let b = Budgeted::new(42, TestW).with_signal(sig);
assert_eq!(b.signal.widget_id, 42);
}
#[test]
fn set_style_area_transparent_bg_is_noop() {
let mut buf = Buffer::new(1, 1);
let base = PackedRgba::rgb(100, 100, 100);
buf.set(0, 0, Cell::default().with_bg(base));
let transparent = PackedRgba::rgba(255, 0, 0, 0);
set_style_area(
&mut buf,
Rect::new(0, 0, 1, 1),
Style::new().bg(transparent),
);
assert_eq!(buf.get(0, 0).unwrap().bg, base);
}
#[test]
fn set_style_area_opaque_bg_replaces() {
let mut buf = Buffer::new(1, 1);
buf.set(
0,
0,
Cell::default().with_bg(PackedRgba::rgb(100, 100, 100)),
);
let opaque = PackedRgba::rgba(0, 255, 0, 255);
set_style_area(&mut buf, Rect::new(0, 0, 1, 1), Style::new().bg(opaque));
assert_eq!(buf.get(0, 0).unwrap().bg, opaque);
}
#[test]
fn draw_text_span_scrolled_skips_chars() {
let mut pool = GraphemePool::new();
let mut frame = Frame::new(10, 1, &mut pool);
let end_x =
draw_text_span_scrolled(&mut frame, 0, 0, "ABCDE", Style::default(), 10, 2, None);
assert_eq!(end_x, 3);
assert_eq!(frame.buffer.get(0, 0).unwrap().content.as_char(), Some('C'));
assert_eq!(frame.buffer.get(1, 0).unwrap().content.as_char(), Some('D'));
assert_eq!(frame.buffer.get(2, 0).unwrap().content.as_char(), Some('E'));
}
}