Skip to main content

kas_widgets/edit/
edit_field.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License in the LICENSE-APACHE file or at:
4//     https://www.apache.org/licenses/LICENSE-2.0
5
6//! The [`EditField`] and [`EditBox`] widgets, plus supporting items
7
8use super::*;
9use kas::event::components::TextInputAction;
10use kas::event::{CursorIcon, ElementState, FocusSource, PhysicalKey};
11use kas::event::{Ime, ImePurpose};
12use kas::messages::{ReplaceSelectedText, SetValueText};
13use kas::prelude::*;
14use kas::text::{Effect, EffectFlags, NotReady};
15use kas::theme::TextClass;
16use std::fmt::{Debug, Display};
17use std::str::FromStr;
18
19#[impl_self]
20mod EditField {
21    /// A text-edit field (single- or multi-line)
22    ///
23    /// The [`EditBox`] widget should be preferred in most cases; this widget
24    /// is a component of `EditBox` and has some special behaviour.
25    ///
26    /// By default, the editor supports a single-line only;
27    /// [`Self::with_multi_line`] and [`Self::with_class`] can be used to change this.
28    ///
29    /// ### Event handling
30    ///
31    /// This widget attempts to handle all standard text-editor input and scroll
32    /// events.
33    ///
34    /// Key events for moving the edit cursor (e.g. arrow keys) are consumed
35    /// only if the edit cursor is moved while key events for adjusting or using
36    /// the selection (e.g. `Command::Copy` and `Command::Deselect`)
37    /// are consumed only when a selection exists. In contrast, key events for
38    /// inserting or deleting text are always consumed.
39    ///
40    /// [`Command::Enter`] inserts a line break in multi-line mode, but in
41    /// single-line mode or if the <kbd>Shift</kbd> key is held it is treated
42    /// the same as [`Command::Activate`].
43    ///
44    /// ### Performance and limitations
45    ///
46    /// Text representation is via a single [`String`]. Edit operations are
47    /// `O(n)` where `n` is the length of text (with text layout algorithms
48    /// having greater cost than copying bytes in the backing [`String`]).
49    /// This isn't necessarily *slow*; when run with optimizations the type can
50    /// handle type-setting around 20kB of UTF-8 in under 10ms (with significant
51    /// scope for optimization, given that currently layout is re-run from
52    /// scratch on each key stroke). Regardless, this approach is not designed
53    /// to scale to handle large documents via a single `EditField` widget.
54    ///
55    /// ### Messages
56    ///
57    /// [`SetValueText`] may be used to replace the entire text and
58    /// [`ReplaceSelectedText`] may be used to replace selected text when this
59    /// widget is [editable](Editor::is_editable)]. This triggers the action
60    /// handlers [`EditGuard::edit`] followed by [`EditGuard::activate`].
61    ///
62    /// ### Special behaviour
63    ///
64    /// This is a [`Viewport`] widget.
65    #[autoimpl(Debug where G: trait)]
66    #[autoimpl(Deref, DerefMut using self.editor)]
67    #[widget]
68    pub struct EditField<G: EditGuard = DefaultGuard<()>> {
69        core: widget_core!(),
70        width: (f32, f32),
71        lines: (f32, f32),
72        editor: Editor,
73        /// The associated [`EditGuard`] implementation
74        pub guard: G,
75    }
76
77    impl Layout for Self {
78        #[inline]
79        fn rect(&self) -> Rect {
80            self.text.rect()
81        }
82
83        fn size_rules(&mut self, cx: &mut SizeCx, axis: AxisInfo) -> SizeRules {
84            let (min, mut ideal): (i32, i32);
85            if axis.is_horizontal() {
86                let dpem = cx.dpem(self.text.class());
87                min = (self.width.0 * dpem).cast_ceil();
88                ideal = (self.width.1 * dpem).cast_ceil();
89            } else if let Some(width) = axis.other() {
90                // Use the height of the first line as a reference
91                let height = self
92                    .text
93                    .measure_height(width.cast(), std::num::NonZero::new(1));
94                min = (self.lines.0 * height).cast_ceil();
95                ideal = (self.lines.1 * height).cast_ceil();
96            } else {
97                unreachable!()
98            };
99
100            let rules = self.text.size_rules(cx, axis);
101            ideal = ideal.max(rules.ideal_size());
102
103            let stretch = if axis.is_horizontal() || self.multi_line() {
104                Stretch::High
105            } else {
106                Stretch::None
107            };
108            SizeRules::new(min, ideal, stretch).with_margins(cx.text_margins().extract(axis))
109        }
110
111        fn set_rect(&mut self, cx: &mut SizeCx, rect: Rect, mut hints: AlignHints) {
112            self.editor.pos = rect.pos;
113            hints.vert = Some(if self.multi_line() {
114                Align::Default
115            } else {
116                Align::Center
117            });
118            self.text.set_rect(cx, rect, hints);
119            self.text.ensure_no_left_overhang();
120            if self.current.is_ime_enabled() {
121                self.set_ime_cursor_area(cx);
122            }
123        }
124    }
125
126    impl Viewport for Self {
127        #[inline]
128        fn content_size(&self) -> Size {
129            if let Ok((tl, br)) = self.text.bounding_box() {
130                (br - tl).cast_ceil()
131            } else {
132                Size::ZERO
133            }
134        }
135
136        fn draw_with_offset(&self, mut draw: DrawCx, rect: Rect, offset: Offset) {
137            let pos = self.rect().pos - offset;
138
139            if let CurrentAction::ImePreedit { edit_range } = self.current.clone() {
140                // TODO: combine underline with selection highlight
141                let effects = [
142                    Effect {
143                        start: 0,
144                        e: 0,
145                        flags: Default::default(),
146                    },
147                    Effect {
148                        start: edit_range.start,
149                        e: 0,
150                        flags: EffectFlags::UNDERLINE,
151                    },
152                    Effect {
153                        start: edit_range.end,
154                        e: 0,
155                        flags: Default::default(),
156                    },
157                ];
158                draw.text_with_effects(pos, rect, &self.text, &[], &effects);
159            } else {
160                draw.text_with_selection(pos, rect, &self.text, self.selection.range());
161            }
162
163            if self.editable && draw.ev_state().has_input_focus(self.id_ref()) == Some(true) {
164                draw.text_cursor(pos, rect, &self.text, self.selection.edit_index());
165            }
166        }
167    }
168
169    impl Tile for Self {
170        fn navigable(&self) -> bool {
171            true
172        }
173
174        #[inline]
175        fn tooltip(&self) -> Option<&str> {
176            self.editor.tooltip()
177        }
178
179        fn role(&self, _: &mut dyn RoleCx) -> Role<'_> {
180            Role::TextInput {
181                text: self.text.as_str(),
182                multi_line: self.multi_line(),
183                cursor: self.cursor_range(),
184            }
185        }
186    }
187
188    impl Events for Self {
189        const REDRAW_ON_MOUSE_OVER: bool = true;
190
191        type Data = G::Data;
192
193        fn probe(&self, _: Coord) -> Id {
194            self.id()
195        }
196
197        #[inline]
198        fn mouse_over_icon(&self) -> Option<CursorIcon> {
199            Some(CursorIcon::Text)
200        }
201
202        fn configure(&mut self, cx: &mut ConfigCx) {
203            self.editor.id = self.id();
204            self.text.configure(&mut cx.size_cx());
205            self.guard.configure(&mut self.editor, cx);
206        }
207
208        fn update(&mut self, cx: &mut ConfigCx, data: &G::Data) {
209            let size = self.content_size();
210            if !self.has_input_focus() {
211                self.guard.update(&mut self.editor, cx, data);
212            }
213            if size != self.content_size() {
214                cx.resize();
215            }
216        }
217
218        fn handle_event(&mut self, cx: &mut EventCx, data: &G::Data, event: Event) -> IsUsed {
219            match event {
220                Event::NavFocus(source) if source == FocusSource::Key => {
221                    if !self.input_handler.is_selecting() {
222                        self.request_key_focus(cx, source);
223                    }
224                    Used
225                }
226                Event::NavFocus(_) => Used,
227                Event::LostNavFocus => Used,
228                Event::SelFocus(source) => {
229                    // NOTE: sel focus implies key focus since we only request
230                    // the latter. We must set before calling self.set_primary.
231                    self.has_key_focus = true;
232                    if source == FocusSource::Pointer {
233                        self.set_primary(cx);
234                    }
235
236                    Used
237                }
238                Event::KeyFocus => {
239                    self.has_key_focus = true;
240                    self.set_view_offset_from_cursor(cx);
241                    if !self.current.is_ime_enabled() {
242                        self.guard.focus_gained(&mut self.editor, cx, data);
243                    }
244
245                    if self.current.is_none() {
246                        let hint = Default::default();
247                        let purpose = ImePurpose::Normal;
248                        let surrounding_text = self.ime_surrounding_text();
249                        cx.replace_ime_focus(self.id.clone(), hint, purpose, surrounding_text);
250                    }
251                    Used
252                }
253                Event::LostKeyFocus => {
254                    self.has_key_focus = false;
255                    cx.redraw();
256                    if !self.current.is_ime_enabled() {
257                        self.guard.focus_lost(&mut self.editor, cx, data);
258                    }
259                    Used
260                }
261                Event::LostSelFocus => {
262                    // NOTE: we can assume that we will receive Ime::Disabled if IME is active
263                    if !self.selection.is_empty() {
264                        self.save_undo_state(None);
265                        self.selection.set_empty();
266                    }
267                    self.input_handler.stop_selecting();
268                    cx.redraw();
269                    Used
270                }
271                Event::Command(cmd, code) => match self.control_key(cx, data, cmd, code) {
272                    Ok(r) => r,
273                    Err(NotReady) => Used,
274                },
275                Event::Key(event, false) if event.state == ElementState::Pressed => {
276                    if let Some(text) = &event.text {
277                        self.save_undo_state(Some(EditOp::KeyInput));
278                        let used = self.received_text(cx, text);
279                        self.call_guard_edit(cx, data);
280                        used
281                    } else {
282                        let opt_cmd = cx
283                            .config()
284                            .shortcuts()
285                            .try_match(cx.modifiers(), &event.key_without_modifiers);
286                        if let Some(cmd) = opt_cmd {
287                            match self.control_key(cx, data, cmd, Some(event.physical_key)) {
288                                Ok(r) => r,
289                                Err(NotReady) => Used,
290                            }
291                        } else {
292                            Unused
293                        }
294                    }
295                }
296                Event::Ime(ime) => match ime {
297                    Ime::Enabled => {
298                        if !self.has_key_focus {
299                            self.guard.focus_gained(&mut self.editor, cx, data);
300                        }
301                        match self.current {
302                            CurrentAction::None => {
303                                self.current = CurrentAction::ImeStart;
304                                self.set_ime_cursor_area(cx);
305                            }
306                            CurrentAction::ImeStart | CurrentAction::ImePreedit { .. } => {
307                                // already enabled
308                            }
309                            CurrentAction::Selection => {
310                                // Do not interrupt selection
311                                cx.cancel_ime_focus(self.id_ref());
312                            }
313                        }
314                        Used
315                    }
316                    Ime::Disabled => {
317                        self.clear_ime();
318                        if !self.has_key_focus {
319                            self.guard.focus_lost(&mut self.editor, cx, data);
320                        }
321                        Used
322                    }
323                    Ime::Preedit { text, cursor } => {
324                        self.save_undo_state(None);
325                        let mut edit_range = match self.current.clone() {
326                            CurrentAction::ImeStart if cursor.is_some() => self.selection.range(),
327                            CurrentAction::ImeStart => return Used,
328                            CurrentAction::ImePreedit { edit_range } => edit_range.cast(),
329                            _ => return Used,
330                        };
331
332                        self.text.replace_range(edit_range.clone(), text);
333                        edit_range.end = edit_range.start + text.len();
334                        if let Some((start, end)) = cursor {
335                            self.selection.set_sel_index_only(edit_range.start + start);
336                            self.selection.set_edit_index(edit_range.start + end);
337                        } else {
338                            self.selection.set_cursor(edit_range.start + text.len());
339                        }
340
341                        self.current = CurrentAction::ImePreedit {
342                            edit_range: edit_range.cast(),
343                        };
344                        self.edit_x_coord = None;
345                        self.prepare_and_scroll(cx, false);
346                        Used
347                    }
348                    Ime::Commit { text } => {
349                        self.save_undo_state(Some(EditOp::Ime));
350                        let edit_range = match self.current.clone() {
351                            CurrentAction::ImeStart => self.selection.range(),
352                            CurrentAction::ImePreedit { edit_range } => edit_range.cast(),
353                            _ => return Used,
354                        };
355
356                        self.text.replace_range(edit_range.clone(), text);
357                        self.selection.set_cursor(edit_range.start + text.len());
358
359                        self.current = CurrentAction::ImePreedit {
360                            edit_range: self.selection.range().cast(),
361                        };
362                        self.edit_x_coord = None;
363                        self.prepare_and_scroll(cx, false);
364                        self.call_guard_edit(cx, data);
365                        Used
366                    }
367                    Ime::DeleteSurrounding {
368                        before_bytes,
369                        after_bytes,
370                    } => {
371                        self.save_undo_state(None);
372                        let edit_range = match self.current.clone() {
373                            CurrentAction::ImeStart => self.selection.range(),
374                            CurrentAction::ImePreedit { edit_range } => edit_range.cast(),
375                            _ => return Used,
376                        };
377
378                        if before_bytes > 0 {
379                            let end = edit_range.start;
380                            let start = end - before_bytes;
381                            if self.as_str().is_char_boundary(start) {
382                                self.text.replace_range(start..end, "");
383                                self.selection.delete_range(start..end);
384                            } else {
385                                log::warn!("buggy IME tried to delete range not at char boundary");
386                            }
387                        }
388
389                        if after_bytes > 0 {
390                            let start = edit_range.end;
391                            let end = start + after_bytes;
392                            if self.as_str().is_char_boundary(end) {
393                                self.text.replace_range(start..end, "");
394                            } else {
395                                log::warn!("buggy IME tried to delete range not at char boundary");
396                            }
397                        }
398
399                        if let Some(text) = self.ime_surrounding_text() {
400                            cx.update_ime_surrounding_text(self.id_ref(), text);
401                        }
402
403                        Used
404                    }
405                },
406                Event::PressStart(press) if press.is_tertiary() => {
407                    press.grab_click(self.id()).complete(cx)
408                }
409                Event::PressEnd { press, .. } if press.is_tertiary() => {
410                    self.set_cursor_from_coord(cx, press.coord);
411                    self.cancel_selection_and_ime(cx);
412
413                    if let Some(content) = cx.get_primary() {
414                        self.save_undo_state(Some(EditOp::Clipboard));
415
416                        let index = self.selection.edit_index();
417                        let range = self.trim_paste(&content);
418
419                        self.text
420                            .replace_range(index..index, &content[range.clone()]);
421                        self.selection.set_cursor(index + range.len());
422                        self.edit_x_coord = None;
423                        self.prepare_and_scroll(cx, false);
424
425                        self.call_guard_edit(cx, data);
426                    }
427
428                    self.request_key_focus(cx, FocusSource::Pointer);
429                    Used
430                }
431                event => match self.editor.input_handler.handle(cx, self.core.id(), event) {
432                    TextInputAction::Used => Used,
433                    TextInputAction::Unused => Unused,
434                    TextInputAction::PressStart {
435                        coord,
436                        clear,
437                        repeats,
438                    } => {
439                        if self.current.is_ime_enabled() {
440                            self.clear_ime();
441                            cx.cancel_ime_focus(self.id_ref());
442                        }
443                        self.save_undo_state(Some(EditOp::Cursor));
444                        self.current = CurrentAction::Selection;
445
446                        self.set_cursor_from_coord(cx, coord);
447                        self.selection.set_anchor(clear);
448                        if repeats > 1 {
449                            self.editor
450                                .selection
451                                .expand(&self.editor.text, repeats >= 3);
452                        }
453
454                        self.request_key_focus(cx, FocusSource::Pointer);
455                        Used
456                    }
457                    TextInputAction::PressMove { coord, repeats } => {
458                        if self.current == CurrentAction::Selection {
459                            self.set_cursor_from_coord(cx, coord);
460                            if repeats > 1 {
461                                self.editor
462                                    .selection
463                                    .expand(&self.editor.text, repeats >= 3);
464                            }
465                        }
466
467                        Used
468                    }
469                    TextInputAction::PressEnd { coord } => {
470                        if self.current.is_ime_enabled() {
471                            self.clear_ime();
472                            cx.cancel_ime_focus(self.id_ref());
473                        }
474                        self.save_undo_state(Some(EditOp::Cursor));
475                        if self.current == CurrentAction::Selection {
476                            self.set_primary(cx);
477                        } else {
478                            self.set_cursor_from_coord(cx, coord);
479                            self.selection.set_empty();
480                        }
481                        self.current = CurrentAction::None;
482
483                        self.request_key_focus(cx, FocusSource::Pointer);
484                        Used
485                    }
486                },
487            }
488        }
489
490        fn handle_messages(&mut self, cx: &mut EventCx, data: &G::Data) {
491            if !self.editable {
492                return;
493            }
494
495            if let Some(SetValueText(string)) = cx.try_pop() {
496                self.pre_commit();
497                self.set_string(cx, string);
498                self.call_guard_edit(cx, data);
499            } else if let Some(ReplaceSelectedText(text)) = cx.try_pop() {
500                self.pre_commit();
501                self.replace_selected_text(cx, &text);
502                self.call_guard_edit(cx, data);
503            }
504        }
505    }
506
507    impl Default for Self
508    where
509        G: Default,
510    {
511        #[inline]
512        fn default() -> Self {
513            EditField::new(G::default())
514        }
515    }
516
517    impl Self {
518        /// Construct an `EditBox` with an [`EditGuard`]
519        #[inline]
520        pub fn new(guard: G) -> EditField<G> {
521            EditField {
522                core: Default::default(),
523                width: (8.0, 16.0),
524                lines: (1.0, 1.0),
525                editor: Editor::new(),
526                guard,
527            }
528        }
529
530        /// Call the [`EditGuard`]'s `activate` method
531        #[inline]
532        pub fn call_guard_activate(&mut self, cx: &mut EventCx, data: &G::Data) {
533            self.guard.activate(&mut self.editor, cx, data);
534        }
535
536        /// Call the [`EditGuard`]'s `edit` method
537        ///
538        /// This call also clears the error state (see [`Editor::set_error`]).
539        #[inline]
540        pub fn call_guard_edit(&mut self, cx: &mut EventCx, data: &G::Data) {
541            self.clear_error();
542            self.guard.edit(&mut self.editor, cx, data);
543        }
544    }
545}
546
547impl<A: 'static> EditField<DefaultGuard<A>> {
548    /// Construct an `EditField` with the given inital `text` (no event handling)
549    #[inline]
550    pub fn text<S: ToString>(text: S) -> Self {
551        EditField {
552            editor: Editor::from(text),
553            ..Default::default()
554        }
555    }
556
557    /// Construct a read-only `EditField` displaying some `String` value
558    #[inline]
559    pub fn string(value_fn: impl Fn(&A) -> String + 'static) -> EditField<StringGuard<A>> {
560        EditField::new(StringGuard::new(value_fn)).with_editable(false)
561    }
562
563    /// Construct an `EditField` for a parsable value (e.g. a number)
564    ///
565    /// On update, `value_fn` is used to extract a value from input data
566    /// which is then formatted as a string via [`Display`].
567    /// If, however, the input field has focus, the update is ignored.
568    ///
569    /// On every edit, the guard attempts to parse the field's input as type
570    /// `T` via [`FromStr`], caching the result and setting the error state.
571    ///
572    /// On field activation and focus loss when a `T` value is cached (see
573    /// previous paragraph), `on_afl` is used to construct a message to be
574    /// emitted via [`EventCx::push`]. The cached value is then cleared to
575    /// avoid sending duplicate messages.
576    #[inline]
577    pub fn parser<T: Debug + Display + FromStr, M: Debug + 'static>(
578        value_fn: impl Fn(&A) -> T + 'static,
579        msg_fn: impl Fn(T) -> M + 'static,
580    ) -> EditField<ParseGuard<A, T>> {
581        EditField::new(ParseGuard::new(value_fn, msg_fn))
582    }
583
584    /// Construct an `EditField` for a parsable value (e.g. a number)
585    ///
586    /// On update, `value_fn` is used to extract a value from input data
587    /// which is then formatted as a string via [`Display`].
588    /// If, however, the input field has focus, the update is ignored.
589    ///
590    /// On every edit, the guard attempts to parse the field's input as type
591    /// `T` via [`FromStr`]. On success, the result is converted to a
592    /// message via `on_afl` then emitted via [`EventCx::push`].
593    pub fn instant_parser<T: Debug + Display + FromStr, M: Debug + 'static>(
594        value_fn: impl Fn(&A) -> T + 'static,
595        msg_fn: impl Fn(T) -> M + 'static,
596    ) -> EditField<InstantParseGuard<A, T>> {
597        EditField::new(InstantParseGuard::new(value_fn, msg_fn))
598    }
599}
600
601impl<A: 'static> EditField<StringGuard<A>> {
602    /// Assign a message function for a `String` value
603    ///
604    /// The `msg_fn` is called when the field is activated (<kbd>Enter</kbd>)
605    /// and when it loses focus after content is changed.
606    ///
607    /// This method sets self as editable (see [`Self::with_editable`]).
608    #[must_use]
609    pub fn with_msg<M>(mut self, msg_fn: impl Fn(&str) -> M + 'static) -> Self
610    where
611        M: Debug + 'static,
612    {
613        self.guard = self.guard.with_msg(msg_fn);
614        self.editable = true;
615        self
616    }
617}
618
619impl<G: EditGuard> EditField<G> {
620    /// Set the initial text (inline)
621    ///
622    /// This method should only be used on a new `EditField`.
623    #[inline]
624    #[must_use]
625    pub fn with_text(mut self, text: impl ToString) -> Self {
626        debug_assert!(self.current == CurrentAction::None && !self.input_handler.is_selecting());
627        let text = text.to_string();
628        let len = text.len();
629        self.text.set_string(text);
630        self.selection.set_cursor(len);
631        self
632    }
633
634    /// Set whether this `EditField` is editable (inline)
635    #[inline]
636    #[must_use]
637    pub fn with_editable(mut self, editable: bool) -> Self {
638        self.editable = editable;
639        self
640    }
641
642    /// Set whether this `EditField` uses multi-line mode
643    ///
644    /// This method does two things:
645    ///
646    /// -   Changes the text class (see [`Self::with_class`])
647    /// -   Changes the vertical height allocation (see [`Self::with_lines`])
648    #[inline]
649    #[must_use]
650    pub fn with_multi_line(mut self, multi_line: bool) -> Self {
651        self.text.set_wrap(multi_line);
652        self.lines = match multi_line {
653            false => (1.0, 1.0),
654            true => (4.0, 7.0),
655        };
656        self
657    }
658
659    /// Set the text class used
660    #[inline]
661    #[must_use]
662    pub fn with_class(mut self, class: TextClass) -> Self {
663        self.text.set_class(class);
664        self
665    }
666
667    /// Adjust the height allocation
668    #[inline]
669    pub fn set_lines(&mut self, min_lines: f32, ideal_lines: f32) {
670        self.lines = (min_lines, ideal_lines);
671    }
672
673    /// Adjust the height allocation (inline)
674    #[inline]
675    #[must_use]
676    pub fn with_lines(mut self, min_lines: f32, ideal_lines: f32) -> Self {
677        self.set_lines(min_lines, ideal_lines);
678        self
679    }
680
681    /// Adjust the width allocation
682    #[inline]
683    pub fn set_width_em(&mut self, min_em: f32, ideal_em: f32) {
684        self.width = (min_em, ideal_em);
685    }
686
687    /// Adjust the width allocation (inline)
688    #[inline]
689    #[must_use]
690    pub fn with_width_em(mut self, min_em: f32, ideal_em: f32) -> Self {
691        self.set_width_em(min_em, ideal_em);
692        self
693    }
694
695    fn control_key(
696        &mut self,
697        cx: &mut EventCx,
698        data: &G::Data,
699        cmd: Command,
700        code: Option<PhysicalKey>,
701    ) -> Result<IsUsed, NotReady> {
702        let action = self.editor.cmd_action(cx, cmd)?;
703        if matches!(action, CmdAction::Unused) {
704            return Ok(Unused);
705        }
706
707        self.prepare_and_scroll(cx, true);
708
709        Ok(match action {
710            CmdAction::Unused => Unused,
711            CmdAction::Used | CmdAction::Cursor => Used,
712            CmdAction::Activate => {
713                cx.depress_with_key(&self, code);
714                self.guard.activate(&mut self.editor, cx, data)
715            }
716            CmdAction::Edit => {
717                self.call_guard_edit(cx, data);
718                Used
719            }
720        })
721    }
722}