From a6c3daff6f2064c53ed7017065fea3e4003010c0 Mon Sep 17 00:00:00 2001 From: lucaspoffo <35241085+lucaspoffo@users.noreply.github.com> Date: Tue, 6 Jul 2021 13:59:52 -0300 Subject: [PATCH] TextEdit: Add visual clipping for singleline inputs when text is large. (#531) * TextEdit: Add visual clipping for singleline inputs when text is large. * TextEdit: Add reviewer suggestions. --- egui/src/widgets/text_edit.rs | 75 ++++++++++++++++++++++++++--------- 1 file changed, 57 insertions(+), 18 deletions(-) diff --git a/egui/src/widgets/text_edit.rs b/egui/src/widgets/text_edit.rs index 50a229cf..78ec9327 100644 --- a/egui/src/widgets/text_edit.rs +++ b/egui/src/widgets/text_edit.rs @@ -14,6 +14,10 @@ pub(crate) struct State { // If IME candidate window is shown on this text edit. #[cfg_attr(feature = "persistence", serde(skip))] has_ime: bool, + + // Visual offset when editing singleline text bigger than the width. + #[cfg_attr(feature = "persistence", serde(skip))] + singleline_offset: f32, } #[derive(Clone, Copy, Debug, Default)] @@ -455,12 +459,13 @@ impl<'t, S: TextBuffer> TextEdit<'t, S> { .unwrap_or_else(|| ui.style().body_text_style); let line_spacing = ui.fonts().row_height(text_style); let available_width = ui.available_width(); + let desired_width = desired_width.unwrap_or_else(|| ui.spacing().text_edit_width); let make_galley = |ui: &Ui, text: &str| { let text = mask_if_password(text); if multiline { ui.fonts() - .layout_multiline(text_style, text, available_width) + .layout_multiline(text_style, text, desired_width.min(available_width)) } else { ui.fonts().layout_single_line(text_style, text) } @@ -474,10 +479,9 @@ impl<'t, S: TextBuffer> TextEdit<'t, S> { let mut galley = make_galley(ui, text.as_ref()); - let desired_width = desired_width.unwrap_or_else(|| ui.spacing().text_edit_width); let desired_height = (desired_height_rows.at_least(1) as f32) * line_spacing; let desired_size = vec2( - galley.size.x.max(desired_width.min(available_width)), + desired_width.min(available_width), galley.size.y.max(desired_height), ); let (auto_id, rect) = ui.allocate_space(desired_size); @@ -497,20 +501,22 @@ impl<'t, S: TextBuffer> TextEdit<'t, S> { Sense::hover() }; let mut response = ui.interact(rect, id, sense); + let painter = ui.painter_at(Rect::from_min_size(response.rect.min, desired_size)); if enabled { if let Some(pointer_pos) = ui.input().pointer.interact_pos() { // TODO: triple-click to select whole paragraph // TODO: drag selected text to either move or clone (ctrl on windows, alt on mac) - - let cursor_at_pointer = galley.cursor_from_pos(pointer_pos - response.rect.min); + let singleline_offset = vec2(state.singleline_offset, 0.0); + let cursor_at_pointer = + galley.cursor_from_pos(pointer_pos - response.rect.min + singleline_offset); if ui.visuals().text_cursor_preview && response.hovered() && ui.input().pointer.is_moving() { // preview: - paint_cursor_end(ui, response.rect.min, &galley, &cursor_at_pointer); + paint_cursor_end(ui, &painter, response.rect.min, &galley, &cursor_at_pointer); } if response.double_clicked() { @@ -726,10 +732,38 @@ impl<'t, S: TextBuffer> TextEdit<'t, S> { ); } + let mut text_draw_pos = response.rect.min; + + // Visual clipping for singleline text editor with text larger than width + if !multiline { + let cursor_pos = match (state.cursorp, ui.memory().has_focus(id)) { + (Some(cursorp), true) => galley.pos_from_cursor(&cursorp.primary).min.x, + _ => 0.0, + }; + + let mut offset_x = state.singleline_offset; + let visible_range = offset_x..=offset_x + desired_size.x; + + if !visible_range.contains(&cursor_pos) { + if cursor_pos < *visible_range.start() { + offset_x = cursor_pos; + } else { + offset_x = cursor_pos - desired_size.x; + } + } + + offset_x = offset_x + .at_most(galley.size.x - desired_size.x) + .at_least(0.0); + + state.singleline_offset = offset_x; + text_draw_pos -= vec2(offset_x, 0.0); + } + if ui.memory().has_focus(id) { if let Some(cursorp) = state.cursorp { - paint_cursor_selection(ui, response.rect.min, &galley, &cursorp); - paint_cursor_end(ui, response.rect.min, &galley, &cursorp.primary); + paint_cursor_selection(ui, &painter, text_draw_pos, &galley, &cursorp); + paint_cursor_end(ui, &painter, text_draw_pos, &galley, &cursorp.primary); if enabled { ui.ctx().output().text_cursor_pos = Some( @@ -746,18 +780,17 @@ impl<'t, S: TextBuffer> TextEdit<'t, S> { .or(ui.visuals().override_text_color) // .unwrap_or_else(|| ui.style().interact(&response).text_color()); // too bright .unwrap_or_else(|| ui.visuals().widgets.inactive.text_color()); - ui.painter().galley(response.rect.min, galley, text_color); + painter.galley(text_draw_pos, galley, text_color); if text.as_ref().is_empty() && !hint_text.is_empty() { let galley = if multiline { ui.fonts() - .layout_multiline(text_style, hint_text, available_width) + .layout_multiline(text_style, hint_text, desired_size.x) } else { ui.fonts().layout_single_line(text_style, hint_text) }; let hint_text_color = ui.visuals().weak_text_color(); - ui.painter() - .galley(response.rect.min, galley, hint_text_color); + painter.galley(response.rect.min, galley, hint_text_color); } ui.memory().id_data.insert(id, state); @@ -803,7 +836,13 @@ impl<'t, S: TextBuffer> TextEdit<'t, S> { // ---------------------------------------------------------------------------- -fn paint_cursor_selection(ui: &mut Ui, pos: Pos2, galley: &Galley, cursorp: &CursorPair) { +fn paint_cursor_selection( + ui: &mut Ui, + painter: &Painter, + pos: Pos2, + galley: &Galley, + cursorp: &CursorPair, +) { let color = ui.visuals().selection.bg_fill; if cursorp.is_empty() { return; @@ -830,11 +869,11 @@ fn paint_cursor_selection(ui: &mut Ui, pos: Pos2, galley: &Galley, cursorp: &Cur row.max_x() + newline_size }; let rect = Rect::from_min_max(pos + vec2(left, row.y_min), pos + vec2(right, row.y_max)); - ui.painter().rect_filled(rect, 0.0, color); + painter.rect_filled(rect, 0.0, color); } } -fn paint_cursor_end(ui: &mut Ui, pos: Pos2, galley: &Galley, cursor: &Cursor) { +fn paint_cursor_end(ui: &mut Ui, painter: &Painter, pos: Pos2, galley: &Galley, cursor: &Cursor) { let stroke = ui.visuals().selection.stroke; let cursor_pos = galley.pos_from_cursor(cursor).translate(pos.to_vec2()); @@ -843,7 +882,7 @@ fn paint_cursor_end(ui: &mut Ui, pos: Pos2, galley: &Galley, cursor: &Cursor) { let top = cursor_pos.center_top(); let bottom = cursor_pos.center_bottom(); - ui.painter().line_segment( + painter.line_segment( [top, bottom], (ui.visuals().text_cursor_width, stroke.color), ); @@ -852,11 +891,11 @@ fn paint_cursor_end(ui: &mut Ui, pos: Pos2, galley: &Galley, cursor: &Cursor) { // Roof/floor: let extrusion = 3.0; let width = 1.0; - ui.painter().line_segment( + painter.line_segment( [top - vec2(extrusion, 0.0), top + vec2(extrusion, 0.0)], (width, stroke.color), ); - ui.painter().line_segment( + painter.line_segment( [bottom - vec2(extrusion, 0.0), bottom + vec2(extrusion, 0.0)], (width, stroke.color), );