use super::*;
impl Widget for TextField {
fn type_name(&self) -> &'static str {
"TextField"
}
fn bounds(&self) -> Rect {
self.bounds
}
fn set_bounds(&mut self, b: Rect) {
self.bounds = b;
}
fn children(&self) -> &[Box<dyn Widget>] {
&self.children
}
fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
&mut self.children
}
fn is_focusable(&self) -> bool {
true
}
fn needs_draw(&self) -> bool {
if !self.focused {
return false;
}
let Some(t) = self.focus_time else {
return false;
};
let current_phase = (t.elapsed().as_millis() / 500) as u64;
current_phase != self.blink_last_phase.get()
}
fn next_draw_deadline(&self) -> Option<web_time::Instant> {
if !self.focused {
return None;
}
let t = self.focus_time?;
let ms = t.elapsed().as_millis() as u64;
let next_phase = (ms / 500) + 1;
Some(t + std::time::Duration::from_millis(next_phase * 500))
}
fn margin(&self) -> Insets {
self.base.margin
}
fn h_anchor(&self) -> HAnchor {
self.base.h_anchor
}
fn v_anchor(&self) -> VAnchor {
self.base.v_anchor
}
fn min_size(&self) -> Size {
self.base.min_size
}
fn max_size(&self) -> Size {
self.base.max_size
}
fn backbuffer_cache_mut(&mut self) -> Option<&mut BackbufferCache> {
Some(&mut self.cache)
}
fn backbuffer_mode(&self) -> BackbufferMode {
if crate::font_settings::lcd_enabled() {
BackbufferMode::LcdCoverage
} else {
BackbufferMode::Rgba
}
}
fn layout(&mut self, available: Size) -> Size {
self.sync_from_text_cell();
let st = self.edit.borrow();
let font = self.active_font();
let sig = TextFieldSig {
text: st.text.clone(),
cursor: st.cursor,
anchor: st.anchor,
focused: self.focused,
hovered: self.hovered,
scroll_x_bits: self.scroll_x.to_bits(),
w_bits: self.bounds.width.to_bits(),
h_bits: self.bounds.height.to_bits(),
font_ptr: Arc::as_ptr(&font) as usize,
font_size_bits: self.font_size.to_bits(),
};
drop(st);
if self.last_sig.as_ref() != Some(&sig) {
self.last_sig = Some(sig);
self.cache.invalidate();
}
Size::new(available.width, (self.font_size * 2.4).max(28.0))
}
fn paint(&mut self, ctx: &mut dyn DrawCtx) {
let w = self.bounds.width;
let h = self.bounds.height;
let r = 6.0;
let pad = self.padding;
let (raw_text, raw_cursor, raw_anchor) = {
let st = self.edit.borrow();
(st.text.clone(), st.cursor, st.anchor)
};
let (text, cursor, anchor) = if self.password_mode {
const BULLET: char = '•';
const BULLET_LEN: usize = 3; let n = raw_text.chars().count();
let masked = BULLET.to_string().repeat(n);
let cur = raw_text[..raw_cursor].chars().count() * BULLET_LEN;
let anc = raw_text[..raw_anchor].chars().count() * BULLET_LEN;
(masked, cur, anc)
} else {
(raw_text, raw_cursor, raw_anchor)
};
let v = ctx.visuals();
ctx.set_fill_color(v.widget_bg);
ctx.begin_path();
ctx.rounded_rect(0.0, 0.0, w, h, r);
ctx.fill();
ctx.clip_rect(pad, 0.0, (w - pad * 2.0).max(0.0), h);
let font = self.active_font();
ctx.set_font(Arc::clone(&font));
ctx.set_font_size(self.font_size);
let m = ctx.measure_text("Ag").unwrap_or_default();
let baseline_y = h * 0.5 - (m.ascent - m.descent) * 0.5;
let text_x = pad - self.scroll_x;
if cursor != anchor {
let lo = cursor.min(anchor);
let hi = cursor.max(anchor);
let lo_x = measure_advance(&font, &text[..lo], self.font_size);
let hi_x = measure_advance(&font, &text[..hi], self.font_size);
let sx = (text_x + lo_x).max(pad);
let sw = (text_x + hi_x).min(w - pad) - sx;
if sw > 0.0 {
let hl_bot = baseline_y - m.descent;
let hl_h = (m.ascent + m.descent) * 1.2;
ctx.set_fill_color(if self.focused {
v.selection_bg
} else {
v.selection_bg_unfocused
});
ctx.begin_path();
ctx.rect(sx, hl_bot - hl_h * 0.1, sw, hl_h);
ctx.fill();
}
}
if text.is_empty() && !self.focused {
ctx.set_fill_color(v.text_dim);
ctx.fill_text(&self.placeholder, text_x, baseline_y);
} else {
ctx.set_fill_color(v.text_color);
ctx.fill_text(&text, text_x, baseline_y);
}
ctx.reset_clip();
let border_color = if self.focused {
v.accent
} else if self.hovered {
v.widget_stroke_active
} else {
v.widget_stroke
};
ctx.set_stroke_color(border_color);
ctx.set_line_width(if self.focused { 2.0 } else { 1.0 });
ctx.begin_path();
ctx.rounded_rect(0.0, 0.0, w, h, r);
ctx.stroke();
}
fn paint_overlay(&mut self, ctx: &mut dyn DrawCtx) {
if self.focused {
if let Some(t) = self.focus_time {
let phase = (t.elapsed().as_millis() / 500) as u64;
self.blink_last_phase.set(phase);
}
}
let cursor_visible = self.focused
&& {
let st = self.edit.borrow();
st.cursor == st.anchor
}
&& match self.focus_time {
Some(t) => (t.elapsed().as_millis() / 500) % 2 == 0,
None => false,
};
if !cursor_visible {
return;
}
let (text, cursor) = {
let st = self.edit.borrow();
let text = if self.password_mode {
const BULLET: char = '•';
let n = st.text.chars().count();
BULLET.to_string().repeat(n)
} else {
st.text.clone()
};
let cursor = if self.password_mode {
const BULLET_LEN: usize = 3;
st.text[..st.cursor].chars().count() * BULLET_LEN
} else {
st.cursor
};
(text, cursor)
};
let h = self.bounds.height;
let pad = self.padding;
let v = ctx.visuals();
let font = self.active_font();
ctx.set_font(Arc::clone(&font));
ctx.set_font_size(self.font_size);
let m = ctx.measure_text("Ag").unwrap_or_default();
let baseline_y = h * 0.5 - (m.ascent - m.descent) * 0.5;
let text_x = pad - self.scroll_x;
let cx = text_x + measure_advance(&font, &text[..cursor], self.font_size);
let top = baseline_y + m.ascent;
let bot = baseline_y - m.descent;
ctx.save();
ctx.clip_rect(pad, 0.0, (self.bounds.width - pad * 2.0).max(0.0), h);
ctx.set_stroke_color(v.accent);
ctx.set_line_width(1.5);
ctx.begin_path();
ctx.move_to(cx, bot);
ctx.line_to(cx, top);
ctx.stroke();
ctx.restore();
}
fn on_event(&mut self, event: &Event) -> EventResult {
match event {
Event::MouseMove { pos } => {
let was = self.hovered;
self.hovered = self.hit_test(*pos);
if self.mouse_down && self.focused {
let tx = pos.x - self.padding + self.scroll_x;
let text = self.edit.borrow().text.clone();
let new_cur = self.click_to_cursor(&text, tx);
self.edit.borrow_mut().cursor = new_cur;
crate::animation::request_draw();
return EventResult::Consumed;
}
if was != self.hovered {
crate::animation::request_draw();
return EventResult::Consumed;
}
EventResult::Ignored
}
Event::MouseDown {
pos,
button: MouseButton::Left,
modifiers: mods,
} => {
self.mouse_down = true;
let tx = pos.x - self.padding + self.scroll_x;
let text = self.edit.borrow().text.clone();
let new_cur = self.click_to_cursor(&text, tx);
let is_double = self
.last_click_time
.map(|t| t.elapsed().as_millis() < 350)
.unwrap_or(false);
self.last_click_time = Some(Instant::now());
if is_double && !mods.shift {
let (ws, we) = word_range_at(&text, new_cur);
self.edit.borrow_mut().anchor = ws;
self.edit.borrow_mut().cursor = we;
} else if mods.shift {
self.edit.borrow_mut().cursor = new_cur;
} else {
self.edit.borrow_mut().cursor = new_cur;
self.edit.borrow_mut().anchor = new_cur;
}
self.focus_time = Some(Instant::now());
crate::animation::request_draw();
EventResult::Consumed
}
Event::MouseUp {
button: MouseButton::Left,
..
} => {
self.mouse_down = false;
EventResult::Ignored
}
Event::FocusGained => {
self.focused = true;
self.focus_time = Some(Instant::now());
self.text_on_focus = self.text();
if self.select_all_on_focus {
let len = self.edit.borrow().text.len();
self.edit.borrow_mut().anchor = 0;
self.edit.borrow_mut().cursor = len;
}
crate::animation::request_draw();
EventResult::Ignored
}
Event::FocusLost => {
let was_focused = self.focused;
self.focused = false;
self.focus_time = None;
self.mouse_down = false;
self.flush_pending();
if self.text() != self.text_on_focus {
self.notify_edit_complete();
}
if was_focused {
crate::animation::request_draw();
}
EventResult::Ignored
}
Event::KeyDown { key, modifiers } if self.focused => {
self.focus_time = Some(Instant::now());
let result = self.handle_key(key, *modifiers);
if result == EventResult::Consumed {
crate::animation::request_draw();
}
result
}
_ => EventResult::Ignored,
}
}
}