Skip to main content

kas_widgets/edit/
edit_box.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 crate::{ScrollBar, ScrollBarMsg};
10use kas::event::Scroll;
11use kas::event::components::ScrollComponent;
12use kas::messages::{ReplaceSelectedText, SetValueText};
13use kas::prelude::*;
14use kas::theme::{Background, FrameStyle, TextClass};
15use std::fmt::{Debug, Display};
16use std::ops::{Deref, DerefMut};
17use std::str::FromStr;
18
19#[impl_self]
20mod EditBox {
21    /// A text-edit box
22    ///
23    /// A single- or multi-line editor for unformatted text.
24    /// See also notes on [`EditField`].
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    /// ### Messages
30    ///
31    /// [`SetValueText`] may be used to replace the entire text and
32    /// [`ReplaceSelectedText`] may be used to replace selected text when this
33    /// widget is [editable](Editor::is_editable). This triggers the action
34    /// handlers [`EditGuard::edit`] followed by [`EditGuard::activate`].
35    ///
36    /// [`kas::messages::SetScrollOffset`] may be used to set the scroll offset.
37    #[autoimpl(Default, Debug where G: trait)]
38    #[widget]
39    pub struct EditBox<G: EditGuard = DefaultGuard<()>> {
40        core: widget_core!(),
41        scroll: ScrollComponent,
42        // NOTE: inner is a Viewport which doesn't use update methods, therefore we don't call them.
43        #[widget]
44        inner: EditField<G>,
45        #[widget(&())]
46        vert_bar: ScrollBar<kas::dir::Down>,
47        frame_offset: Offset,
48        frame_size: Size,
49        frame_offset_ex_margin: Offset,
50        inner_margin: i32,
51        clip_rect: Rect,
52    }
53
54    impl Layout for Self {
55        fn size_rules(&mut self, cx: &mut SizeCx, mut axis: AxisInfo) -> SizeRules {
56            let size = self.frame_size.extract(axis.flipped());
57            axis.map_other(|x| x - size);
58
59            let mut rules = self.inner.size_rules(cx, axis);
60            let bar_rules = self.vert_bar.size_rules(cx, axis);
61            if axis.is_horizontal() && self.multi_line() {
62                self.inner_margin = rules.margins_i32().1.max(bar_rules.margins_i32().0);
63                rules.append(bar_rules);
64            }
65
66            let frame_rules = cx.frame(FrameStyle::EditBox, axis);
67            self.frame_offset_ex_margin
68                .set_component(axis, frame_rules.size());
69            let (rules, offset, size) = frame_rules.surround(rules);
70            self.frame_offset.set_component(axis, offset);
71            self.frame_size.set_component(axis, size);
72            rules
73        }
74
75        fn set_rect(&mut self, cx: &mut SizeCx, outer_rect: Rect, hints: AlignHints) {
76            self.core.set_rect(outer_rect);
77            let mut rect = outer_rect;
78
79            self.clip_rect = Rect {
80                pos: rect.pos + self.frame_offset_ex_margin,
81                size: rect.size - (self.frame_offset_ex_margin * 2).cast(),
82            };
83
84            rect.pos += self.frame_offset;
85            rect.size -= self.frame_size;
86
87            let mut bar_rect = Rect::ZERO;
88            if self.multi_line() {
89                let bar_width = cx.scroll_bar_width();
90                let x1 = rect.pos.0 + rect.size.0;
91                let x0 = x1 - bar_width;
92                bar_rect = Rect::new(Coord(x0, rect.pos.1), Size(bar_width, rect.size.1));
93                rect.size.0 = (rect.size.0 - bar_width - self.inner_margin).max(0);
94            }
95            self.vert_bar.set_rect(cx, bar_rect, AlignHints::NONE);
96
97            self.inner.set_rect(cx, rect, hints);
98            self.update_content_size(cx);
99        }
100
101        fn draw(&self, mut draw: DrawCx) {
102            let mut draw_inner = draw.re();
103            draw_inner.set_id(self.inner.id());
104            let bg = if self.inner.has_error() {
105                Background::Error
106            } else {
107                Background::Default
108            };
109            draw_inner.frame(self.rect(), FrameStyle::EditBox, bg);
110
111            self.inner
112                .draw_with_offset(draw.re(), self.clip_rect, self.scroll.offset());
113
114            if self.scroll.max_offset().1 > 0 {
115                self.vert_bar.draw(draw.re());
116            }
117        }
118    }
119
120    impl Tile for Self {
121        #[inline]
122        fn tooltip(&self) -> Option<&str> {
123            self.deref().tooltip()
124        }
125
126        fn role(&self, _: &mut dyn RoleCx) -> Role<'_> {
127            Role::ScrollRegion {
128                offset: self.scroll.offset(),
129                max_offset: self.scroll.max_offset(),
130            }
131        }
132
133        fn translation(&self, index: usize) -> Offset {
134            if index == widget_index!(self.inner) {
135                self.scroll.offset()
136            } else {
137                Offset::ZERO
138            }
139        }
140    }
141
142    impl Events for Self {
143        type Data = G::Data;
144
145        fn probe(&self, coord: Coord) -> Id {
146            if self.scroll.max_offset().1 > 0 {
147                if let Some(id) = self.vert_bar.try_probe(coord) {
148                    return id;
149                }
150            }
151
152            // If coord is over self but not over self.vert_bar, we assign
153            // the event to self.inner without further question.
154            self.inner.id()
155        }
156
157        fn handle_event(&mut self, cx: &mut EventCx, _: &Self::Data, event: Event) -> IsUsed {
158            let rect = Rect {
159                pos: self.rect().pos + self.frame_offset,
160                size: self.rect().size - self.frame_size,
161            };
162            let used = self.scroll.scroll_by_event(cx, event, self.id(), rect);
163            self.update_content_size(cx);
164            used
165        }
166
167        fn handle_messages(&mut self, cx: &mut EventCx<'_>, data: &G::Data) {
168            let action = if cx.last_child() == Some(widget_index![self.vert_bar])
169                && let Some(ScrollBarMsg(y)) = cx.try_pop()
170            {
171                let offset = Offset(self.scroll.offset().0, y);
172                self.scroll.set_offset(offset)
173            } else if let Some(kas::messages::SetScrollOffset(offset)) = cx.try_pop() {
174                self.scroll.set_offset(offset)
175            } else if self.is_editable()
176                && let Some(SetValueText(string)) = cx.try_pop()
177            {
178                self.pre_commit();
179                self.set_string(cx, string);
180                self.inner.call_guard_edit(cx, data);
181                return;
182            } else if let Some(&ReplaceSelectedText(_)) = cx.try_peek() {
183                self.inner.handle_messages(cx, data);
184                return;
185            } else {
186                return;
187            };
188
189            if let Some(moved) = action {
190                cx.action_moved(moved);
191                self.update_scroll_offset(cx);
192            }
193        }
194
195        fn handle_resize(&mut self, cx: &mut ConfigCx, _: &Self::Data) -> Option<ActionResize> {
196            let size = self.inner.rect().size;
197            let axis = AxisInfo::new(false, Some(size.1));
198            let mut resize = self.inner.size_rules(&mut cx.size_cx(), axis).min_size() > size.0;
199            let axis = AxisInfo::new(true, Some(size.0));
200            resize |= self.inner.size_rules(&mut cx.size_cx(), axis).min_size() > size.1;
201            self.update_content_size(cx);
202            resize.then_some(ActionResize)
203        }
204
205        fn handle_scroll(&mut self, cx: &mut EventCx<'_>, _: &G::Data, scroll: Scroll) {
206            let rect = self.inner.rect();
207            self.scroll.scroll(cx, self.id(), rect, scroll);
208            self.update_scroll_offset(cx);
209        }
210    }
211
212    impl Self {
213        /// Construct an `EditBox` with an [`EditGuard`]
214        #[inline]
215        pub fn new(guard: G) -> Self {
216            EditBox {
217                core: Default::default(),
218                scroll: Default::default(),
219                inner: EditField::new(guard),
220                vert_bar: Default::default(),
221                frame_offset: Default::default(),
222                frame_size: Default::default(),
223                frame_offset_ex_margin: Default::default(),
224                inner_margin: Default::default(),
225                clip_rect: Default::default(),
226            }
227        }
228
229        fn update_content_size(&mut self, cx: &mut EventState) {
230            if !self.core.status.is_sized() {
231                return;
232            }
233            let size = self.inner.rect().size;
234            let _ = self.scroll.set_sizes(size, self.inner.content_size());
235            let max_offset = self.scroll.max_offset().1;
236            self.vert_bar.set_limits(cx, max_offset, size.1);
237            self.update_scroll_offset(cx);
238        }
239
240        fn update_scroll_offset(&mut self, cx: &mut EventState) {
241            self.vert_bar.set_value(cx, self.scroll.offset().1);
242        }
243
244        /// Clear text contents and undo history
245        #[inline]
246        pub fn clear(&mut self, cx: &mut EventState) {
247            self.inner.clear(cx);
248        }
249
250        /// Commit outstanding changes to the undo history
251        ///
252        /// Call this *before* changing the text with `set_str` or `set_string`
253        /// to commit changes to the undo history.
254        #[inline]
255        pub fn pre_commit(&mut self) {
256            self.inner.pre_commit();
257        }
258
259        // Set text contents from a `str`
260        ///
261        /// This does not interact with undo history; see also [`Self::clear`],
262        /// [`Self::pre_commit`].
263        #[inline]
264        pub fn set_str(&mut self, cx: &mut EventState, text: &str) {
265            if self.inner.set_str(cx, text) {
266                self.update_content_size(cx);
267            }
268        }
269
270        /// Set text contents from a `String`
271        ///
272        /// This does not interact with undo history; see also [`Self::clear`],
273        /// [`Self::pre_commit`].
274        ///
275        /// This method does not call action handlers on the [`EditGuard`].
276        #[inline]
277        pub fn set_string(&mut self, cx: &mut EventState, text: String) {
278            if self.inner.set_string(cx, text) {
279                self.update_content_size(cx);
280            }
281        }
282
283        /// Replace selected text
284        ///
285        /// This does not interact with undo history or call action handlers on the
286        /// guard.
287        #[inline]
288        pub fn replace_selected_text(&mut self, cx: &mut EventState, text: &str) {
289            if self.inner.replace_selected_text(cx, text) {
290                self.update_content_size(cx);
291            }
292        }
293
294        /// Access the edit guard
295        #[inline]
296        pub fn guard(&self) -> &G {
297            &self.inner.guard
298        }
299
300        /// Access the edit guard mutably
301        #[inline]
302        pub fn guard_mut(&mut self) -> &mut G {
303            &mut self.inner.guard
304        }
305    }
306}
307
308impl<G: EditGuard> Deref for EditBox<G> {
309    type Target = Editor;
310
311    fn deref(&self) -> &Editor {
312        self.inner.deref()
313    }
314}
315
316impl<G: EditGuard> DerefMut for EditBox<G> {
317    fn deref_mut(&mut self) -> &mut Editor {
318        self.inner.deref_mut()
319    }
320}
321
322impl<A: 'static> EditBox<DefaultGuard<A>> {
323    /// Construct an `EditBox` with the given inital `text` (no event handling)
324    #[inline]
325    pub fn text<S: ToString>(text: S) -> Self {
326        EditBox {
327            inner: EditField::text(text),
328            ..Default::default()
329        }
330    }
331
332    /// Construct a read-only `EditBox` displaying some `String` value
333    #[inline]
334    pub fn string(value_fn: impl Fn(&A) -> String + 'static) -> EditBox<StringGuard<A>> {
335        EditBox::new(StringGuard::new(value_fn)).with_editable(false)
336    }
337
338    /// Construct an `EditBox` for a parsable value (e.g. a number)
339    ///
340    /// On update, `value_fn` is used to extract a value from input data
341    /// which is then formatted as a string via [`Display`].
342    /// If, however, the input field has focus, the update is ignored.
343    ///
344    /// On every edit, the guard attempts to parse the field's input as type
345    /// `T` via [`FromStr`], caching the result and setting the error state.
346    ///
347    /// On field activation and focus loss when a `T` value is cached (see
348    /// previous paragraph), `on_afl` is used to construct a message to be
349    /// emitted via [`EventCx::push`]. The cached value is then cleared to
350    /// avoid sending duplicate messages.
351    #[inline]
352    pub fn parser<T: Debug + Display + FromStr, M: Debug + 'static>(
353        value_fn: impl Fn(&A) -> T + 'static,
354        msg_fn: impl Fn(T) -> M + 'static,
355    ) -> EditBox<ParseGuard<A, T>> {
356        EditBox::new(ParseGuard::new(value_fn, msg_fn))
357    }
358
359    /// Construct an `EditBox` for a parsable value (e.g. a number)
360    ///
361    /// On update, `value_fn` is used to extract a value from input data
362    /// which is then formatted as a string via [`Display`].
363    /// If, however, the input field has focus, the update is ignored.
364    ///
365    /// On every edit, the guard attempts to parse the field's input as type
366    /// `T` via [`FromStr`]. On success, the result is converted to a
367    /// message via `on_afl` then emitted via [`EventCx::push`].
368    pub fn instant_parser<T: Debug + Display + FromStr, M: Debug + 'static>(
369        value_fn: impl Fn(&A) -> T + 'static,
370        msg_fn: impl Fn(T) -> M + 'static,
371    ) -> EditBox<InstantParseGuard<A, T>> {
372        EditBox::new(InstantParseGuard::new(value_fn, msg_fn))
373    }
374}
375
376impl<A: 'static> EditBox<StringGuard<A>> {
377    /// Assign a message function for a `String` value
378    ///
379    /// The `msg_fn` is called when the field is activated (<kbd>Enter</kbd>)
380    /// and when it loses focus after content is changed.
381    ///
382    /// This method sets self as editable (see [`Self::with_editable`]).
383    #[must_use]
384    pub fn with_msg<M>(mut self, msg_fn: impl Fn(&str) -> M + 'static) -> Self
385    where
386        M: Debug + 'static,
387    {
388        self.inner.guard = self.inner.guard.with_msg(msg_fn);
389        self.inner.set_editable(true);
390        self
391    }
392}
393
394impl<G: EditGuard> EditBox<G> {
395    /// Set the initial text (inline)
396    ///
397    /// This method should only be used on a new `EditBox`.
398    #[inline]
399    #[must_use]
400    pub fn with_text(mut self, text: impl ToString) -> Self {
401        self.inner = self.inner.with_text(text);
402        self
403    }
404
405    /// Set whether this widget is editable (inline)
406    #[inline]
407    #[must_use]
408    pub fn with_editable(mut self, editable: bool) -> Self {
409        self.inner = self.inner.with_editable(editable);
410        self
411    }
412
413    /// Set whether this `EditBox` uses multi-line mode
414    ///
415    /// This setting has two effects: the vertical size allocation is increased
416    /// and wrapping is enabled if true. Default: false.
417    ///
418    /// This method is ineffective if the text class is set by
419    /// [`Self::with_class`] to anything other than [`TextClass::Editor`].
420    #[inline]
421    #[must_use]
422    pub fn with_multi_line(mut self, multi_line: bool) -> Self {
423        self.inner = self.inner.with_multi_line(multi_line);
424        self
425    }
426
427    /// Set the text class used
428    #[inline]
429    #[must_use]
430    pub fn with_class(mut self, class: TextClass) -> Self {
431        self.inner = self.inner.with_class(class);
432        self
433    }
434
435    /// Adjust the height allocation
436    #[inline]
437    pub fn set_lines(&mut self, min_lines: f32, ideal_lines: f32) {
438        self.inner.set_lines(min_lines, ideal_lines);
439    }
440
441    /// Adjust the height allocation (inline)
442    #[inline]
443    #[must_use]
444    pub fn with_lines(mut self, min_lines: f32, ideal_lines: f32) -> Self {
445        self.set_lines(min_lines, ideal_lines);
446        self
447    }
448
449    /// Adjust the width allocation
450    #[inline]
451    pub fn set_width_em(&mut self, min_em: f32, ideal_em: f32) {
452        self.inner.set_width_em(min_em, ideal_em);
453    }
454
455    /// Adjust the width allocation (inline)
456    #[inline]
457    #[must_use]
458    pub fn with_width_em(mut self, min_em: f32, ideal_em: f32) -> Self {
459        self.set_width_em(min_em, ideal_em);
460        self
461    }
462}