tui/components/
bordered_text_field.rs1use crate::components::text_field::TextField;
2use crate::components::{Component, Event, ViewContext};
3use crate::line::Line;
4use crate::rendering::frame::Frame;
5use crate::rendering::soft_wrap::{display_width_text, truncate_line};
6use crate::style::Style;
7
8const HORIZONTAL_PADDING: usize = 4;
10const TOP_BORDER_FIXED_COLS: usize = 5;
12
13pub struct BorderedTextField {
21 pub inner: TextField,
22 label: String,
23 width: usize,
24}
25
26impl BorderedTextField {
27 pub fn new(label: impl Into<String>, value: String) -> Self {
28 Self { inner: TextField::new(value), label: label.into(), width: 0 }
29 }
30
31 pub fn set_width(&mut self, width: usize) {
32 self.width = width;
33 self.inner.set_content_width(width.saturating_sub(HORIZONTAL_PADDING).max(1));
34 }
35
36 pub fn set_value(&mut self, value: String) {
37 self.inner.set_value(value);
38 }
39
40 pub fn value(&self) -> &str {
41 &self.inner.value
42 }
43
44 pub fn clear(&mut self) {
45 self.inner.clear();
46 }
47
48 pub fn to_json(&self) -> serde_json::Value {
49 self.inner.to_json()
50 }
51
52 pub fn render_field(&self, context: &ViewContext, focused: bool) -> Vec<Line> {
53 let width = self.width.max(self.min_width());
54 let glyphs = BorderGlyphs::for_focus(focused);
55 let border_color = if focused { context.theme.primary() } else { context.theme.text_secondary() };
56 let border_style = Style::fg(border_color);
57 let label_style = Style::fg(context.theme.text_primary());
58
59 vec![
60 self.top_border(width, glyphs, border_style, label_style),
61 self.middle_row(width, glyphs, border_style, context, focused),
62 Self::bottom_border(width, glyphs, border_style),
63 ]
64 }
65
66 fn min_width(&self) -> usize {
67 TOP_BORDER_FIXED_COLS + 1 + display_width_text(&self.label)
68 }
69
70 fn top_border(&self, width: usize, glyphs: BorderGlyphs, border_style: Style, label_style: Style) -> Line {
71 let label_cols = display_width_text(&self.label);
72 let dash_cols = width.saturating_sub(label_cols + TOP_BORDER_FIXED_COLS);
73
74 let mut line = Line::default();
75 line.push_with_style(format!("{}{} ", glyphs.top_left, glyphs.horizontal), border_style);
76 line.push_with_style(self.label.clone(), label_style);
77 line.push_with_style(" ", border_style);
78 line.push_with_style(glyphs.horizontal.repeat(dash_cols), border_style);
79 line.push_with_style(glyphs.top_right, border_style);
80 line
81 }
82
83 fn middle_row(
84 &self,
85 width: usize,
86 glyphs: BorderGlyphs,
87 border_style: Style,
88 context: &ViewContext,
89 focused: bool,
90 ) -> Line {
91 let content_width = width.saturating_sub(HORIZONTAL_PADDING);
92 let inner_line = self.inner.render_field(context, focused).into_iter().next().unwrap_or_default();
93 let clipped = truncate_line(&inner_line, content_width);
94
95 let mut row = Line::default();
96 row.push_with_style(format!("{} ", glyphs.vertical), border_style);
97 row.append_line(&clipped);
98 row.extend_bg_to_width(width.saturating_sub(2));
99 row.push_with_style(format!(" {}", glyphs.vertical), border_style);
100 row
101 }
102
103 fn bottom_border(width: usize, glyphs: BorderGlyphs, border_style: Style) -> Line {
104 let inner_dashes = width.saturating_sub(2);
105 let mut line = Line::default();
106 line.push_with_style(glyphs.bottom_left, border_style);
107 line.push_with_style(glyphs.horizontal.repeat(inner_dashes), border_style);
108 line.push_with_style(glyphs.bottom_right, border_style);
109 line
110 }
111}
112
113#[derive(Clone, Copy)]
114struct BorderGlyphs {
115 top_left: &'static str,
116 top_right: &'static str,
117 bottom_left: &'static str,
118 bottom_right: &'static str,
119 horizontal: &'static str,
120 vertical: &'static str,
121}
122
123impl BorderGlyphs {
124 const LIGHT: Self =
125 Self {
126 top_left: "┌", top_right: "┐", bottom_left: "└", bottom_right: "┘", horizontal: "─", vertical: "│"
127 };
128 const HEAVY: Self =
129 Self {
130 top_left: "┏", top_right: "┓", bottom_left: "┗", bottom_right: "┛", horizontal: "━", vertical: "┃"
131 };
132
133 fn for_focus(focused: bool) -> Self {
134 if focused { Self::HEAVY } else { Self::LIGHT }
135 }
136}
137
138impl Component for BorderedTextField {
139 type Message = ();
140
141 async fn on_event(&mut self, event: &Event) -> Option<Vec<Self::Message>> {
142 self.inner.on_event(event).await
143 }
144
145 fn render(&mut self, context: &ViewContext) -> Frame {
146 Frame::new(self.render_field(context, true))
147 }
148}