added text edit history / undo (ctrl-z)

This commit is contained in:
2025-11-22 20:37:37 -05:00
parent 90c579d734
commit 2aa5719166
4 changed files with 34 additions and 56 deletions

View File

@@ -95,6 +95,7 @@ impl TextBuilderOutput for TextEditOutput {
let mut text = TextEdit { let mut text = TextEdit {
view: TextView::new(buf, builder.attrs, builder.hint.get(ui)), view: TextView::new(buf, builder.attrs, builder.hint.get(ui)),
selection: Default::default(), selection: Default::default(),
history: Default::default(),
}; };
let font_system = &mut ui.data.text.font_system; let font_system = &mut ui.data.text.font_system;
text.buf text.buf

View File

@@ -11,6 +11,7 @@ use winit::{
pub struct TextEdit { pub struct TextEdit {
pub(super) view: TextView, pub(super) view: TextView,
pub(super) selection: TextSelection, pub(super) selection: TextSelection,
pub(super) history: Vec<(String, TextSelection)>,
} }
impl TextEdit { impl TextEdit {
@@ -180,6 +181,13 @@ impl<'a> TextEditCtx<'a> {
text text
} }
pub fn set(&mut self, text: &str) {
self.text
.buf
.set_text(self.font_system, text, &Attrs::new(), SHAPING, None);
self.text.selection.clear();
}
pub fn motion(&mut self, motion: Motion) { pub fn motion(&mut self, motion: Motion) {
if let TextSelection::Pos(cursor) = self.text.selection if let TextSelection::Pos(cursor) = self.text.selection
&& let Some(cursor) = self.buf_motion(cursor, motion) && let Some(cursor) = self.buf_motion(cursor, motion)
@@ -372,6 +380,24 @@ impl<'a> TextEditCtx<'a> {
} }
pub fn apply_event(&mut self, event: &KeyEvent, modifiers: &Modifiers) -> TextInputResult { pub fn apply_event(&mut self, event: &KeyEvent, modifiers: &Modifiers) -> TextInputResult {
let old = (self.text.content(), self.text.selection);
let mut undo = false;
let res = self.apply_event_inner(event, modifiers, &mut undo);
if undo && let Some((old, selection)) = self.text.history.pop() {
self.set(&old);
self.text.selection = selection;
} else if self.text.content() != old.0 {
self.text.history.push(old);
}
res
}
fn apply_event_inner(
&mut self,
event: &KeyEvent,
modifiers: &Modifiers,
undo: &mut bool,
) -> TextInputResult {
match &event.logical_key { match &event.logical_key {
Key::Named(named) => match named { Key::Named(named) => match named {
NamedKey::Backspace => self.backspace(modifiers.control), NamedKey::Backspace => self.backspace(modifiers.control),
@@ -429,6 +455,9 @@ impl<'a> TextEditCtx<'a> {
}; };
} }
} }
"z" => {
*undo = true;
}
_ => self.insert(text), _ => self.insert(text),
} }
} else { } else {
@@ -463,7 +492,7 @@ pub enum TextInputResult {
Paste, Paste,
} }
#[derive(Debug, Default)] #[derive(Debug, Default, Clone, Copy)]
pub enum TextSelection { pub enum TextSelection {
#[default] #[default]
None, None,

View File

@@ -9,7 +9,7 @@ use std::{
}; };
use crate::{ use crate::{
layout::{FnTag, Ui, Widget, WidgetLike, WidgetTag}, layout::{Ui, WidgetLike},
util::{Id, RefCounter}, util::{Id, RefCounter},
}; };
@@ -141,49 +141,6 @@ pub struct IdFnTag;
pub trait WidgetIdFn<W>: FnOnce(&mut Ui) -> WidgetId<W> {} pub trait WidgetIdFn<W>: FnOnce(&mut Ui) -> WidgetId<W> {}
impl<W, F: FnOnce(&mut Ui) -> WidgetId<W>> WidgetIdFn<W> for F {} impl<W, F: FnOnce(&mut Ui) -> WidgetId<W>> WidgetIdFn<W> for F {}
/// TODO: does this ever make sense to use? it allows for invalid ids
pub trait Idable<Tag> {
type Widget: Widget;
fn set(self, ui: &mut Ui, id: &WidgetId<Self::Widget>);
fn id(self, id: &WidgetId<Self::Widget>) -> impl WidgetIdFn<Self::Widget>
where
Self: Sized,
{
let id = id.clone();
move |ui| {
self.set(ui, &id);
id
}
}
fn id_static(self, id: StaticWidgetId<Self::Widget>) -> impl WidgetIdFn<Self::Widget>
where
Self: Sized,
{
move |ui| {
let id = id.id(&ui.send);
self.set(ui, &id);
id
}
}
}
impl<W: Widget> Idable<WidgetTag> for W {
type Widget = W;
fn set(self, ui: &mut Ui, id: &WidgetId<Self::Widget>) {
ui.set(id, self);
}
}
impl<F: FnOnce(&mut Ui) -> W, W: Widget> Idable<FnTag> for F {
type Widget = W;
fn set(self, ui: &mut Ui, id: &WidgetId<Self::Widget>) {
let w = self(ui);
ui.set(id, w);
}
}
impl<W: 'static> WidgetLike<IdTag> for WidgetId<W> { impl<W: 'static> WidgetLike<IdTag> for WidgetId<W> {
type Widget = W; type Widget = W;
fn add(self, _: &mut Ui) -> WidgetId<W> { fn add(self, _: &mut Ui) -> WidgetId<W> {

View File

@@ -52,15 +52,11 @@ impl Ui {
} }
pub fn push<W: Widget>(&mut self, w: W) -> WidgetId<W> { pub fn push<W: Widget>(&mut self, w: W) -> WidgetId<W> {
let id = self.id(); let id = self.new_id();
self.data.widgets.insert(id.id, w); self.data.widgets.insert(id.id, w);
id id
} }
pub fn set<W: Widget>(&mut self, id: &WidgetId<W>, w: W) {
self.data.widgets.insert(id.id, w);
}
pub fn set_root<Tag>(&mut self, w: impl WidgetLike<Tag>) { pub fn set_root<Tag>(&mut self, w: impl WidgetLike<Tag>) {
self.root = Some(w.add(self).any()); self.root = Some(w.add(self).any());
self.full_redraw = true; self.full_redraw = true;
@@ -78,7 +74,7 @@ impl Ui {
self.data.widgets.get_mut(id) self.data.widgets.get_mut(id)
} }
pub fn id<W: Widget>(&mut self) -> WidgetId<W> { fn new_id<W: Widget>(&mut self) -> WidgetId<W> {
WidgetId::new( WidgetId::new(
self.data.widgets.reserve(), self.data.widgets.reserve(),
TypeId::of::<W>(), TypeId::of::<W>(),
@@ -87,11 +83,6 @@ impl Ui {
) )
} }
pub fn id_static<W: Widget>(&mut self) -> StaticWidgetId<W> {
let id = self.id();
id.into_static()
}
pub fn add_texture(&mut self, image: DynamicImage) -> TextureHandle { pub fn add_texture(&mut self, image: DynamicImage) -> TextureHandle {
self.data.textures.add(image) self.data.textures.add(image)
} }