agg_gui/widgets/text_field/
widget_impl.rs1use super::*;
2
3impl Widget for TextField {
4 fn type_name(&self) -> &'static str {
5 "TextField"
6 }
7 fn bounds(&self) -> Rect {
8 self.bounds
9 }
10 fn set_bounds(&mut self, b: Rect) {
11 self.bounds = b;
12 }
13 fn children(&self) -> &[Box<dyn Widget>] {
14 &self.children
15 }
16 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
17 &mut self.children
18 }
19 fn is_focusable(&self) -> bool {
20 true
21 }
22
23 fn focus_id(&self) -> Option<crate::focus::FocusId> {
24 self.focus_request_id
25 }
26
27 fn accepts_text_input(&self) -> bool {
28 !self.read_only
29 }
30
31 fn text_input_value(&self) -> Option<String> {
32 Some(self.text())
33 }
34
35 fn text_input_mode(&self) -> crate::widgets::on_screen_keyboard::KeyboardInputMode {
36 self.keyboard_mode()
37 }
38
39 fn set_label_text(&mut self, text: &str) {
46 if self.focused {
47 return;
48 }
49 if self.edit.borrow().text == text {
50 return;
51 }
52 self.set_text(text);
53 }
54
55 fn needs_draw(&self) -> bool {
66 if !self.focused {
67 return false;
68 }
69 let Some(t) = self.focus_time else {
70 return false;
71 };
72 let current_phase = (t.elapsed().as_millis() / 500) as u64;
73 current_phase != self.blink_last_phase.get()
74 }
75
76 fn next_draw_deadline(&self) -> Option<web_time::Instant> {
77 if !self.focused {
78 return None;
79 }
80 let t = self.focus_time?;
81 let ms = t.elapsed().as_millis() as u64;
82 let next_phase = (ms / 500) + 1;
83 Some(t + std::time::Duration::from_millis(next_phase * 500))
84 }
85
86 fn has_active_modal(&self) -> bool {
89 self.context_menu.is_open()
90 }
91
92 fn paint_global_overlay(&mut self, ctx: &mut dyn DrawCtx) {
95 let font = self.active_font();
96 self.context_menu.paint(ctx, font, self.font_size);
97 }
98
99 fn margin(&self) -> Insets {
100 self.base.margin
101 }
102 fn widget_base(&self) -> Option<&WidgetBase> {
103 Some(&self.base)
104 }
105 fn widget_base_mut(&mut self) -> Option<&mut WidgetBase> {
106 Some(&mut self.base)
107 }
108 fn h_anchor(&self) -> HAnchor {
109 self.base.h_anchor
110 }
111 fn v_anchor(&self) -> VAnchor {
112 self.base.v_anchor
113 }
114 fn min_size(&self) -> Size {
115 self.base.min_size
116 }
117 fn max_size(&self) -> Size {
118 self.base.max_size
119 }
120
121 fn backbuffer_cache_mut(&mut self) -> Option<&mut BackbufferCache> {
122 Some(&mut self.cache)
123 }
124
125 fn backbuffer_mode(&self) -> BackbufferMode {
126 if crate::font_settings::lcd_enabled() {
127 BackbufferMode::LcdCoverage
128 } else {
129 BackbufferMode::Rgba
130 }
131 }
132
133 fn layout(&mut self, available: Size) -> Size {
134 self.sync_from_text_cell();
135 let st = self.edit.borrow();
139 let font = self.active_font();
140 let sig = TextFieldSig {
141 text: st.text.clone(),
142 cursor: st.cursor,
143 anchor: st.anchor,
144 focused: self.focused,
145 hovered: self.hovered,
146 scroll_x_bits: self.scroll_x.to_bits(),
147 w_bits: self.bounds.width.to_bits(),
148 h_bits: self.bounds.height.to_bits(),
149 font_ptr: Arc::as_ptr(&font) as usize,
150 font_size_bits: self.font_size.to_bits(),
151 masking: self.masking_active(),
152 };
153 drop(st);
154 if self.last_sig.as_ref() != Some(&sig) {
155 self.last_sig = Some(sig);
156 self.cache.invalidate();
157 }
158 Size::new(available.width, (self.font_size * 2.4).max(28.0))
159 }
160
161 fn paint(&mut self, ctx: &mut dyn DrawCtx) {
162 let w = self.bounds.width;
163 let h = self.bounds.height;
164 let r = self.theme.border_radius.unwrap_or(6.0);
165 let pad = self.padding;
166 let (raw_text, raw_cursor, raw_anchor) = {
167 let st = self.edit.borrow();
168 (st.text.clone(), st.cursor, st.anchor)
169 };
170 let (text, cursor, anchor) = if self.masking_active() {
173 const BULLET: char = '•';
174 const BULLET_LEN: usize = 3; let n = raw_text.chars().count();
176 let masked = BULLET.to_string().repeat(n);
177 let cur = raw_text[..raw_cursor].chars().count() * BULLET_LEN;
178 let anc = raw_text[..raw_anchor].chars().count() * BULLET_LEN;
179 (masked, cur, anc)
180 } else {
181 (raw_text, raw_cursor, raw_anchor)
182 };
183
184 let v = ctx.visuals();
185 let t = &self.theme;
186
187 ctx.set_fill_color(t.background.unwrap_or(v.widget_bg));
189 ctx.begin_path();
190 ctx.rounded_rect(0.0, 0.0, w, h, r);
191 ctx.fill();
192
193 ctx.clip_rect(pad, 0.0, (w - pad * 2.0).max(0.0), h);
195
196 let font = self.active_font();
197 ctx.set_font(Arc::clone(&font));
198 ctx.set_font_size(self.font_size);
199
200 let m = ctx.measure_text("Ag").unwrap_or_default();
201 let baseline_y = h * 0.5 - (m.ascent - m.descent) * 0.5;
202 let text_x = pad - self.scroll_x;
203
204 if cursor != anchor {
206 let lo = cursor.min(anchor);
207 let hi = cursor.max(anchor);
208 let lo_x = measure_advance(&font, &text[..lo], self.font_size);
209 let hi_x = measure_advance(&font, &text[..hi], self.font_size);
210 let sx = (text_x + lo_x).max(pad);
211 let sw = (text_x + hi_x).min(w - pad) - sx;
212 if sw > 0.0 {
213 let hl_bot = baseline_y - m.descent;
214 let hl_h = (m.ascent + m.descent) * 1.2;
215 ctx.set_fill_color(if self.focused {
216 t.selection_bg.unwrap_or(v.selection_bg)
217 } else {
218 t.selection_bg_unfocused.unwrap_or(v.selection_bg_unfocused)
219 });
220 ctx.begin_path();
221 ctx.rect(sx, hl_bot - hl_h * 0.1, sw, hl_h);
222 ctx.fill();
223 }
224 }
225
226 if text.is_empty() && !self.focused {
228 ctx.set_fill_color(t.placeholder_color.unwrap_or(v.text_dim));
229 ctx.fill_text(&self.placeholder, text_x, baseline_y);
230 } else {
231 ctx.set_fill_color(t.text_color.unwrap_or(v.text_color));
232 ctx.fill_text(&text, text_x, baseline_y);
233 }
234
235 ctx.reset_clip();
239
240 let border_color = if self.focused {
242 t.border_color_focused.unwrap_or(v.accent)
243 } else if self.hovered {
244 t.border_color_hovered.unwrap_or(v.widget_stroke_active)
245 } else {
246 t.border_color.unwrap_or(v.widget_stroke)
247 };
248 ctx.set_stroke_color(border_color);
249 let border_w = if self.focused { 2.0 } else { 1.0 };
250 ctx.set_line_width(border_w);
251 ctx.begin_path();
252 let inset = border_w * 0.5;
253 ctx.rounded_rect(
254 inset,
255 inset,
256 (w - border_w).max(0.0),
257 (h - border_w).max(0.0),
258 r,
259 );
260 ctx.stroke();
261 }
262
263 fn paint_overlay(&mut self, ctx: &mut dyn DrawCtx) {
268 if self.focused {
273 if let Some(t) = self.focus_time {
274 let phase = (t.elapsed().as_millis() / 500) as u64;
275 self.blink_last_phase.set(phase);
276 }
277 }
278
279 let cursor_visible = self.focused
280 && {
281 let st = self.edit.borrow();
282 st.cursor == st.anchor
283 }
284 && match self.focus_time {
285 Some(t) => (t.elapsed().as_millis() / 500) % 2 == 0,
286 None => false,
287 };
288 if !cursor_visible {
289 return;
290 }
291
292 let h = self.bounds.height;
293 let pad = self.padding;
294 let v = ctx.visuals();
295
296 let font = self.active_font();
297 ctx.set_font(Arc::clone(&font));
298 ctx.set_font_size(self.font_size);
299 let m = ctx.measure_text("Ag").unwrap_or_default();
300 let baseline_y = h * 0.5 - (m.ascent - m.descent) * 0.5;
301 let cx = self.caret_x();
302 let top = baseline_y + m.ascent;
303 let bot = baseline_y - m.descent;
304
305 ctx.save();
308 ctx.clip_rect(pad, 0.0, (self.bounds.width - pad * 2.0).max(0.0), h);
309 ctx.set_stroke_color(self.theme.cursor_color.unwrap_or(v.accent));
310 ctx.set_line_width(1.5);
311 ctx.begin_path();
312 ctx.move_to(cx, bot);
313 ctx.line_to(cx, top);
314 ctx.stroke();
315 ctx.restore();
316 }
317
318 fn on_event(&mut self, event: &Event) -> EventResult {
319 if let Some(result) = self.route_context_menu(event) {
322 return result;
323 }
324 match event {
325 Event::MouseMove { pos } => {
326 let was = self.hovered;
327 self.hovered = self.hit_test(*pos);
328 if self.mouse_down && self.focused {
329 let tx = pos.x - self.padding + self.scroll_x;
330 let text = self.edit.borrow().text.clone();
331 let new_cur = self.click_to_cursor(&text, tx);
332 self.extend_selection_drag(&text, new_cur);
333 crate::animation::request_draw();
334 return EventResult::Consumed;
335 }
336 if was != self.hovered {
337 crate::animation::request_draw();
338 return EventResult::Consumed;
339 }
340 EventResult::Ignored
341 }
342
343 Event::MouseDown {
344 pos,
345 button: MouseButton::Left,
346 modifiers: mods,
347 } => {
348 self.mouse_down = true;
349 let tx = pos.x - self.padding + self.scroll_x;
350 let text = self.edit.borrow().text.clone();
351 let new_cur = self.click_to_cursor(&text, tx);
352
353 let clicks = self.multi_click.register(*pos);
358 use crate::widgets::multi_click::SelectGranularity;
359
360 if mods.shift {
361 self.select_granularity = SelectGranularity::Char;
362 self.select_pivot = (new_cur, new_cur);
363 self.edit.borrow_mut().cursor = new_cur;
364 } else if clicks >= 3 {
365 self.select_granularity = SelectGranularity::Line;
366 let len = text.len();
367 self.select_pivot = (0, len);
368 self.edit.borrow_mut().anchor = 0;
369 self.edit.borrow_mut().cursor = len;
370 } else if clicks == 2 {
371 self.select_granularity = SelectGranularity::Word;
372 let (ws, we) = word_range_at(&text, new_cur);
373 self.select_pivot = (ws, we);
374 self.edit.borrow_mut().anchor = ws;
375 self.edit.borrow_mut().cursor = we;
376 } else {
377 self.select_granularity = SelectGranularity::Char;
378 self.select_pivot = (new_cur, new_cur);
379 self.edit.borrow_mut().cursor = new_cur;
380 self.edit.borrow_mut().anchor = new_cur;
381 }
382 self.focus_time = Some(Instant::now());
384 crate::animation::request_draw();
385 EventResult::Consumed
386 }
387
388 Event::MouseDown {
389 pos,
390 button: MouseButton::Right,
391 ..
392 } => {
393 if self.context_menu_enabled {
394 self.open_context_menu(*pos);
395 EventResult::Consumed
396 } else {
397 EventResult::Ignored
398 }
399 }
400
401 Event::MouseUp {
402 button: MouseButton::Left,
403 ..
404 } => {
405 self.mouse_down = false;
406 EventResult::Ignored
407 }
408
409 Event::FocusGained => {
410 self.focused = true;
411 self.focus_time = Some(Instant::now());
412 self.text_on_focus = self.text();
413 if self.select_all_on_focus {
414 let len = self.edit.borrow().text.len();
415 self.edit.borrow_mut().anchor = 0;
416 self.edit.borrow_mut().cursor = len;
417 }
418 crate::animation::request_draw();
419 EventResult::Ignored
420 }
421
422 Event::FocusLost => {
423 let was_focused = self.focused;
424 self.focused = false;
425 self.focus_time = None;
426 self.mouse_down = false;
427 self.flush_pending();
428 if self.text() != self.text_on_focus {
429 self.notify_edit_complete();
430 }
431 if was_focused {
432 crate::animation::request_draw();
433 }
434 EventResult::Ignored
435 }
436
437 Event::KeyDown { key, modifiers } if self.focused => {
438 self.focus_time = Some(Instant::now());
440 let result = self.handle_key(key, *modifiers);
441 if result.is_consumed() {
444 crate::animation::request_draw();
445 }
446 result
447 }
448
449 _ => EventResult::Ignored,
450 }
451 }
452}
453
454impl TextField {
455 pub(crate) fn caret_x(&self) -> f64 {
461 let st = self.edit.borrow();
462 let font = self.active_font();
463 let advance = if self.masking_active() {
464 const BULLET: char = '•';
465 let n = st.text[..st.cursor].chars().count();
466 let masked = BULLET.to_string().repeat(n);
467 measure_advance(&font, &masked, self.font_size)
468 } else {
469 measure_advance(&font, &st.text[..st.cursor], self.font_size)
470 };
471 self.padding - self.scroll_x + advance
472 }
473}