Skip to main content

teksilo_widgets/
code_editor.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Multi-line plain-text and code editing surfaces.
5//!
6//! Three faces over one core:
7//!
8//! - `CodeEditor` — a source editor: gutter, current-line highlight,
9//!   indentation, bracket handling, multiple carets.
10//! - `PlainTextEditor` — the same core with the code affordances off and
11//!   wrapping on: a notes field, a commit message, a description box.
12//! - `LogView` — read-only, append-only, tail-following.
13//!
14//! They are one implementation because they differ in *configuration*, not in
15//! kind. All three are a monospaced-or-not run of lines with a caret in it; a
16//! separate widget per face would triplicate the caret, selection, IME,
17//! clipboard, scrolling, and accessibility and let them drift.
18//!
19//! # Why not `RichTextEditor`
20//!
21//! `RichTextEditor` already edits multi-line text, and this deliberately does
22//! not build on it. Its command vocabulary is tables, lists, blockquotes, and
23//! bold — reusing it would put Tab-navigates-a-table-cell and
24//! Ctrl+B-emboldens into a source file, where the first is wrong and the second
25//! is meaningless. Its state carries a table-aware Ctrl+A ladder and a rich
26//! clipboard fragment; this one carries an indent policy and a caret vector.
27//! The overlap is real but it is the *clock* — the caret blink, the debounce
28//! window, the scroll arithmetic — and that lives in the crate-internal
29//! `common::editor_runtime`, shared by both.
30//!
31//! # Language-agnostic by construction
32//!
33//! There is no `Language` enum here. Comment tokens, bracket pairs, indent
34//! width, and highlighting are [`CodeConfig`] values the application supplies:
35//! the editor knows how to toggle a line comment, not that Rust uses `//`.
36//! Guessing would be worse than not knowing — inserting `//` into a Python file
37//! corrupts it silently.
38
39mod a11y;
40mod clipboard;
41mod completion;
42mod config;
43mod context_menu;
44mod frame_loop;
45mod gutter;
46mod keyboard;
47mod log_stream;
48mod log_view;
49mod mouse;
50mod policy;
51mod semantics;
52mod state;
53mod touch;
54mod widget;
55
56#[cfg(test)]
57mod tests;
58#[cfg(test)]
59mod touch_tests;
60
61pub use completion::{CompletionContext, CompletionItem, CompletionKind};
62pub use config::{BracketPair, COMMON_BRACKETS, CodeConfig, IndentStyle};
63pub use log_view::{LogView, LogViewHandle};
64pub use policy::{CODE_EDITOR_PRESET, CODE_READ_ONLY_PRESET, CodeCommand};
65pub use widget::{CodeEditor, PlainTextEditor};
66
67use std::rc::Rc;
68
69use teksilo_canvas::{Canvas, Point, Rect, Size, SizeProposal};
70use teksilo_core::accessibility::AccessNodeBuilder;
71use teksilo_core::build_context::BuildContext;
72use teksilo_core::widget::{LayoutContext, PaintContext, Widget, WidgetPlacement};
73use teksilo_core::widget_id::WidgetId;
74use teksilo_text::text_document::TextDocument;
75use teksilo_text::{RichTextEngine, SharedTypesetter, WrapMode};
76
77use self::state::{CodeEditorState, SharedState};
78use crate::common::editor_runtime::PolicyBundle;
79use crate::rich_text::paint::{PaintParams, paint_frame};
80
81/// The paint-only leaf that renders the document.
82///
83/// Split from the wrapper for the same reason the rich text editor is: the
84/// wrapper owns focus, handlers, and style-supplied chrome, so the body can be
85/// a pure leaf that an application's custom style may place anywhere inside its
86/// decoration without the focus semantics moving with it. The two are joined
87/// only by the shared state — neither holds a reference to the other.
88pub(crate) struct CodeEditorBody {
89    state: SharedState,
90    min_lines: Option<u32>,
91    max_lines: Option<u32>,
92}
93
94impl std::fmt::Debug for CodeEditorBody {
95    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        f.debug_struct("CodeEditorBody")
97            .field("policy", &self.state.borrow().policy)
98            .finish_non_exhaustive()
99    }
100}
101
102impl Widget for CodeEditorBody {
103    fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
104        use teksilo_core::binding::BindingLevel;
105
106        let self_id = ctx.self_id();
107        let registry = ctx.binding_registry();
108
109        let st = self.state.borrow();
110
111        // The caret is painted here, so its every toggle must mark *this* node
112        // for repaint. Skipped when the policy never draws one.
113        if st.policy.caret_policy != crate::common::editor_runtime::CaretPolicy::Hidden {
114            st.caret_visible
115                .bind_to(self_id, registry, BindingLevel::RepaintOnly);
116        }
117
118        // An edit must both repaint and re-walk the accessibility tree — this
119        // body is the node carrying the role and the paragraph/run children.
120        st.document_version
121            .bind_to(self_id, registry, BindingLevel::AccessibilityOnly);
122        st.document_version
123            .bind_to(self_id, registry, BindingLevel::RepaintOnly);
124
125        // The completion popup's open/selection state rides on this node's a11y
126        // (expanded / controls / active_descendant), so re-walk when it changes.
127        st.completion
128            .open
129            .bind_to(self_id, registry, BindingLevel::AccessibilityOnly);
130        st.completion
131            .selected
132            .bind_to(self_id, registry, BindingLevel::AccessibilityOnly);
133
134        // Scroll never changes the AT tree, so it is repaint-only.
135        for sig in [&st.scroll_x, &st.scroll_y] {
136            sig.bind_to(self_id, registry, BindingLevel::RepaintOnly);
137        }
138        // Caret and selection are repaint-only for geometry — they never change
139        // this widget's size — but they ALSO change what the a11y walk reports
140        // via `set_text_selection_to`. A caret-only move (arrow key, click,
141        // drag-select) emits no document event, so `document_version` never
142        // bumps; without an `AccessibilityOnly` binding here `a11y_dirty` would
143        // never flip and a screen reader would hear the caret frozen at the last
144        // edit. Binding one signal at two levels is the same pattern
145        // `document_version` uses above. `has_selection` is derived from the
146        // caret and anchor, so binding those two covers every selection change.
147        st.cursor_position
148            .bind_to(self_id, registry, BindingLevel::RepaintOnly);
149        st.cursor_position
150            .bind_to(self_id, registry, BindingLevel::AccessibilityOnly);
151        st.cursor_anchor
152            .bind_to(self_id, registry, BindingLevel::RepaintOnly);
153        st.cursor_anchor
154            .bind_to(self_id, registry, BindingLevel::AccessibilityOnly);
155        st.has_selection
156            .bind_to(self_id, registry, BindingLevel::RepaintOnly);
157        // A caret added or removed changes what is drawn but not the layout.
158        st.caret_count
159            .bind_to(self_id, registry, BindingLevel::RepaintOnly);
160
161        Vec::new()
162    }
163
164    fn layout_response(
165        &self,
166        proposal: SizeProposal,
167        ctx: &LayoutContext,
168    ) -> teksilo_core::widget::LayoutResponse {
169        let w = proposal.width.unwrap_or(200.0).max(0.0);
170
171        // Greedy: fill whatever we are given. The editor is normally the
172        // scrollable region of a pane, so it takes the space and scrolls.
173        if self.min_lines.is_none() && self.max_lines.is_none() {
174            let h = proposal.height.unwrap_or(100.0).max(0.0);
175            return Size::new(w, h).into();
176        }
177
178        // Intrinsic: size to content, clamped to [min_lines, max_lines] — the
179        // composer pattern, where the field grows with what is typed until it
180        // is allowed to grow no further and starts scrolling.
181        let st = self.state.borrow();
182        let line_scale = if st.follow_text_scale {
183            ctx.text_scale
184        } else {
185            1.0
186        };
187        let line_h = st.engine.default_line_height() * line_scale;
188        let content_h = st.engine.content_height();
189        drop(st);
190
191        let min_h = self.min_lines.map(|n| n as f32 * line_h).unwrap_or(0.0);
192        let max_h = self
193            .max_lines
194            .map(|n| n as f32 * line_h)
195            .unwrap_or(f32::INFINITY);
196        Size::new(w, content_h.clamp(min_h, max_h).max(0.0)).into()
197    }
198
199    fn place_children(
200        &self,
201        bounds: Rect,
202        _proposal: SizeProposal,
203        _children: &mut [WidgetPlacement],
204        _ctx: &LayoutContext,
205    ) {
206        // A leaf, but layout runs before paint, so this is the earliest — and
207        // therefore authoritative — point at which the viewport is known.
208        self.state.borrow_mut().sync_viewport(bounds);
209    }
210
211    fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
212        use crate::common::editor_runtime::CaretPolicy;
213
214        let mut st = self.state.borrow_mut();
215
216        // Resolve the app's colour overrides against the live theme each paint,
217        // so a theme swap or a Signal-bound colour reaches the glyphs. A
218        // changed colour forces a full render because the cached glyph quads
219        // have the old colour baked in.
220        let new_text = match &st.text_color_prop {
221            Some(p) => p.resolve(ctx.theme, true).to_array(),
222            None => ctx.theme.colors.editor_fg.to_array(),
223        };
224        st.engine.set_text_color(new_text);
225        if st.last_text_color != Some(new_text) {
226            st.last_text_color = Some(new_text);
227            st.pending_full_render = true;
228        }
229
230        let new_caret = match &st.caret_color_prop {
231            Some(p) => p.resolve(ctx.theme, true).to_array(),
232            None => ctx.theme.colors.editor_caret.to_array(),
233        };
234        st.engine.set_cursor_color(new_caret);
235        if st.last_cursor_color != Some(new_caret) {
236            st.last_cursor_color = Some(new_caret);
237            st.pending_full_render = true;
238        }
239
240        // Selection desaturates in an inactive window unless the app pinned a
241        // colour — the same convention every desktop selection follows.
242        let new_sel = if let Some(p) = st.selection_color_prop.as_ref() {
243            p.resolve(ctx.theme, true).to_array()
244        } else if ctx.window_active {
245            ctx.theme.colors.editor_selection_bg.to_array()
246        } else {
247            ctx.theme.colors.selection_bg_inactive.to_array()
248        };
249        if st.last_selection_color != Some(new_sel) {
250            st.engine.set_selection_color(new_sel);
251            st.last_selection_color = Some(new_sel);
252            st.pending_full_render = true;
253        }
254
255        // Logical font scale: a11y × per-editor `font_size_scale`. Changes
256        // glyph advances, so it forces a relayout, not just a re-render.
257        let target_scale = st.effective_font_scale(ctx.text_scale);
258        if st.last_font_scale.is_nan() || (st.last_font_scale - target_scale).abs() > f32::EPSILON {
259            st.last_font_scale = target_scale;
260            st.engine.set_font_scale(target_scale);
261            st.needs_full_layout = true;
262            st.pending_full_render = true;
263        }
264
265        // Idempotent echo of place_children, which already adopted these exact
266        // bounds. Kept because paint is reachable on a first frame where layout
267        // has run but the frame loop has not yet ticked.
268        st.sync_viewport(bounds);
269
270        let did_full_layout = st.needs_full_layout || !st.engine.has_full_layout();
271        if did_full_layout {
272            let flow = st.document.snapshot_flow();
273            st.engine.layout_full(&flow);
274            st.needs_full_layout = false;
275            st.content_dirty = true;
276        }
277
278        // The viewport got smaller since the last frame — a window resize, a
279        // pane opening, the on-screen keyboard rising under a focused editor —
280        // and the caret may now be outside it. `sync_viewport` recorded the
281        // shrink (it is the only place that sees both sizes); here, with the
282        // relayout it forced already run, is the earliest point the reveal can
283        // be computed against real geometry.
284        if std::mem::take(&mut st.pending_caret_reveal) {
285            super::code_editor::keyboard::ensure_caret_visible_locked(&mut st);
286        }
287
288        let caret_on = match st.policy.caret_policy {
289            CaretPolicy::Hidden => false,
290            CaretPolicy::StaticVisible => st.has_focus && st.window_active,
291            CaretPolicy::Blinking => st.caret_visible.get() && st.has_focus && st.window_active,
292        };
293
294        // Publish every caret to the engine in one call. A single-caret editor
295        // is just the one-element case, so there is no second code path to keep
296        // in step.
297        let cursors: Vec<teksilo_text::CursorDisplay> = st
298            .all_carets()
299            .map(|c| teksilo_text::CursorDisplay {
300                position: c.position(),
301                anchor: c.anchor(),
302                affinity: st.cursor_affinity,
303                visible: caret_on,
304                selected_cells: Vec::new(),
305            })
306            .collect();
307        st.engine.set_cursors(&cursors);
308
309        let scroll_y = st.scroll_y.get();
310        st.engine.set_scroll_offset(scroll_y);
311
312        // Cull the render to the visible clip band when the editor is laid out at
313        // full document height inside an outer scroller (opt-in, off by default).
314        // `clip_bounds` is the on-screen slice an ancestor clip leaves visible; we
315        // map it into content space and render only that band (plus a half-viewport
316        // margin), the same window the rich text editor uses. Never moves glyph
317        // positions or hit-testing — it only limits what is emitted.
318        let render_window = if st.window_to_clip {
319            ctx.clip_bounds.map(|clip| {
320                let vis_top = (scroll_y + (clip.y - bounds.y)).max(0.0);
321                let vis_h = clip.height.max(0.0);
322                let margin = vis_h * 0.5;
323                ((vis_top - margin).max(0.0), vis_h + 2.0 * margin)
324            })
325        } else {
326            None
327        };
328        st.engine.set_render_window(render_window);
329
330        canvas.set_clip(bounds);
331
332        let pending_full = std::mem::replace(&mut st.pending_full_render, false);
333        let block_relayout = st.last_relayout_block_id.take();
334
335        let state_ref: &mut CodeEditorState = &mut st;
336        let CodeEditorState {
337            ref mut engine,
338            ref document,
339            ref mut image_cache,
340            ..
341        } = *state_ref;
342        let paint_closure = |frame: &teksilo_text::RenderFrame| {
343            paint_frame(
344                canvas,
345                PaintParams {
346                    frame,
347                    origin: Point::new(bounds.x, bounds.y),
348                    document,
349                    image_cache,
350                    // No inline images on this surface, so none can be missing.
351                    image_resolver: None,
352                    selection: None,
353                    selection_color: [0.0; 4],
354                    selected_image_out: None,
355                    resize_preview: None,
356                    draw_caret: caret_on,
357                },
358            );
359        };
360        if did_full_layout || pending_full {
361            engine.with_render_frame(paint_closure);
362        } else if let Some(bid) = block_relayout {
363            engine.with_render_block_only(bid, paint_closure);
364        } else {
365            engine.with_render_cursor_only(paint_closure);
366        }
367
368        canvas.clear_clip();
369    }
370
371    fn accessibility(&self, builder: &mut AccessNodeBuilder) {
372        let st = self.state.borrow();
373
374        // Role, read-only, the paragraph/run tree, selection reporting, and the
375        // text actions — the walk shared with the log view (full-document here).
376        a11y::build_editor_a11y(&st, builder);
377
378        // Completion popup — the ARIA combobox-with-listbox pattern (as ComboBox
379        // and SearchField): the editor keeps focus and carries has-popup +
380        // autocomplete, announces expanded (both branches, so it never sticks
381        // open), and — only while shown, or a stale reference can crash a screen
382        // reader — points controls at the listbox and active-descendant at the
383        // highlighted row.
384        if st.completion.has_provider() {
385            use teksilo_core::accessibility::widget_id_to_node_id;
386            use teksilo_core::accesskit::{AutoComplete, HasPopup};
387
388            let inner = builder.inner_mut();
389            inner.set_has_popup(HasPopup::Listbox);
390            inner.set_auto_complete(AutoComplete::List);
391            let open = st.completion.is_open();
392            inner.set_expanded(open);
393            if open {
394                if let Some(pid) = st.completion.panel_id {
395                    inner.push_controlled(widget_id_to_node_id(pid));
396                }
397                if let Some(row) = st.completion.active_row.get() {
398                    inner.set_active_descendant(widget_id_to_node_id(row));
399                }
400            }
401        }
402    }
403
404    fn clips_children(&self) -> bool {
405        true
406    }
407}
408
409/// Shared construction for every face of the editor.
410///
411/// Returns the state handle; the public builders wrap it. Keeping this one
412/// function is what makes `CodeEditor` / `PlainTextEditor` / `LogView`
413/// genuinely the same core rather than three that merely look alike.
414pub(crate) fn construct(
415    document: TextDocument,
416    policy: PolicyBundle,
417    config: CodeConfig,
418    wrap_mode: WrapMode,
419) -> SharedState {
420    // A private engine to begin with. `build()` swaps in one sharing the
421    // application's typesetter when there is one, so glyphs land in the atlas
422    // the renderer uploads; headless tests have no typesetter and the private
423    // engine is then exactly right, since no renderer is ever invoked.
424    let mut engine = RichTextEngine::private_default();
425    engine.set_wrap_mode(wrap_mode);
426    // No hyphenation, ever: it is a prose affordance, and hyphenating source
427    // code would break identifiers across lines.
428    CodeEditorState::new(document, engine, policy, config, wrap_mode)
429}
430
431/// Swap the private engine for one sharing the application's typesetter.
432///
433/// Called from the wrapper's `build`. Outside a windowed app there is no
434/// typesetter and the private engine stays, which is why the headless tests
435/// exercise the same paths.
436pub(crate) fn adopt_shared_typesetter(state: &SharedState, ctx: &mut BuildContext) {
437    let Some(shared) = ctx.app_state::<SharedTypesetter>() else {
438        return;
439    };
440    let mut st = state.borrow_mut();
441    let wrap = st.wrap_mode;
442    let typography = st.engine.typography_defaults().clone();
443    let mut engine = RichTextEngine::from_shared(shared.clone());
444    engine.set_wrap_mode(wrap);
445    engine.set_typography_defaults(typography);
446    st.engine = engine;
447    st.needs_full_layout = true;
448}
449
450/// Build the paint-only body for a state handle.
451pub(crate) fn body_for(
452    state: &SharedState,
453    min_lines: Option<u32>,
454    max_lines: Option<u32>,
455) -> CodeEditorBody {
456    CodeEditorBody {
457        state: state.clone(),
458        min_lines,
459        max_lines,
460    }
461}
462
463/// Publish cursor state onto the reactive signals.
464///
465/// Every mutating path ends here. The signals are written *after* the state
466/// borrow is dropped: `Signal::set` fans out to observers synchronously, and an
467/// observer that reaches back into the widget would panic on a live borrow.
468pub(crate) fn sync_cursor_signals(state: &SharedState) {
469    let mut st = state.borrow_mut();
470    let pos = st.cursor.position();
471    let anchor = st.cursor.anchor();
472    let has_sel = st.all_carets().any(|c| c.has_selection());
473    let count = 1 + st.extra_carets.len();
474
475    let pos_sig = st.cursor_position.clone();
476    let anchor_sig = st.cursor_anchor.clone();
477    let sel_sig = st.has_selection.clone();
478    let count_sig = st.caret_count.clone();
479    let caret_vis = st.caret_visible.clone();
480
481    // Recompute the bracket match at the single choke point every caret move
482    // passes through — but only when the app asked for it, so a plain-text
483    // editor or a document with no configured pairs pays nothing. The scan reads
484    // the document while it is borrowed here; the resulting signal is written
485    // after the borrow drops, with the rest.
486    let bracket_sig = st.bracket_match.clone();
487    let bracket_val = if st.config.match_brackets {
488        semantics::current_bracket_match(&st)
489    } else {
490        None
491    };
492
493    // Restart the blink so the caret stays lit through a held arrow key rather
494    // than toggling mid-motion. `restart` deliberately does not write the
495    // signal — see its docs — so the caller does, below, outside the borrow.
496    let blink_reset = st.has_focus
497        && matches!(
498            st.policy.caret_policy,
499            crate::common::editor_runtime::CaretPolicy::Blinking
500        );
501    if blink_reset {
502        st.blink.restart();
503    }
504    drop(st);
505
506    pos_sig.set_if_changed(pos);
507    anchor_sig.set_if_changed(anchor);
508    sel_sig.set_if_changed(has_sel);
509    count_sig.set_if_changed(count);
510    bracket_sig.set_if_changed(bracket_val);
511    if blink_reset {
512        caret_vis.set_if_changed(true);
513    }
514}
515
516/// A handle onto a live editor, cloneable and detachable from the widget.
517///
518/// The `EditorHandle` pattern: an app keeps one to drive the editor from a
519/// toolbar, a shortcut, or a test without holding the widget itself.
520#[derive(Clone)]
521pub struct CodeEditorHandle {
522    state: SharedState,
523}
524
525impl std::fmt::Debug for CodeEditorHandle {
526    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
527        f.debug_struct("CodeEditorHandle").finish_non_exhaustive()
528    }
529}
530
531impl CodeEditorHandle {
532    pub(crate) fn new(state: SharedState) -> Self {
533        Self { state }
534    }
535
536    /// The caret's document position.
537    pub fn cursor_position(&self) -> usize {
538        self.state.borrow().cursor.position()
539    }
540
541    /// The primary caret's document position — a character offset into the whole
542    /// document, not a line or column — as a reactive signal. Bind it in a status
543    /// bar to show a caret position that tracks every caret move, not only edits.
544    pub fn cursor_position_signal(&self) -> teksilo_core::Signal<usize> {
545        self.state.borrow().cursor_position.clone()
546    }
547
548    /// Live caret count — `1` unless multi-caret editing is active.
549    pub fn caret_count(&self) -> teksilo_core::Signal<usize> {
550        self.state.borrow().caret_count.clone()
551    }
552
553    /// The bracket next to the caret and its match, as document positions, or
554    /// `None`. Populated only when the editor was configured with
555    /// `match_brackets` and bracket pairs; a status surface can bind it, or an
556    /// app can read it to drive its own overlay.
557    pub fn bracket_match(&self) -> teksilo_core::Signal<Option<(usize, usize)>> {
558        self.state.borrow().bracket_match.clone()
559    }
560
561    pub fn has_selection(&self) -> teksilo_core::Signal<bool> {
562        self.state.borrow().has_selection.clone()
563    }
564
565    pub fn can_undo(&self) -> teksilo_core::Signal<bool> {
566        self.state.borrow().can_undo.clone()
567    }
568
569    /// Undo this editor's last edit.
570    ///
571    /// The handle could report [`can_undo`](Self::can_undo) long before it could
572    /// *act* on it, which left a host able to light an Undo button here and
573    /// unable to make it do anything. Ctrl+Z inside the widget always worked;
574    /// this is the same command from outside.
575    pub fn undo(&self) {
576        let st = self.state.borrow();
577        let _ = st.document.undo();
578    }
579
580    /// Redo this editor's last undone edit.
581    pub fn redo(&self) {
582        let st = self.state.borrow();
583        let _ = st.document.redo();
584    }
585
586    /// Copy the selection to the clipboard.
587    pub fn copy(&self, ctx: &teksilo_core::widget::EventContext<'_>) {
588        clipboard::copy(&self.state.borrow(), ctx);
589    }
590
591    /// Cut the selection to the clipboard.
592    pub fn cut(&self, ctx: &teksilo_core::widget::EventContext<'_>) {
593        clipboard::cut(&mut self.state.borrow_mut(), ctx);
594    }
595
596    /// Paste over the selection.
597    pub fn paste(&self, ctx: &teksilo_core::widget::EventContext<'_>) {
598        clipboard::paste(&mut self.state.borrow_mut(), ctx);
599    }
600
601    /// Select the whole document.
602    pub fn select_all(&self) {
603        let st = self.state.borrow();
604        st.cursor
605            .select(teksilo_text::text_document::SelectionType::Document);
606    }
607
608    /// Is this editor refusing edits?
609    pub fn is_read_only(&self) -> bool {
610        self.state.borrow().policy.is_read_only()
611    }
612
613    pub fn can_redo(&self) -> teksilo_core::Signal<bool> {
614        self.state.borrow().can_redo.clone()
615    }
616
617    /// Bumps on every content or format change.
618    pub fn document_version(&self) -> teksilo_core::Signal<u64> {
619        self.state.borrow().document_version.clone()
620    }
621
622    pub fn scroll_y(&self) -> teksilo_core::Signal<f32> {
623        self.state.borrow().scroll_y.clone()
624    }
625
626    #[cfg(test)]
627    pub(crate) fn state_handle(&self) -> SharedState {
628        self.state.clone()
629    }
630}
631
632/// Keep `Rc` in the import set for the state alias.
633const _: () = {
634    fn _assert_shared(_: &Rc<std::cell::RefCell<CodeEditorState>>) {}
635};
636
637// ── The framework's uniform view of a text-editing widget ────────────────────
638
639impl teksilo_core::text_surface::TextSurface for CodeEditorHandle {
640    fn can_undo(&self) -> bool {
641        CodeEditorHandle::can_undo(self).get()
642    }
643
644    fn can_redo(&self) -> bool {
645        CodeEditorHandle::can_redo(self).get()
646    }
647
648    fn undo(&self) {
649        CodeEditorHandle::undo(self);
650    }
651
652    fn redo(&self) {
653        CodeEditorHandle::redo(self);
654    }
655
656    fn has_selection(&self) -> bool {
657        CodeEditorHandle::has_selection(self).get()
658    }
659
660    fn is_read_only(&self) -> bool {
661        CodeEditorHandle::is_read_only(self)
662    }
663
664    fn allows_copy(&self) -> bool {
665        self.state.borrow().policy.clipboard_policy.allows_copy()
666    }
667
668    fn history_frozen(&self) -> bool {
669        !self
670            .state
671            .borrow()
672            .policy
673            .command_filter
674            .accepts(policy::CodeCommand::Undo)
675    }
676
677    fn cut(&self, ctx: &teksilo_core::widget::EventContext<'_>) {
678        CodeEditorHandle::cut(self, ctx);
679    }
680
681    fn copy(&self, ctx: &teksilo_core::widget::EventContext<'_>) {
682        CodeEditorHandle::copy(self, ctx);
683    }
684
685    fn paste(&self, ctx: &teksilo_core::widget::EventContext<'_>) {
686        CodeEditorHandle::paste(self, ctx);
687    }
688
689    /// Code has no rich formatting to strip; the plain paste is the paste.
690    fn paste_plain(&self, ctx: &teksilo_core::widget::EventContext<'_>) {
691        CodeEditorHandle::paste(self, ctx);
692    }
693
694    fn select_all(&self) {
695        CodeEditorHandle::select_all(self);
696    }
697}