Implement blinking text cursor in `TextEdit` (#4279)
On by default. Can be set with `style.text_cursor.blink`. * Closes https://github.com/emilk/egui/pull/4121
This commit is contained in:
parent
d3c6895443
commit
3b147c066b
|
|
@ -647,6 +647,39 @@ pub struct Interaction {
|
|||
pub multi_widget_text_select: bool,
|
||||
}
|
||||
|
||||
/// Look and feel of the text cursor.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct TextCursorStyle {
|
||||
/// The color and width of the text cursor
|
||||
pub stroke: Stroke,
|
||||
|
||||
/// Show where the text cursor would be if you clicked?
|
||||
pub preview: bool,
|
||||
|
||||
/// Should the cursor blink?
|
||||
pub blink: bool,
|
||||
|
||||
/// When blinking, this is how long the cursor is visible.
|
||||
pub on_duration: f32,
|
||||
|
||||
/// When blinking, this is how long the cursor is invisible.
|
||||
pub off_duration: f32,
|
||||
}
|
||||
|
||||
impl Default for TextCursorStyle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
stroke: Stroke::new(2.0, Color32::from_rgb(192, 222, 255)), // Dark mode
|
||||
preview: false,
|
||||
blink: true,
|
||||
on_duration: 0.5,
|
||||
off_duration: 0.5,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Controls the visual style (colors etc) of egui.
|
||||
///
|
||||
/// You can change the visuals of a [`Ui`] with [`Ui::visuals_mut`]
|
||||
|
|
@ -722,11 +755,8 @@ pub struct Visuals {
|
|||
|
||||
pub resize_corner_size: f32,
|
||||
|
||||
/// The color and width of the text cursor
|
||||
pub text_cursor: Stroke,
|
||||
|
||||
/// show where the text cursor would be if you clicked
|
||||
pub text_cursor_preview: bool,
|
||||
/// How the text cursor acts.
|
||||
pub text_cursor: TextCursorStyle,
|
||||
|
||||
/// Allow child widgets to be just on the border and still have a stroke with some thickness
|
||||
pub clip_rect_margin: f32,
|
||||
|
|
@ -1094,8 +1124,7 @@ impl Visuals {
|
|||
|
||||
resize_corner_size: 12.0,
|
||||
|
||||
text_cursor: Stroke::new(2.0, Color32::from_rgb(192, 222, 255)),
|
||||
text_cursor_preview: false,
|
||||
text_cursor: Default::default(),
|
||||
|
||||
clip_rect_margin: 3.0, // should be at least half the size of the widest frame stroke + max WidgetVisuals::expansion
|
||||
button_frame: true,
|
||||
|
|
@ -1146,7 +1175,10 @@ impl Visuals {
|
|||
color: Color32::from_black_alpha(25),
|
||||
},
|
||||
|
||||
text_cursor: Stroke::new(2.0, Color32::from_rgb(0, 83, 125)),
|
||||
text_cursor: TextCursorStyle {
|
||||
stroke: Stroke::new(2.0, Color32::from_rgb(0, 83, 125)),
|
||||
..Default::default()
|
||||
},
|
||||
|
||||
..Self::dark()
|
||||
}
|
||||
|
|
@ -1737,8 +1769,9 @@ impl Visuals {
|
|||
popup_shadow,
|
||||
|
||||
resize_corner_size,
|
||||
|
||||
text_cursor,
|
||||
text_cursor_preview,
|
||||
|
||||
clip_rect_margin,
|
||||
button_frame,
|
||||
collapsing_header_frame,
|
||||
|
|
@ -1834,16 +1867,14 @@ impl Visuals {
|
|||
);
|
||||
|
||||
ui_color(ui, hyperlink_color, "hyperlink_color");
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Text cursor");
|
||||
ui.add(text_cursor);
|
||||
});
|
||||
ui.collapsing("Text cursor", |ui| {
|
||||
text_cursor.ui(ui);
|
||||
});
|
||||
|
||||
ui.collapsing("Misc", |ui| {
|
||||
ui.add(Slider::new(resize_corner_size, 0.0..=20.0).text("resize_corner_size"));
|
||||
ui.checkbox(text_cursor_preview, "Preview text cursor on hover");
|
||||
ui.add(Slider::new(clip_rect_margin, 0.0..=20.0).text("clip_rect_margin"));
|
||||
|
||||
ui.checkbox(button_frame, "Button has a frame");
|
||||
|
|
@ -1887,6 +1918,49 @@ impl Visuals {
|
|||
}
|
||||
}
|
||||
|
||||
impl TextCursorStyle {
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
let Self {
|
||||
stroke,
|
||||
preview,
|
||||
blink,
|
||||
on_duration,
|
||||
off_duration,
|
||||
} = self;
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Stroke");
|
||||
ui.add(stroke);
|
||||
});
|
||||
|
||||
ui.checkbox(preview, "Preview text cursor on hover");
|
||||
|
||||
ui.checkbox(blink, "Blink");
|
||||
|
||||
if *blink {
|
||||
Grid::new("cursor_blink").show(ui, |ui| {
|
||||
ui.label("On time");
|
||||
ui.add(
|
||||
DragValue::new(on_duration)
|
||||
.speed(0.1)
|
||||
.clamp_range(0.0..=2.0)
|
||||
.suffix(" s"),
|
||||
);
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Off time");
|
||||
ui.add(
|
||||
DragValue::new(off_duration)
|
||||
.speed(0.1)
|
||||
.clamp_range(0.0..=2.0)
|
||||
.suffix(" s"),
|
||||
);
|
||||
ui.end_row();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
impl DebugOptions {
|
||||
pub fn ui(&mut self, ui: &mut crate::Ui) {
|
||||
|
|
|
|||
|
|
@ -51,8 +51,10 @@ pub fn paint_text_selection(
|
|||
}
|
||||
|
||||
/// Paint one end of the selection, e.g. the primary cursor.
|
||||
pub fn paint_cursor(painter: &Painter, visuals: &Visuals, cursor_rect: Rect) {
|
||||
let stroke = visuals.text_cursor;
|
||||
///
|
||||
/// This will never blink.
|
||||
pub fn paint_cursor_end(painter: &Painter, visuals: &Visuals, cursor_rect: Rect) {
|
||||
let stroke = visuals.text_cursor.stroke;
|
||||
|
||||
let top = cursor_rect.center_top();
|
||||
let bottom = cursor_rect.center_bottom();
|
||||
|
|
@ -73,3 +75,33 @@ pub fn paint_cursor(painter: &Painter, visuals: &Visuals, cursor_rect: Rect) {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Paint one end of the selection, e.g. the primary cursor, with blinking (if enabled).
|
||||
pub fn paint_text_cursor(
|
||||
ui: &mut Ui,
|
||||
painter: &Painter,
|
||||
primary_cursor_rect: Rect,
|
||||
time_since_last_edit: f64,
|
||||
) {
|
||||
if ui.visuals().text_cursor.blink {
|
||||
let on_duration = ui.visuals().text_cursor.on_duration;
|
||||
let off_duration = ui.visuals().text_cursor.off_duration;
|
||||
let total_duration = on_duration + off_duration;
|
||||
|
||||
let time_in_cycle = (time_since_last_edit % (total_duration as f64)) as f32;
|
||||
|
||||
let wake_in = if time_in_cycle < on_duration {
|
||||
// Cursor is visible
|
||||
paint_cursor_end(painter, ui.visuals(), primary_cursor_rect);
|
||||
on_duration - time_in_cycle
|
||||
} else {
|
||||
// Cursor is not visible
|
||||
total_duration - time_in_cycle
|
||||
};
|
||||
|
||||
ui.ctx()
|
||||
.request_repaint_after(std::time::Duration::from_secs_f32(wake_in));
|
||||
} else {
|
||||
paint_cursor_end(painter, ui.visuals(), primary_cursor_rect);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,7 @@ use crate::{
|
|||
os::OperatingSystem,
|
||||
output::OutputEvent,
|
||||
text_selection::{
|
||||
text_cursor_state::cursor_rect,
|
||||
visuals::{paint_cursor, paint_text_selection},
|
||||
CCursorRange, CursorRange,
|
||||
text_cursor_state::cursor_rect, visuals::paint_text_selection, CCursorRange, CursorRange,
|
||||
},
|
||||
*,
|
||||
};
|
||||
|
|
@ -544,14 +542,14 @@ impl<'t> TextEdit<'t> {
|
|||
let cursor_at_pointer =
|
||||
galley.cursor_from_pos(pointer_pos - rect.min + singleline_offset);
|
||||
|
||||
if ui.visuals().text_cursor_preview
|
||||
if ui.visuals().text_cursor.preview
|
||||
&& response.hovered()
|
||||
&& ui.input(|i| i.pointer.is_moving())
|
||||
{
|
||||
// preview:
|
||||
// text cursor preview:
|
||||
let cursor_rect =
|
||||
cursor_rect(rect.min, &galley, &cursor_at_pointer, row_height);
|
||||
paint_cursor(&painter, ui.visuals(), cursor_rect);
|
||||
text_selection::visuals::paint_cursor_end(&painter, ui.visuals(), cursor_rect);
|
||||
}
|
||||
|
||||
let is_being_dragged = ui.ctx().is_being_dragged(response.id);
|
||||
|
|
@ -683,18 +681,32 @@ impl<'t> TextEdit<'t> {
|
|||
ui.scroll_to_rect(primary_cursor_rect, None);
|
||||
}
|
||||
|
||||
if text.is_mutable() {
|
||||
paint_cursor(&painter, ui.visuals(), primary_cursor_rect);
|
||||
|
||||
if interactive {
|
||||
// For IME, so only set it when text is editable and visible!
|
||||
ui.ctx().output_mut(|o| {
|
||||
o.ime = Some(crate::output::IMEOutput {
|
||||
rect,
|
||||
cursor_rect: primary_cursor_rect,
|
||||
});
|
||||
});
|
||||
if text.is_mutable() && interactive {
|
||||
let now = ui.ctx().input(|i| i.time);
|
||||
if response.changed || selection_changed {
|
||||
state.last_edit_time = now;
|
||||
}
|
||||
|
||||
// Only show (and blink) cursor if the egui viewport has focus.
|
||||
// This is for two reasons:
|
||||
// * Don't give the impression that the user can type into a window without focus
|
||||
// * Don't repaint the ui because of a blinking cursor in an app that is not in focus
|
||||
if ui.ctx().input(|i| i.focused) {
|
||||
text_selection::visuals::paint_text_cursor(
|
||||
ui,
|
||||
&painter,
|
||||
primary_cursor_rect,
|
||||
now - state.last_edit_time,
|
||||
);
|
||||
}
|
||||
|
||||
// For IME, so only set it when text is editable and visible!
|
||||
ui.ctx().output_mut(|o| {
|
||||
o.ime = Some(crate::output::IMEOutput {
|
||||
rect,
|
||||
cursor_rect: primary_cursor_rect,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,11 @@ pub struct TextEditState {
|
|||
// Visual offset when editing singleline text bigger than the width.
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
pub(crate) singleline_offset: f32,
|
||||
|
||||
/// When did the user last press a key?
|
||||
/// Used to pause the cursor animation when typing.
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
pub(crate) last_edit_time: f64,
|
||||
}
|
||||
|
||||
impl TextEditState {
|
||||
|
|
|
|||
Loading…
Reference in New Issue