Skip to main content

teksilo_widgets/code_editor/
widget.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! The public editing surfaces: [`CodeEditor`] and [`PlainTextEditor`].
5//!
6//! The wrapper is the focus + event target; it owns the gutter (optional), the
7//! paint-only body, and the overlay scrollbars, joined to them only through the
8//! shared [`CodeEditorState`](super::state::CodeEditorState). This mirrors
9//! `RichTextEditor` exactly — the wrapper carries focus so a future style may
10//! place the body anywhere in its chrome without the focus semantics moving —
11//! and adds the two things a source editor needs on top: a line-number gutter to
12//! the left, and a paint pass that draws the current-line band (across gutter and
13//! body) and the matched-bracket cells behind the text.
14//!
15//! `PlainTextEditor` is the same machinery with the code affordances off and
16//! wrapping on — a notes field, a commit message — so the two never drift.
17//!
18//! ## Pan to scroll
19//!
20//! The surface installs [`common::scrollable::ScrollableBehavior`](crate::common::scrollable::ScrollableBehavior)
21//! — the shared wheel arithmetic, a finger's pan, and the `PanClaim`. The wheel
22//! path is unchanged: no tween (these offsets are plain signals), 16 dp a line,
23//! `Ignored` at a hard boundary so the page around it takes the rest, and a
24//! repaint asked for exactly when an axis moved.
25//!
26//! **The claim serves this surface even though it also owns the press
27//! arena**, which its double- and triple-tap recognizers give it. The router
28//! stops its arbitration walk at the press owner only for a `Gesture` member,
29//! whose recognizer the capture dispatch is already driving; a `Pan` member is
30//! decided in that walk and nowhere else, so it is exempt. A finger on the
31//! text therefore scrolls the text, and hands the gesture outward only at this
32//! surface's own boundary. See `docs/kinetic-scrolling.md` §10.1.
33
34use std::cell::Cell;
35use std::rc::Rc;
36
37use teksilo_canvas::{Canvas, Point, Rect, Size, SizeProposal};
38use teksilo_core::accessibility::AccessNodeBuilder;
39use teksilo_core::binding::BindingLevel;
40use teksilo_core::build_context::BuildContext;
41use teksilo_core::widget::{
42    CursorIcon, LayoutContext, LayoutResponse, PaintContext, Widget, WidgetPlacement,
43};
44use teksilo_core::widget_builder::{HandlerSet, WidgetBuilder};
45use teksilo_core::widget_id::WidgetId;
46use teksilo_text::text_document::TextDocument;
47use teksilo_text::{CursorAffinity, WrapMode};
48
49use super::completion::{self, CompletionContext, CompletionItem, CompletionPanel};
50use super::config::{BracketPair, CodeConfig, IndentStyle};
51use super::gutter::CodeGutter;
52use super::policy::{CODE_EDITOR_PRESET, CODE_READ_ONLY_PRESET};
53use super::state::SharedState;
54use super::{CodeEditorHandle, adopt_shared_typesetter, body_for, construct};
55use crate::common::editor_runtime::CaretPolicy;
56use crate::common::scroll::OverscrollBehavior;
57use crate::rich_text::ScrollPolicy;
58use crate::rich_text::touch_mount::ToolbarIntent;
59use crate::scroll_bar::{ScrollBar, ScrollBarOrientation, ScrollBarVariant};
60
61/// Overlay scrollbar thickness, matching the rich-text editor and `ScrollArea`.
62const SCROLLBAR_THICKNESS: f32 = 12.0;
63
64/// A multi-line source-code editing surface: gutter, current-line highlight,
65/// indentation, bracket handling, and multiple carets.
66///
67/// Construct with [`CodeEditor::new`] (editable) or [`CodeEditor::read_only`]
68/// (view + select + copy). Every code affordance is injected configuration, not
69/// a built-in language — see [`CodeConfig`].
70pub struct CodeEditor {
71    pub(super) state: SharedState,
72    v_scroll_policy: ScrollPolicy,
73    h_scroll_policy: ScrollPolicy,
74    overscroll_behavior: OverscrollBehavior,
75    min_lines: Option<u32>,
76    max_lines: Option<u32>,
77    show_gutter: bool,
78
79    // Child ids, filled during `build`.
80    gutter_id: Option<WidgetId>,
81    body_id: Option<WidgetId>,
82    v_scrollbar_id: Option<WidgetId>,
83    h_scrollbar_id: Option<WidgetId>,
84    // Scrollbar window-local bounds, published by `place_children`, read by the
85    // pointer handler to bypass the drag-select latch over an overlay bar.
86    v_scrollbar_bounds: Rc<Cell<Rect>>,
87    h_scrollbar_bounds: Rc<Cell<Rect>>,
88    // Gutter width, published by `place_children`. The bracket cells in `paint`
89    // offset from the body's own `viewport_origin`, which already includes it.
90    gutter_width: Rc<Cell<f32>>,
91    /// The touch-selection mount: the controller, its two overlays, and the
92    /// host intent behind the selection toolbar.
93    ///
94    /// Minted with the widget rather than in `build()` so the ids and the
95    /// toolbar intent survive a rebuild. Inert for a mouse: every entry point
96    /// begins by asking whether the pointer is direct.
97    pub(super) touch: Rc<crate::rich_text::touch_mount::EditorTouch>,
98    /// Install the built-in right-click menu during `build()`. Default `true`;
99    /// [`default_context_menu`](CodeEditor::default_context_menu) turns it off.
100    default_context_menu_enabled: bool,
101    /// A replacement factory, taken (`Option::take`) during `build()` because a
102    /// `Box<dyn Fn>` is not `Clone`.
103    custom_context_menu: Option<super::context_menu::CodeContextMenuFactory>,
104    /// Accessible name, set via [`label`](CodeEditor::label) and applied to
105    /// the body.
106    label: Option<teksilo_i18n::LocalizedString>,
107}
108
109impl std::fmt::Debug for CodeEditor {
110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111        f.debug_struct("CodeEditor")
112            .field("policy", &self.state.borrow().policy)
113            .field("show_gutter", &self.show_gutter)
114            .finish_non_exhaustive()
115    }
116}
117
118impl CodeEditor {
119    /// An editable code editor bound to `document`: gutter on, current-line
120    /// highlight on, no wrapping. Code affordances (comment token, bracket
121    /// pairs) stay off until the application supplies them — the editor never
122    /// guesses a language.
123    pub fn new(document: TextDocument) -> Self {
124        let this = Self::from_state(construct(
125            document,
126            CODE_EDITOR_PRESET,
127            CodeConfig::default(),
128            WrapMode::None,
129        ));
130        this.state.borrow_mut().current_line_highlight = true;
131        this
132    }
133
134    /// A read-only code viewer bound to `document`: no caret, navigation and
135    /// copy only, `Role::Document`. Still gets the gutter and syntax colours.
136    pub fn read_only(document: TextDocument) -> Self {
137        Self::from_state(construct(
138            document,
139            CODE_READ_ONLY_PRESET,
140            CodeConfig::default(),
141            WrapMode::None,
142        ))
143    }
144
145    fn from_state(state: SharedState) -> Self {
146        let touch = super::touch::mount_for(state.clone());
147        Self {
148            state,
149            v_scroll_policy: ScrollPolicy::Auto,
150            h_scroll_policy: ScrollPolicy::Auto,
151            overscroll_behavior: OverscrollBehavior::default(),
152            min_lines: None,
153            max_lines: None,
154            show_gutter: true,
155            gutter_id: None,
156            body_id: None,
157            v_scrollbar_id: None,
158            h_scrollbar_id: None,
159            v_scrollbar_bounds: Rc::new(Cell::new(Rect::ZERO)),
160            h_scrollbar_bounds: Rc::new(Cell::new(Rect::ZERO)),
161            gutter_width: Rc::new(Cell::new(0.0)),
162            touch,
163            default_context_menu_enabled: true,
164            custom_context_menu: None,
165            label: None,
166        }
167    }
168
169    // --- Shared builder methods ------------------------------------------
170
171    /// Accessible name for the editor.
172    ///
173    /// Applied to the body that holds the text, which is the node focus is
174    /// published on and the one a screen reader announces ("Code, entry"). The
175    /// editor's own node is structure that no adapter shows. An
176    /// `.access_label(..)`, `.access_labelled_by(..)` or tooltip attached to
177    /// the editor reaches the same node.
178    ///
179    /// Stays locale-reactive: a `tr!(...)` name is re-resolved when the
180    /// locale changes, without a rebuild.
181    pub fn label(mut self, label: impl Into<teksilo_i18n::LocalizedString>) -> Self {
182        self.label = Some(label.into());
183        self
184    }
185
186    /// Replace the built-in right-click menu with `factory`, called on each
187    /// right-click with the **window** position of the click. Returning `None`
188    /// shows no menu.
189    ///
190    /// A replacement is responsible for repositioning the caret if it wants the
191    /// platform convention — the built-in menu does it through
192    /// `context_menu::factory`, and only when a pointer opened the menu
193    /// ([`EventContext::context_menu_trigger`](teksilo_core::widget::EventContext::context_menu_trigger)).
194    /// Shift+F10 hands the factory an anchor, not a place the user chose.
195    pub fn context_menu(
196        mut self,
197        factory: impl Fn(
198            teksilo_canvas::Point,
199            &mut teksilo_core::widget::EventContext,
200        ) -> Option<Box<dyn teksilo_core::widget::Widget>>
201        + 'static,
202    ) -> Self {
203        self.custom_context_menu = Some(Box::new(factory));
204        self
205    }
206
207    /// Whether to install the built-in Cut / Copy / Paste / Select All menu
208    /// (default `true`). `false` lets a right-click bubble past the editor, so an
209    /// application can render its own menu from outside; a factory installed with
210    /// [`context_menu`](Self::context_menu) wins over this either way.
211    ///
212    /// The **touch** selection toolbar is *not* affected. It is raised by the
213    /// controller rather than by a right-click, its rows are the same four
214    /// commands, and a surface with no menu still has to be usable by a finger —
215    /// which has no second button and no chord.
216    pub fn default_context_menu(mut self, enabled: bool) -> Self {
217        self.default_context_menu_enabled = enabled;
218        self
219    }
220
221    /// Set the line-wrap mode. `CodeEditor` defaults to `WrapMode::None` (source
222    /// lines must not fold, or the gutter's one-number-per-line correspondence
223    /// breaks); pair with `.h_scroll_policy(Auto)` to scroll wide lines.
224    pub fn wrap_mode(self, mode: WrapMode) -> Self {
225        {
226            let mut st = self.state.borrow_mut();
227            st.wrap_mode = mode;
228            st.engine.set_wrap_mode(mode);
229            st.needs_full_layout = true;
230        }
231        self
232    }
233
234    /// Vertical scrollbar policy (default `Auto`).
235    pub fn v_scroll_policy(mut self, policy: ScrollPolicy) -> Self {
236        self.v_scroll_policy = policy;
237        self
238    }
239
240    /// Horizontal scrollbar policy (default `Auto`).
241    pub fn h_scroll_policy(mut self, policy: ScrollPolicy) -> Self {
242        self.h_scroll_policy = policy;
243        self
244    }
245
246    /// Wheel scroll-chaining at the editor's scroll boundary. `Chain` (default)
247    /// hands leftover scroll to an enclosing scrollable; `Contain` absorbs it.
248    pub fn overscroll_behavior(mut self, behavior: OverscrollBehavior) -> Self {
249        self.overscroll_behavior = behavior;
250        self
251    }
252
253    /// Cull the render to the visible clip band (default `false`). Turn on only
254    /// for an editor deliberately laid out at full document height inside an
255    /// outer `ScrollArea` (`v_scroll_policy(AlwaysOff)` + `min_lines(1)`): the
256    /// body's bounds then span the whole document, and this renders only the
257    /// on-screen slice instead of every line. A normally-scrolling editor already
258    /// renders just a viewport's worth, so it needs nothing.
259    pub fn window_to_clip(self, on: bool) -> Self {
260        self.state.borrow_mut().window_to_clip = on;
261        self
262    }
263
264    /// Minimum visible height in lines — switches the editor from greedy (fill
265    /// the proposal) to intrinsic sizing (grow with content up to `max_lines`,
266    /// then scroll). The composer pattern.
267    pub fn min_lines(mut self, lines: u32) -> Self {
268        self.min_lines = Some(lines);
269        self
270    }
271
272    /// Maximum visible height in lines — caps intrinsic growth.
273    pub fn max_lines(mut self, lines: u32) -> Self {
274        self.max_lines = Some(lines);
275        self
276    }
277
278    /// Fallback font family for the document's text. `None` (the default) keeps
279    /// the typesetter's registry default; a code editor should pass a monospace
280    /// family so columns line up.
281    pub fn font_family(self, family: impl Into<String>) -> Self {
282        {
283            let mut st = self.state.borrow_mut();
284            let mut d = st.engine.typography_defaults().clone();
285            d.font_family = Some(family.into());
286            st.engine.set_typography_defaults(d);
287            st.needs_full_layout = true;
288        }
289        self
290    }
291
292    /// Per-editor logical font-size multiplier (`1.0` = 100 %), composed with
293    /// the accessibility text scale when [`follow_text_scale`](Self::follow_text_scale)
294    /// is on. Sharp — shapes at a larger ppem.
295    pub fn font_size_scale(self, scale: f32) -> Self {
296        {
297            let mut st = self.state.borrow_mut();
298            st.font_size_scale = scale.clamp(0.1, 10.0);
299            st.last_font_scale = f32::NAN;
300            st.needs_full_layout = true;
301        }
302        self
303    }
304
305    /// Whether the editor grows text with the global accessibility text scale
306    /// (default `true`). Turn off for a WYSIWYG surface whose font sizes are
307    /// document content. Composed with [`font_size_scale`](Self::font_size_scale).
308    pub fn follow_text_scale(self, follow: bool) -> Self {
309        self.state.borrow_mut().follow_text_scale = follow;
310        self
311    }
312
313    /// A callback fired once per drain batch that contained a real content edit.
314    pub fn on_change(self, callback: impl Fn() + 'static) -> Self {
315        self.state.borrow_mut().on_change = Some(Rc::new(callback));
316        self
317    }
318
319    /// Override the editor background colour (accepts `Color`, a theme role, or a
320    /// `Signal`). `None`-equivalent default tracks the theme's `editor_bg`.
321    pub fn background(self, color: impl Into<teksilo_core::color_prop::ColorProp>) -> Self {
322        self.state.borrow_mut().background_prop = Some(color.into());
323        self
324    }
325
326    /// Override the text colour. Default tracks the theme's `editor_fg`.
327    pub fn text_color(self, color: impl Into<teksilo_core::color_prop::ColorProp>) -> Self {
328        self.state.borrow_mut().text_color_prop = Some(color.into());
329        self
330    }
331
332    /// Override the caret colour. Default tracks the theme's `editor_caret`.
333    pub fn caret_color(self, color: impl Into<teksilo_core::color_prop::ColorProp>) -> Self {
334        self.state.borrow_mut().caret_color_prop = Some(color.into());
335        self
336    }
337
338    /// Override the selection colour. A pinned colour opts out of the
339    /// window-inactive desaturation.
340    pub fn selection_color(self, color: impl Into<teksilo_core::color_prop::ColorProp>) -> Self {
341        self.state.borrow_mut().selection_color_prop = Some(color.into());
342        self
343    }
344
345    // --- Code-only builder methods ---------------------------------------
346
347    /// Whether the line-number gutter is shown (default `true`).
348    pub fn gutter(mut self, show: bool) -> Self {
349        self.show_gutter = show;
350        self
351    }
352
353    /// Whether the caret's line gets a full-width background wash (default
354    /// `true` for `CodeEditor`).
355    pub fn current_line_highlight(self, on: bool) -> Self {
356        self.state.borrow_mut().current_line_highlight = on;
357        self
358    }
359
360    /// Set the indentation style directly (spaces of a width, or tabs rendered a
361    /// width wide).
362    pub fn indent_style(self, style: IndentStyle) -> Self {
363        self.state.borrow_mut().config.indent = style;
364        self
365    }
366
367    /// Set the indent width, keeping the current spaces-vs-tabs kind.
368    pub fn tab_width(self, width: u8) -> Self {
369        {
370            let mut st = self.state.borrow_mut();
371            st.config.indent = match st.config.indent {
372                IndentStyle::Spaces(_) => IndentStyle::Spaces(width),
373                IndentStyle::Tabs { .. } => IndentStyle::Tabs { width },
374            };
375        }
376        self
377    }
378
379    /// Whether indentation is written with spaces (`true`, the default) or a tab
380    /// character (`false`), keeping the current width.
381    pub fn use_soft_tabs(self, soft: bool) -> Self {
382        {
383            let mut st = self.state.borrow_mut();
384            let w = st.config.indent.width();
385            st.config.indent = if soft {
386                IndentStyle::Spaces(w)
387            } else {
388                IndentStyle::Tabs { width: w }
389            };
390        }
391        self
392    }
393
394    /// Whether Enter carries the current line's indentation onto the new line
395    /// (default `true`).
396    pub fn auto_indent(self, on: bool) -> Self {
397        self.state.borrow_mut().config.auto_indent = on;
398        self
399    }
400
401    /// The delimiter pairs the editor auto-closes and match-highlights. Empty
402    /// (the default) disables both.
403    pub fn bracket_pairs(self, pairs: impl Into<Vec<BracketPair>>) -> Self {
404        self.state.borrow_mut().config.brackets = pairs.into();
405        self
406    }
407
408    /// Whether typing an opener inserts its closing partner (default `false`;
409    /// needs configured `bracket_pairs`).
410    pub fn auto_close_brackets(self, on: bool) -> Self {
411        self.state.borrow_mut().config.auto_close_brackets = on;
412        self
413    }
414
415    /// Whether the delimiter matching the caret's is highlighted (default
416    /// `false`; needs configured `bracket_pairs`).
417    pub fn bracket_matching(self, on: bool) -> Self {
418        self.state.borrow_mut().config.match_brackets = on;
419        self
420    }
421
422    /// The token that starts a line comment (`"//"`, `"#"`, `"--"`). Enables
423    /// `Ctrl+/` comment toggling; unset (the default) leaves it a no-op rather
424    /// than guessing.
425    pub fn line_comment(self, token: impl Into<String>) -> Self {
426        self.state.borrow_mut().config.line_comment = Some(token.into());
427        self
428    }
429
430    /// Supply the completion candidates. The provider is called for the word
431    /// being completed and given a [`CompletionContext`]; the editor filters its
432    /// result by the live prefix, shows the popup, and replaces the word on
433    /// accept. Language-agnostic — the app knows the candidates, the editor knows
434    /// the mechanics. Without a provider there is no completion.
435    pub fn completion_provider(
436        self,
437        provider: impl Fn(&CompletionContext) -> Vec<CompletionItem> + 'static,
438    ) -> Self {
439        self.state.borrow_mut().completion.provider = Some(Rc::new(provider));
440        self
441    }
442
443    /// Whether typing an identifier character opens the completion popup
444    /// automatically (default `true`). When off, only `Ctrl+Space` opens it.
445    pub fn auto_complete(self, auto: bool) -> Self {
446        self.state.borrow_mut().completion.auto_trigger = auto;
447        self
448    }
449
450    /// A cloneable handle to drive the editor from a toolbar, shortcut, or test.
451    pub fn handle(&self) -> CodeEditorHandle {
452        CodeEditorHandle::new(self.state.clone())
453    }
454
455    // --- Internal ---------------------------------------------------------
456
457    /// The vertical extent (window y, height) of the caret's line, or `None`
458    /// before a layout exists.
459    fn caret_line_band(st: &super::state::CodeEditorState) -> Option<(f32, f32)> {
460        if !st.engine.has_full_layout() {
461            return None;
462        }
463        let c = st
464            .engine
465            .caret_rect(st.cursor.position(), st.cursor_affinity);
466        let y = st.viewport_origin.y + c[1] - st.scroll_y.get();
467        Some((y, c[3]))
468    }
469}
470
471impl Widget for CodeEditor {
472    fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
473        // Tell the framework this widget edits text — registered on *this*
474        // node, the `.focusable(true)` one below, because the registry is keyed
475        // by whichever widget holds the focus. See `teksilo_core::text_surface`.
476        ctx.register_text_surface(std::rc::Rc::new(self.handle()));
477
478        // Swap the private engine for one sharing the app's typesetter (no-op
479        // headless), carrying over builder-set typography.
480        adopt_shared_typesetter(&self.state, ctx);
481
482        {
483            let mut st = self.state.borrow_mut();
484            st.frame_request = Some(ctx.frame_request_handle());
485            st.frame_wake_at = Some(ctx.wake_at_handle());
486            st.self_id = Some(ctx.self_id());
487            // The density ladder the pointer geometry reads. A snapshot, not a
488            // per-event read: `EventContext` exposes no theme, and
489            // `set_input_density` marks at `BindingLevel::Rebuild`, so this
490            // refreshes with the density.
491            st.input_tokens = ctx.theme().input;
492        }
493        // Same dormancy discipline as `RichTextEditor` / `TextInputField`: a
494        // code or plain-text editor parked in a non-selected Switcher branch
495        // must not keep the event loop awake. `PlainTextEditor` is a thin
496        // wrap of this widget, so it inherits the gate for free.
497        let activation = ctx.activation_signal(ctx.self_id());
498        if activation.get() {
499            ctx.request_frame();
500        }
501
502        {
503            let state = self.state.clone();
504            ctx.effect(&activation, move |&active| {
505                if active {
506                    // **Re-activated** — re-arm the frame loop. The dormant branch
507                    // below does not re-arm `frame_request` (by design: a parked
508                    // editor has nothing to paint) and the frame-tick effect is
509                    // skipped entirely while dormant, so nothing restarts the tick
510                    // on the way back. Only the tick pushes the cursor through to
511                    // the engine, so a re-activated editor that is then focused
512                    // draws **no caret at all**.
513                    //
514                    // The in-tree modal path takes this route on every open —
515                    // build, `set_dormant`, mount, `activate`, *then* focus (see
516                    // `present_in_tree_modal_request`) — as do a tab switch and a
517                    // collapsed pane. Same fix as `RichTextEditor`; this file backs
518                    // both `CodeEditor` and `PlainTextEditor`.
519                    let st = state.borrow();
520                    if let Some(handle) = &st.frame_request {
521                        handle.set(true);
522                    }
523                    return;
524                }
525                let mut st = state.borrow_mut();
526                if st.has_focus {
527                    st.has_focus = false;
528                    st.focus_signal.set_if_changed(false);
529                }
530                st.caret_visible.set_if_changed(false);
531                st.blink.reset();
532            });
533        }
534
535        // Frame-tick effect: drain events, blink, lay out, publish metrics.
536        // Skipped while dormant so multi-tab / multi-page hosts do not pay
537        // O(open editors) per wake for surfaces nobody can see.
538        {
539            let state = self.state.clone();
540            let active = activation.clone();
541            let tick_signal = ctx.frame_tick();
542            ctx.effect(&tick_signal, move |delta| {
543                if !active.get() {
544                    return;
545                }
546                let mut st = state.borrow_mut();
547                let more = super::frame_loop::tick(&mut st, *delta);
548                if more && let Some(handle) = &st.frame_request {
549                    handle.set(true);
550                }
551            });
552        }
553
554        // Window-active effect: hide the caret synchronously on deactivation
555        // (the loop may not tick while the window is inactive). Re-arm the
556        // frame loop only while this editor is itself active.
557        {
558            let state = self.state.clone();
559            let active = activation.clone();
560            let wa_signal = ctx.window_active_signal();
561            ctx.effect(&wa_signal, move |&window_active| {
562                let mut st = state.borrow_mut();
563                st.window_active = window_active;
564                if window_active {
565                    let show =
566                        st.has_focus && !matches!(st.policy.caret_policy, CaretPolicy::Hidden);
567                    if show {
568                        st.caret_visible.set_if_changed(true);
569                    }
570                    st.blink.reset();
571                } else {
572                    st.caret_visible.set_if_changed(false);
573                    st.blink.reset();
574                }
575                if active.get()
576                    && let Some(handle) = &st.frame_request
577                {
578                    handle.set(true);
579                }
580            });
581        }
582
583        // Handlers on the wrapper — the focus + event target.
584        let mut handlers = HandlerSet::new();
585        if !self.state.borrow().policy.is_read_only() {
586            handlers = handlers.ime_input(teksilo_core::ime::ImeContext::text());
587        }
588        handlers = handlers
589            .focusable(true)
590            .cursor(CursorIcon::Text)
591            .on_focus({
592                let state = self.state.clone();
593                let touch = self.touch.clone();
594                move |gained, ctx| {
595                    {
596                        let mut st = state.borrow_mut();
597                        st.has_focus = gained;
598                        st.focus_signal.set_if_changed(gained);
599                        if gained && matches!(st.policy.caret_policy, CaretPolicy::Blinking) {
600                            st.blink.restart();
601                            st.caret_visible.set_if_changed(true);
602                        }
603                    }
604                    if gained {
605                        super::keyboard::report_ime_cursor_area(&state, ctx);
606                    } else {
607                        super::keyboard::clear_ime_preedit(&state);
608                        // A popup that outlived its editor's focus would float
609                        // detached — close it on blur.
610                        completion::close(&state, ctx);
611                        // The affordance band is exempt from outside-press
612                        // dismissal — every caret-moving tap is outside a handle
613                        // — so retirement on focus loss is the host's.
614                        touch.dismiss();
615                        let mut st = state.borrow_mut();
616                        st.last_ime_area = None;
617                        st.last_chase_pos = None;
618                    }
619                    ctx.request_frame();
620                }
621            })
622            .on_pointer_event({
623                let state = self.state.clone();
624                let touch = self.touch.clone();
625                let v_sb = self.v_scrollbar_bounds.clone();
626                let h_sb = self.h_scrollbar_bounds.clone();
627                move |event, ctx| {
628                    super::mouse::handle_pointer_event(&state, &touch, &v_sb, &h_sb, event, ctx)
629                }
630            })
631            // A hold selects the word under the finger. Attaching this
632            // **withdraws** the tree-owned long-press route (`touch_route`
633            // rule 1: a widget's own `on_long_press` wins), and the selection
634            // toolbar the mount raises is what replaces it — offering the same
635            // commands, from the same rows, as the right-click menu below.
636            .on_long_press({
637                let state = self.state.clone();
638                let touch = self.touch.clone();
639                move |event, ctx| {
640                    super::mouse::handle_long_press(&state, &touch, event, ctx);
641                }
642            })
643            .on_key({
644                let state = self.state.clone();
645                let touch = self.touch.clone();
646                move |event, ctx| {
647                    let response = super::keyboard::handle_key(&state, event, ctx);
648                    // A keystroke moves the caret and edits the text, neither of
649                    // which the controller made — so the handles it published
650                    // are pointing at where the text used to be. `refresh` is a
651                    // no-op until something has been raised.
652                    touch.refresh(ctx, ToolbarIntent::Hide);
653                    response
654                }
655            })
656            .on_double_tap({
657                let state = self.state.clone();
658                let touch = self.touch.clone();
659                move |event, ctx| {
660                    super::mouse::handle_double_tap(&state, event.position, ctx);
661                    // A finger can double-tap too, and the selection it just
662                    // made is one the controller did not make.
663                    if event.pointer.kind.is_direct() {
664                        touch.raise(ctx, ToolbarIntent::Show);
665                    }
666                }
667            })
668            .on_triple_tap({
669                let state = self.state.clone();
670                let touch = self.touch.clone();
671                move |event, ctx| {
672                    super::mouse::handle_triple_tap(&state, event.position, ctx);
673                    if event.pointer.kind.is_direct() {
674                        touch.raise(ctx, ToolbarIntent::Show);
675                    }
676                }
677            })
678            .on_access_action_request({
679                let state = self.state.clone();
680                let touch = self.touch.clone();
681                move |action, target, data, ctx| {
682                    let response =
683                        super::a11y::handle_access_action(&state, action, target, data, ctx);
684                    // An assistive client's `SetTextSelection` / `SetValue` /
685                    // `ReplaceSelectedText` moves the selection without going
686                    // through the controller, so raised handles would be left
687                    // marking the old range.
688                    touch.refresh(ctx, ToolbarIntent::Keep);
689                    response
690                }
691            });
692        // The right-click menu this editor never had. Built fresh per click so
693        // each row's enabled state reflects the live selection and policy.
694        if let Some(factory) = super::context_menu::resolve_factory(
695            self.custom_context_menu.take(),
696            self.default_context_menu_enabled,
697            self.state.clone(),
698        ) {
699            handlers = handlers.context_menu(move |pos, ctx| factory(pos, ctx));
700        }
701        // Scroll: the wheel path this surface always had, a finger's pan, and
702        // the claim that puts it on a pan's claimant chain — all from
703        // `common::text_scroll`, which the three text surfaces share.
704        {
705            let (x, max_x, y, max_y, scroller) = {
706                let st = self.state.borrow();
707                (
708                    st.scroll_x.clone(),
709                    st.max_scroll_x.clone(),
710                    st.scroll_y.clone(),
711                    st.max_scroll_y.clone(),
712                    st.scroller.clone(),
713                )
714            };
715            let behavior = crate::common::text_scroll::text_surface_behavior(
716                crate::common::text_scroll::TextScrollState {
717                    x,
718                    max_x,
719                    y,
720                    max_y,
721                    scroller,
722                },
723                self.overscroll_behavior,
724                ctx.prefers_reduced_motion(),
725                ctx.theme().input.scroll_physics,
726            );
727            handlers = behavior.install(handlers);
728        }
729
730        ctx.apply_self_handlers(handlers);
731
732        // Select-all, the clipboard and undo / redo come through the
733        // rebindable `edit.*` shortcuts, not as keys.
734        {
735            let state = self.state.clone();
736            let touch = self.touch.clone();
737            let surfaces = ctx.text_surfaces();
738            crate::common::standard_edit::register_standard_edit_actions(
739                ctx,
740                move |command, ctx| {
741                    let ran = super::keyboard::run_standard_edit(
742                        &state,
743                        command,
744                        surfaces.empty_history_passes_on(),
745                        ctx,
746                    );
747                    if ran {
748                        // As after a key: the published handles point at
749                        // where the text used to be.
750                        touch.refresh(ctx, ToolbarIntent::Hide);
751                    }
752                    ran
753                },
754            );
755        }
756
757        // Comment and duplicate: shared `code.*` shortcuts, declared while an
758        // editable editor exists, and an action per command here.
759        if !self.state.borrow().policy.is_read_only() {
760            for command in crate::editor_shortcuts::CodeShortcut::ALL {
761                ctx.register_shortcut_shared(command.shortcut());
762            }
763        }
764        for command in crate::editor_shortcuts::CodeShortcut::ALL {
765            let state = self.state.clone();
766            let touch = self.touch.clone();
767            crate::common::standard_edit::register_command_action(ctx, command.id(), move |ctx| {
768                let ran = super::keyboard::run_code_shortcut(&state, command, ctx);
769                if ran {
770                    touch.refresh(ctx, ToolbarIntent::Hide);
771                }
772                ran
773            });
774        }
775
776        // The touch-selection overlays: the affordance layer (handles + lens)
777        // and the selection toolbar, both detached content owned by this build.
778        // Inert until a finger raises them.
779        let self_id = ctx.self_id();
780        self.touch.build(ctx, self_id);
781
782        // Body — the pure-paint leaf. Always greedy: the wrapper does intrinsic
783        // sizing (min/max_lines) and hands the body its final rect.
784        let body = body_for(&self.state, None, None);
785        // The name goes on the body, the node focus is published on; see
786        // [`label`](CodeEditor::label).
787        let body_id = match self.label.clone() {
788            Some(label) => ctx.add(body.access_label(label)),
789            None => ctx.add(body),
790        };
791        self.body_id = Some(body_id);
792
793        // Reactive colour overrides repaint the body (the leaf that resolves
794        // them). Theme-role changes already dirty every node; this covers
795        // Signal-bound props.
796        {
797            let props = {
798                let st = self.state.borrow();
799                [
800                    st.text_color_prop.clone(),
801                    st.caret_color_prop.clone(),
802                    st.selection_color_prop.clone(),
803                ]
804            };
805            let registry = ctx.binding_registry();
806            for prop in props.iter().flatten() {
807                prop.register_if_bound(body_id, registry, BindingLevel::RepaintOnly);
808            }
809        }
810
811        let mut children = Vec::with_capacity(4);
812        if self.show_gutter {
813            let gutter_id = ctx.add(CodeGutter::new(&self.state));
814            self.gutter_id = Some(gutter_id);
815            children.push(gutter_id);
816        }
817        children.push(body_id);
818
819        // Overlay scrollbars driven by the metrics the frame loop publishes.
820        let (scroll_x, scroll_y, max_x, max_y, vr_x, vr_y) = {
821            let st = self.state.borrow();
822            (
823                st.scroll_x.clone(),
824                st.scroll_y.clone(),
825                st.max_scroll_x.clone(),
826                st.max_scroll_y.clone(),
827                st.viewport_ratio_x.clone(),
828                st.viewport_ratio_y.clone(),
829            )
830        };
831        if self.v_scroll_policy != ScrollPolicy::AlwaysOff {
832            let v = ScrollBar::new(
833                ScrollBarOrientation::Vertical,
834                scroll_y,
835                max_y.clone(),
836                vr_y,
837            )
838            .visual(ScrollBarVariant::Overlay);
839            let id = ctx.add(v);
840            self.v_scrollbar_id = Some(id);
841            children.push(id);
842        }
843        if self.h_scroll_policy != ScrollPolicy::AlwaysOff {
844            let h = ScrollBar::new(
845                ScrollBarOrientation::Horizontal,
846                scroll_x,
847                max_x.clone(),
848                vr_x,
849            )
850            .visual(ScrollBarVariant::Overlay);
851            let id = ctx.add(h);
852            self.h_scrollbar_id = Some(id);
853            children.push(id);
854        }
855
856        // Completion popup content — pre-created and kept dormant (the ComboBox
857        // dropdown pattern), so it is never an orphan arena root and never
858        // ghost-paints while logically closed. `show_overlay` moves it to the
859        // overlay layer when completion opens.
860        if self.state.borrow().completion.has_provider() {
861            let open = self.state.borrow().completion.open.clone();
862            // Built the first time completion opens, not on every rebuild of the
863            // editor. See `teksilo_core::deferred_subtree::DeferredSubtree`.
864            let panel_id = ctx.add_deferred(open.clone(), CompletionPanel::new(&self.state));
865            ctx.set_dormant(panel_id);
866            ctx.visible_when(panel_id, open);
867            self.state.borrow_mut().completion.panel_id = Some(panel_id);
868            children.push(panel_id);
869        }
870
871        // The `Auto` scrollbars appear only when there is overflow; those maxima
872        // are published by the frame loop, so re-place when they cross zero.
873        let self_id = ctx.self_id();
874        let registry = ctx.binding_registry();
875        max_y.bind_to(self_id, registry, BindingLevel::Relayout);
876        max_x.bind_to(self_id, registry, BindingLevel::Relayout);
877
878        children
879    }
880
881    fn layout_response(&self, proposal: SizeProposal, ctx: &LayoutContext) -> LayoutResponse {
882        let w = proposal.width.unwrap_or(400.0).max(0.0);
883
884        // Greedy unless the composer knobs are set.
885        if self.min_lines.is_none() && self.max_lines.is_none() {
886            let h = proposal.height.unwrap_or(300.0).max(0.0);
887            return Size::new(w, h).into();
888        }
889
890        let st = self.state.borrow();
891        let line_scale = st.effective_font_scale(ctx.text_scale);
892        let line_h = st.engine.default_line_height() * line_scale;
893        let content_h = st.engine.content_height();
894        drop(st);
895
896        let min_h = self.min_lines.map(|n| n as f32 * line_h).unwrap_or(0.0);
897        let max_h = self
898            .max_lines
899            .map(|n| n as f32 * line_h)
900            .unwrap_or(f32::INFINITY);
901        Size::new(w, content_h.clamp(min_h, max_h).max(0.0)).into()
902    }
903
904    fn place_children(
905        &self,
906        bounds: Rect,
907        _proposal: SizeProposal,
908        children: &mut [WidgetPlacement],
909        ctx: &LayoutContext,
910    ) {
911        self.state.borrow_mut().node_origin = Point::new(bounds.x, bounds.y);
912
913        // Gutter width, measured from its intrinsic response (it sizes to the
914        // widest line number the document will ever hold).
915        let gutter_w = self
916            .gutter_id
917            .and_then(|id| ctx.child_size(id, SizeProposal::with_height(bounds.height)))
918            .map(|s| s.width)
919            .unwrap_or(0.0);
920        self.gutter_width.set(gutter_w);
921
922        let body_x = bounds.x + gutter_w;
923        let body_w = (bounds.width - gutter_w).max(0.0);
924
925        let (max_y, max_x) = {
926            let st = self.state.borrow();
927            (st.max_scroll_y.get(), st.max_scroll_x.get())
928        };
929        let show_v = match self.v_scroll_policy {
930            ScrollPolicy::AlwaysOn => true,
931            ScrollPolicy::Auto => max_y > 0.0,
932            ScrollPolicy::AlwaysOff => false,
933        };
934        let show_h = match self.h_scroll_policy {
935            ScrollPolicy::AlwaysOn => true,
936            ScrollPolicy::Auto => max_x > 0.0,
937            ScrollPolicy::AlwaysOff => false,
938        };
939
940        let mut v_rect = Rect::ZERO;
941        let mut h_rect = Rect::ZERO;
942        for child in children.iter_mut() {
943            if Some(child.id) == self.gutter_id {
944                child.origin = Point::new(bounds.x, bounds.y);
945                child.size = Size::new(gutter_w, bounds.height);
946            } else if Some(child.id) == self.body_id {
947                child.origin = Point::new(body_x, bounds.y);
948                child.size = Size::new(body_w, bounds.height);
949            } else if Some(child.id) == self.v_scrollbar_id {
950                if show_v {
951                    let h = if show_h {
952                        (bounds.height - SCROLLBAR_THICKNESS).max(0.0)
953                    } else {
954                        bounds.height
955                    };
956                    child.origin =
957                        Point::new(bounds.x + bounds.width - SCROLLBAR_THICKNESS, bounds.y);
958                    child.size = Size::new(SCROLLBAR_THICKNESS, h);
959                    v_rect = Rect::new(
960                        child.origin.x - bounds.x,
961                        child.origin.y - bounds.y,
962                        SCROLLBAR_THICKNESS,
963                        h,
964                    );
965                } else {
966                    child.origin = Point::new(bounds.x, bounds.y);
967                    child.size = Size::ZERO;
968                }
969            } else if Some(child.id) == self.h_scrollbar_id {
970                if show_h {
971                    let w = if show_v {
972                        (body_w - SCROLLBAR_THICKNESS).max(0.0)
973                    } else {
974                        body_w
975                    };
976                    child.origin =
977                        Point::new(body_x, bounds.y + bounds.height - SCROLLBAR_THICKNESS);
978                    child.size = Size::new(w, SCROLLBAR_THICKNESS);
979                    h_rect = Rect::new(
980                        child.origin.x - bounds.x,
981                        child.origin.y - bounds.y,
982                        w,
983                        SCROLLBAR_THICKNESS,
984                    );
985                } else {
986                    child.origin = Point::new(bounds.x, bounds.y);
987                    child.size = Size::ZERO;
988                }
989            }
990        }
991        self.v_scrollbar_bounds.set(v_rect);
992        self.h_scrollbar_bounds.set(h_rect);
993    }
994
995    fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
996        // Background fill, then the current-line band, then the matched-bracket
997        // cells — all behind the children (gutter numbers, body text), which
998        // paint on top. A band spanning gutter and body is exactly why it lives
999        // on the wrapper rather than in either child.
1000        let st = self.state.borrow();
1001
1002        let bg = match &st.background_prop {
1003            Some(p) => p.resolve(ctx.theme, true),
1004            None => ctx.theme.colors.editor_bg,
1005        };
1006        canvas.fill_rect(bounds, bg);
1007
1008        // Current-line band: only for a single collapsed caret in a focused,
1009        // active window — a band under a selection or several carets reads as
1010        // noise, which is the convention every editor follows.
1011        let single_collapsed = st.extra_carets.is_empty() && !st.cursor.has_selection();
1012        if st.current_line_highlight
1013            && st.has_focus
1014            && st.window_active
1015            && single_collapsed
1016            && let Some((y, h)) = Self::caret_line_band(&st)
1017            && y + h > bounds.y
1018            && y < bounds.y + bounds.height
1019        {
1020            let band = Rect::new(bounds.x, y, bounds.width, h);
1021            canvas.fill_rect(band, ctx.theme.colors.surface_hover);
1022        }
1023
1024        // Matched-bracket cells: a faint wash behind each of the two brackets.
1025        if let Some((a, b)) = st.bracket_match.get()
1026            && st.engine.has_full_layout()
1027        {
1028            let origin = st.viewport_origin;
1029            let scroll_x = st.scroll_x.get();
1030            let scroll_y = st.scroll_y.get();
1031            for p in [a, b] {
1032                let r0 = st.engine.caret_rect(p, CursorAffinity::Downstream);
1033                let r1 = st.engine.caret_rect(p + 1, CursorAffinity::Downstream);
1034                let x = origin.x + r0[0] - scroll_x;
1035                let w = (r1[0] - r0[0]).max(2.0);
1036                let y = origin.y + r0[1] - scroll_y;
1037                let h = r0[3];
1038                // Clip to the body region so a bracket scrolled behind the
1039                // gutter does not paint over the numbers.
1040                if x + w > origin.x && y + h > bounds.y && y < bounds.y + bounds.height {
1041                    canvas.fill_rect(Rect::new(x, y, w, h), ctx.theme.colors.accent_subtle_bg);
1042                }
1043            }
1044        }
1045
1046        drop(st);
1047
1048        // A 1 px border that brightens on focus — minimal chrome until a Tier-3
1049        // style lands.
1050        let focused = self.state.borrow().focus_signal.get();
1051        let border = if focused {
1052            ctx.theme.colors.border_focused
1053        } else {
1054            ctx.theme.colors.border
1055        };
1056        canvas.stroke_rect(bounds, border, 1.0);
1057    }
1058
1059    fn accessibility(&self, builder: &mut AccessNodeBuilder) {
1060        // The role, actions and the run tree live on the body leaf, mirroring
1061        // RichTextEditor. The wrapper is structure: with the default
1062        // `Role::Unknown` an adapter would show it as an unnamed "unknown".
1063        builder.set_role(teksilo_core::accesskit::Role::GenericContainer);
1064    }
1065
1066    fn accessibility_proxy(&self) -> Option<WidgetId> {
1067        // The body stands for the editor: this widget takes the keys, and the
1068        // body holds the text, so focus, a name and anything an application
1069        // attaches here are published on the body.
1070        self.body_id
1071    }
1072
1073    fn children(&self) -> Vec<WidgetId> {
1074        let mut ids = Vec::with_capacity(5);
1075        ids.extend(self.gutter_id);
1076        ids.extend(self.body_id);
1077        ids.extend(self.v_scrollbar_id);
1078        ids.extend(self.h_scrollbar_id);
1079        // The completion popup (a dormant overlay node) — tracked here so it is
1080        // not an orphan; positioned by the overlay manager when shown, skipped by
1081        // `place_children` otherwise.
1082        ids.extend(self.state.borrow().completion.panel_id);
1083        ids
1084    }
1085
1086    fn clips_children(&self) -> bool {
1087        true
1088    }
1089}
1090
1091/// A multi-line plain-text editing surface — the code editor with its code
1092/// affordances off and wrapping on. A notes field, a commit message, a
1093/// description box.
1094///
1095/// It shares [`CodeEditor`]'s machinery (caret, selection, IME, clipboard,
1096/// scrolling, accessibility); the difference is configuration, so the two never
1097/// drift. Construct with [`PlainTextEditor::new`] / [`PlainTextEditor::read_only`].
1098#[derive(Debug)]
1099pub struct PlainTextEditor {
1100    pub(super) inner: Option<CodeEditor>,
1101    inner_id: Option<WidgetId>,
1102}
1103
1104impl PlainTextEditor {
1105    /// An editable plain-text editor bound to `document`: no gutter, no
1106    /// current-line highlight, word wrapping, and no code affordances.
1107    pub fn new(document: TextDocument) -> Self {
1108        Self::wrap(CodeEditor::new(document))
1109    }
1110
1111    /// A read-only plain-text viewer bound to `document`.
1112    pub fn read_only(document: TextDocument) -> Self {
1113        Self::wrap(CodeEditor::read_only(document))
1114    }
1115
1116    fn wrap(editor: CodeEditor) -> Self {
1117        // Plain-text defaults: fold the code chrome away, wrap like prose.
1118        let editor = editor
1119            .gutter(false)
1120            .current_line_highlight(false)
1121            .wrap_mode(WrapMode::Word);
1122        Self {
1123            inner: Some(editor),
1124            inner_id: None,
1125        }
1126    }
1127
1128    /// Restrict growth to `[min, max]` lines (intrinsic sizing — the composer
1129    /// pattern).
1130    pub fn min_lines(mut self, lines: u32) -> Self {
1131        self.map(|e| e.min_lines(lines));
1132        self
1133    }
1134
1135    /// Cap intrinsic growth at `lines`.
1136    pub fn max_lines(mut self, lines: u32) -> Self {
1137        self.map(|e| e.max_lines(lines));
1138        self
1139    }
1140
1141    /// Set the line-wrap mode (default `Word`).
1142    pub fn wrap_mode(mut self, mode: WrapMode) -> Self {
1143        self.map(|e| e.wrap_mode(mode));
1144        self
1145    }
1146
1147    /// Fallback font family.
1148    pub fn font_family(mut self, family: impl Into<String>) -> Self {
1149        self.map(|e| e.font_family(family));
1150        self
1151    }
1152
1153    /// Whether the editor follows the global accessibility text scale.
1154    pub fn follow_text_scale(mut self, follow: bool) -> Self {
1155        self.map(|e| e.follow_text_scale(follow));
1156        self
1157    }
1158
1159    /// Per-editor logical font-size multiplier (`1.0` = 100 %).
1160    pub fn font_size_scale(mut self, scale: f32) -> Self {
1161        self.map(|e| e.font_size_scale(scale));
1162        self
1163    }
1164
1165    /// A callback fired on each content-changing edit batch.
1166    pub fn on_change(mut self, callback: impl Fn() + 'static) -> Self {
1167        self.map(|e| e.on_change(callback));
1168        self
1169    }
1170
1171    /// Override the background colour.
1172    pub fn background(mut self, color: impl Into<teksilo_core::color_prop::ColorProp>) -> Self {
1173        self.map(|e| e.background(color));
1174        self
1175    }
1176
1177    /// Replace the built-in right-click menu — see
1178    /// [`CodeEditor::context_menu`].
1179    pub fn context_menu(
1180        mut self,
1181        factory: impl Fn(
1182            teksilo_canvas::Point,
1183            &mut teksilo_core::widget::EventContext,
1184        ) -> Option<Box<dyn teksilo_core::widget::Widget>>
1185        + 'static,
1186    ) -> Self {
1187        self.map(|e| e.context_menu(factory));
1188        self
1189    }
1190
1191    /// Whether to install the built-in right-click menu — see
1192    /// [`CodeEditor::default_context_menu`].
1193    pub fn default_context_menu(mut self, enabled: bool) -> Self {
1194        self.map(|e| e.default_context_menu(enabled));
1195        self
1196    }
1197
1198    /// Accessible name for the editor: see [`CodeEditor::label`].
1199    pub fn label(mut self, label: impl Into<teksilo_i18n::LocalizedString>) -> Self {
1200        self.map(|e| e.label(label));
1201        self
1202    }
1203
1204    /// A cloneable handle to drive the editor.
1205    pub fn handle(&self) -> CodeEditorHandle {
1206        self.inner.as_ref().expect("handle() before build").handle()
1207    }
1208
1209    /// Apply `f` to the inner editor in place (builders consume and return it).
1210    fn map(&mut self, f: impl FnOnce(CodeEditor) -> CodeEditor) {
1211        if let Some(e) = self.inner.take() {
1212            self.inner = Some(f(e));
1213        }
1214    }
1215}
1216
1217impl Widget for PlainTextEditor {
1218    fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
1219        let inner = self.inner.take().expect("PlainTextEditor built once");
1220        let id = ctx.add(inner);
1221        self.inner_id = Some(id);
1222        vec![id]
1223    }
1224
1225    fn layout_response(&self, proposal: SizeProposal, ctx: &LayoutContext) -> LayoutResponse {
1226        self.inner_id
1227            .and_then(|id| ctx.child_size(id, proposal))
1228            .unwrap_or_else(|| proposal.resolve(0.0, 0.0))
1229            .into()
1230    }
1231
1232    fn place_children(
1233        &self,
1234        bounds: Rect,
1235        _proposal: SizeProposal,
1236        children: &mut [WidgetPlacement],
1237        _ctx: &LayoutContext,
1238    ) {
1239        if let Some(child) = children.first_mut() {
1240            child.origin = Point::new(bounds.x, bounds.y);
1241            child.size = Size::new(bounds.width, bounds.height);
1242        }
1243    }
1244
1245    fn children(&self) -> Vec<WidgetId> {
1246        self.inner_id.into_iter().collect()
1247    }
1248
1249    fn accessibility_proxy(&self) -> Option<WidgetId> {
1250        // The inner editor stands for this one, and its body for it in turn.
1251        self.inner_id
1252    }
1253}