ftui_widgets/focus/
indicator.rs1#![forbid(unsafe_code)]
2
3use ftui_style::Style;
24
25#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
27pub enum FocusIndicatorKind {
28 #[default]
30 StyleOverlay,
31 Underline,
33 Border,
35 None,
37}
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
46pub struct FocusIndicator {
47 kind: FocusIndicatorKind,
48 style: Style,
49}
50
51impl Default for FocusIndicator {
52 fn default() -> Self {
54 Self {
55 kind: FocusIndicatorKind::StyleOverlay,
56 style: Style::new().reverse(),
57 }
58 }
59}
60
61impl FocusIndicator {
62 #[must_use]
64 pub fn style_overlay(style: Style) -> Self {
65 Self {
66 kind: FocusIndicatorKind::StyleOverlay,
67 style,
68 }
69 }
70
71 #[must_use]
73 pub fn underline() -> Self {
74 Self {
75 kind: FocusIndicatorKind::Underline,
76 style: Style::new().underline(),
77 }
78 }
79
80 #[must_use]
82 pub fn border() -> Self {
83 Self {
84 kind: FocusIndicatorKind::Border,
85 style: Style::new().bold(),
86 }
87 }
88
89 #[must_use]
91 pub fn none() -> Self {
92 Self {
93 kind: FocusIndicatorKind::None,
94 style: Style::new(),
95 }
96 }
97
98 #[must_use]
100 pub fn with_style(mut self, style: Style) -> Self {
101 self.style = style;
102 self
103 }
104
105 #[must_use]
107 pub fn with_kind(mut self, kind: FocusIndicatorKind) -> Self {
108 self.kind = kind;
109 self
110 }
111
112 #[inline]
114 #[must_use]
115 pub fn kind(&self) -> FocusIndicatorKind {
116 self.kind
117 }
118
119 #[inline]
121 #[must_use]
122 pub fn style(&self) -> Style {
123 self.style
124 }
125
126 #[inline]
128 #[must_use]
129 pub fn is_visible(&self) -> bool {
130 self.kind != FocusIndicatorKind::None
131 }
132
133 #[must_use]
138 pub fn apply_to(&self, base: Style) -> Style {
139 if self.kind == FocusIndicatorKind::None {
140 return base;
141 }
142 self.style.merge(&base)
143 }
144}
145
146#[cfg(test)]
151mod tests {
152 use super::*;
153 use ftui_render::cell::PackedRgba;
154
155 #[test]
156 fn default_is_reverse_overlay() {
157 let ind = FocusIndicator::default();
158 assert_eq!(ind.kind(), FocusIndicatorKind::StyleOverlay);
159 assert!(ind.is_visible());
160 }
161
162 #[test]
163 fn underline_indicator() {
164 let ind = FocusIndicator::underline();
165 assert_eq!(ind.kind(), FocusIndicatorKind::Underline);
166 assert!(ind.is_visible());
167 }
168
169 #[test]
170 fn border_indicator() {
171 let ind = FocusIndicator::border();
172 assert_eq!(ind.kind(), FocusIndicatorKind::Border);
173 assert!(ind.is_visible());
174 }
175
176 #[test]
177 fn none_indicator_not_visible() {
178 let ind = FocusIndicator::none();
179 assert_eq!(ind.kind(), FocusIndicatorKind::None);
180 assert!(!ind.is_visible());
181 }
182
183 #[test]
184 fn with_style_builder() {
185 let style = Style::new().bold().italic();
186 let ind = FocusIndicator::underline().with_style(style);
187 assert_eq!(ind.style(), style);
188 assert_eq!(ind.kind(), FocusIndicatorKind::Underline);
189 }
190
191 #[test]
192 fn with_kind_builder() {
193 let ind = FocusIndicator::default().with_kind(FocusIndicatorKind::Border);
194 assert_eq!(ind.kind(), FocusIndicatorKind::Border);
195 }
196
197 #[test]
198 fn apply_to_merges_styles() {
199 let base = Style::new().fg(PackedRgba::rgb(255, 0, 0));
200 let ind = FocusIndicator::style_overlay(Style::new().bold());
201 let result = ind.apply_to(base);
202 assert_eq!(result.fg, Some(PackedRgba::rgb(255, 0, 0)));
204 }
205
206 #[test]
207 fn apply_to_none_returns_base() {
208 let base = Style::new().fg(PackedRgba::rgb(255, 0, 0)).bold();
209 let ind = FocusIndicator::none();
210 let result = ind.apply_to(base);
211 assert_eq!(result, base);
212 }
213
214 #[test]
215 fn style_overlay_constructor() {
216 let style = Style::new().italic();
217 let ind = FocusIndicator::style_overlay(style);
218 assert_eq!(ind.kind(), FocusIndicatorKind::StyleOverlay);
219 assert_eq!(ind.style(), style);
220 }
221}