Skip to main content

egui/widgets/text_edit/
state.rs

1use std::sync::Arc;
2
3use epaint::text::cursor::CCursor;
4
5use crate::mutex::Mutex;
6
7use crate::{
8    Context, Id, Vec2,
9    text_selection::{CCursorRange, TextCursorState},
10};
11
12pub type TextEditUndoer = crate::util::undoer::Undoer<(CCursorRange, String)>;
13
14/// The text edit state stored between frames.
15///
16/// Attention: You also need to `store` the updated state.
17/// ```
18/// # egui::__run_test_ui(|ui| {
19/// # let mut text = String::new();
20/// use egui::text::{CCursor, CCursorRange};
21///
22/// let mut output = egui::TextEdit::singleline(&mut text).show(ui);
23///
24/// // Create a new selection range
25/// let min = CCursor::new(0);
26/// let max = CCursor::new(0);
27/// let new_range = CCursorRange::two(min, max);
28///
29/// // Update the state
30/// output.state.cursor.set_char_range(Some(new_range));
31/// // Store the updated state
32/// output.state.store(ui.ctx(), output.response.id);
33/// # });
34/// ```
35#[derive(Clone, Default)]
36#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
37#[cfg_attr(feature = "serde", serde(default))]
38pub struct TextEditState {
39    /// Controls the text selection.
40    pub cursor: TextCursorState,
41
42    /// The purpose of the cursor.
43    #[cfg_attr(feature = "serde", serde(skip))]
44    pub(crate) cursor_purpose: TextEditCursorPurpose,
45
46    /// Wrapped in Arc for cheaper clones.
47    #[cfg_attr(feature = "serde", serde(skip))]
48    pub(crate) undoer: Arc<Mutex<TextEditUndoer>>,
49
50    // Text offset within the widget area.
51    // Used for sensing and singleline text clipping.
52    #[cfg_attr(feature = "serde", serde(skip))]
53    pub(crate) text_offset: Vec2,
54
55    /// When did the user last press a key or click on the `TextEdit`.
56    /// Used to pause the cursor animation when typing.
57    #[cfg_attr(feature = "serde", serde(skip))]
58    pub(crate) last_interaction_time: f64,
59}
60
61impl TextEditState {
62    pub fn load(ctx: &Context, id: Id) -> Option<Self> {
63        ctx.data_mut(|d| d.get_persisted(id))
64    }
65
66    pub fn store(self, ctx: &Context, id: Id) {
67        ctx.data_mut(|d| d.insert_persisted(id, self));
68    }
69
70    pub fn undoer(&self) -> TextEditUndoer {
71        self.undoer.lock().clone()
72    }
73
74    #[expect(clippy::needless_pass_by_ref_mut)] // Intentionally hide interiority of mutability
75    pub fn set_undoer(&mut self, undoer: TextEditUndoer) {
76        *self.undoer.lock() = undoer;
77    }
78
79    pub fn clear_undoer(&mut self) {
80        self.set_undoer(TextEditUndoer::default());
81    }
82}
83
84#[derive(Clone, Default)]
85pub(crate) enum TextEditCursorPurpose {
86    /// The cursor is used for text selection.
87    #[default]
88    Selection,
89
90    /// The cursor is used for IME composition. Its direction is irrelevant in
91    /// this case.
92    ImeComposition {
93        /// An optional cursor/segment within the composing text itself,
94        /// relative to the start of the composing region. Its direction is
95        /// irrelevant.
96        ///
97        /// When `None`, no active range is displayed.
98        active_range: Option<core::ops::Range<CCursor>>,
99    },
100}
101
102impl TextEditCursorPurpose {
103    pub(crate) fn is_selection(&self) -> bool {
104        matches!(self, Self::Selection)
105    }
106
107    pub(crate) fn is_ime_composition(&self) -> bool {
108        matches!(self, Self::ImeComposition { .. })
109    }
110}