1use egui::{
10 CornerRadius, FontId, FontSelection, Response, Stroke, TextEdit, Ui, Vec2, Widget, WidgetInfo,
11 WidgetText, WidgetType,
12};
13
14use crate::{flash, theme::Theme};
15
16#[must_use = "Add with `ui.add(...)`."]
38pub struct TextArea<'a> {
39 text: &'a mut String,
40 label: Option<WidgetText>,
41 hint: Option<&'a str>,
42 dirty: bool,
43 rows: usize,
44 desired_width: Option<f32>,
45 monospace: bool,
46 id_salt: Option<egui::Id>,
47}
48
49impl<'a> std::fmt::Debug for TextArea<'a> {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 f.debug_struct("TextArea")
52 .field("dirty", &self.dirty)
53 .field("rows", &self.rows)
54 .field("monospace", &self.monospace)
55 .field("desired_width", &self.desired_width)
56 .finish()
57 }
58}
59
60impl<'a> TextArea<'a> {
61 pub fn new(text: &'a mut String) -> Self {
63 Self {
64 text,
65 label: None,
66 hint: None,
67 dirty: false,
68 rows: 5,
69 desired_width: None,
70 monospace: false,
71 id_salt: None,
72 }
73 }
74
75 pub fn label(mut self, text: impl Into<WidgetText>) -> Self {
77 self.label = Some(text.into());
78 self
79 }
80
81 pub fn hint(mut self, text: &'a str) -> Self {
83 self.hint = Some(text);
84 self
85 }
86
87 pub fn dirty(mut self, dirty: bool) -> Self {
90 self.dirty = dirty;
91 self
92 }
93
94 pub fn rows(mut self, rows: usize) -> Self {
96 self.rows = rows.max(1);
97 self
98 }
99
100 pub fn desired_width(mut self, width: f32) -> Self {
102 self.desired_width = Some(width);
103 self
104 }
105
106 pub fn monospace(mut self, monospace: bool) -> Self {
109 self.monospace = monospace;
110 self
111 }
112
113 pub fn id_salt(mut self, id: impl std::hash::Hash) -> Self {
116 self.id_salt = Some(egui::Id::new(id));
117 self
118 }
119}
120
121impl<'a> Widget for TextArea<'a> {
122 fn ui(self, ui: &mut Ui) -> Response {
123 let theme = Theme::current(ui.ctx());
124 let p = &theme.palette;
125 let t = &theme.typography;
126
127 ui.vertical(|ui| {
128 if let Some(label) = &self.label {
129 ui.add_space(2.0);
130 let rich = egui::RichText::new(label.text())
131 .color(p.text_muted)
132 .size(t.label);
133 ui.add(egui::Label::new(rich).wrap_mode(egui::TextWrapMode::Extend));
134 ui.add_space(2.0);
135 }
136
137 let id_salt = self.id_salt.unwrap_or_else(|| ui.next_auto_id());
138 let widget_id = ui.make_persistent_id(egui::Id::new(id_salt));
139
140 let flash = flash::active_flash(ui.ctx(), widget_id);
141 let bg_fill = flash::background_fill(&theme, p.input_bg, flash);
142
143 let desired_width = self.desired_width.unwrap_or_else(|| ui.available_width());
144 let margin = Vec2::new(theme.control_padding_x * 0.5, theme.control_padding_y);
145
146 let font_id = if self.monospace {
147 FontId::monospace(t.monospace)
148 } else {
149 FontId::proportional(t.body)
150 };
151
152 let response = crate::theme::with_themed_visuals(ui, |ui| {
153 let v = ui.visuals_mut();
154 crate::theme::themed_input_visuals(v, &theme, bg_fill);
155 v.extreme_bg_color = bg_fill;
156 v.selection.bg_fill = crate::theme::with_alpha(p.sky, 90);
157 v.selection.stroke = Stroke::new(1.0, p.sky);
158
159 let mut edit = TextEdit::multiline(self.text)
160 .id_salt(id_salt)
161 .font(FontSelection::FontId(font_id))
162 .text_color(p.text)
163 .margin(margin)
164 .desired_width(desired_width)
165 .desired_rows(self.rows);
166 if let Some(hint) = self.hint {
167 edit = edit.hint_text(egui::RichText::new(hint).color(p.text_faint));
168 }
169
170 ui.add(edit)
171 });
172
173 if self.dirty && ui.is_rect_visible(response.rect) {
174 let stroke_w = 1.0;
178 let bar_w = 3.0;
179 let r = ((theme.control_radius - stroke_w).max(0.0)) as u8;
180 let bar = egui::Rect::from_min_max(
181 egui::pos2(
182 response.rect.min.x + stroke_w,
183 response.rect.min.y + stroke_w,
184 ),
185 egui::pos2(
186 response.rect.min.x + stroke_w + bar_w,
187 response.rect.max.y - stroke_w,
188 ),
189 );
190 let corner = CornerRadius {
191 nw: r,
192 sw: r,
193 ne: 0,
194 se: 0,
195 };
196 ui.painter().rect_filled(bar, corner, p.sky);
197 }
198
199 if let Some(label) = &self.label {
200 let label = label.text().to_string();
201 response.widget_info(|| WidgetInfo::labeled(WidgetType::TextEdit, true, &label));
202 }
203
204 response
205 })
206 .inner
207 }
208}