1mod a11y;
40mod clipboard;
41mod completion;
42mod config;
43mod frame_loop;
44mod gutter;
45mod keyboard;
46mod log_stream;
47mod log_view;
48mod mouse;
49mod policy;
50mod semantics;
51mod state;
52mod widget;
53
54#[cfg(test)]
55mod tests;
56
57pub use completion::{CompletionContext, CompletionItem, CompletionKind};
58pub use config::{BracketPair, COMMON_BRACKETS, CodeConfig, IndentStyle};
59pub use log_view::{LogView, LogViewHandle};
60pub use policy::{CODE_EDITOR_PRESET, CODE_READ_ONLY_PRESET, CodeCommand};
61pub use widget::{CodeEditor, PlainTextEditor};
62
63use std::rc::Rc;
64
65use teksilo_canvas::{Canvas, Point, Rect, Size, SizeProposal};
66use teksilo_core::accessibility::AccessNodeBuilder;
67use teksilo_core::build_context::BuildContext;
68use teksilo_core::widget::{LayoutContext, PaintContext, Widget, WidgetPlacement};
69use teksilo_core::widget_id::WidgetId;
70use teksilo_text::text_document::TextDocument;
71use teksilo_text::{RichTextEngine, SharedTypesetter, WrapMode};
72
73use self::state::{CodeEditorState, SharedState};
74use crate::common::editor_runtime::PolicyBundle;
75use crate::rich_text::paint::{PaintParams, paint_frame};
76
77pub(crate) struct CodeEditorBody {
85 state: SharedState,
86 min_lines: Option<u32>,
87 max_lines: Option<u32>,
88}
89
90impl std::fmt::Debug for CodeEditorBody {
91 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92 f.debug_struct("CodeEditorBody")
93 .field("policy", &self.state.borrow().policy)
94 .finish_non_exhaustive()
95 }
96}
97
98impl Widget for CodeEditorBody {
99 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
100 use teksilo_core::binding::BindingLevel;
101
102 let self_id = ctx.self_id();
103 let registry = ctx.binding_registry();
104
105 let st = self.state.borrow();
106
107 if st.policy.caret_policy != crate::common::editor_runtime::CaretPolicy::Hidden {
110 st.caret_visible
111 .bind_to(self_id, registry, BindingLevel::RepaintOnly);
112 }
113
114 st.document_version
117 .bind_to(self_id, registry, BindingLevel::AccessibilityOnly);
118 st.document_version
119 .bind_to(self_id, registry, BindingLevel::RepaintOnly);
120
121 st.completion
124 .open
125 .bind_to(self_id, registry, BindingLevel::AccessibilityOnly);
126 st.completion
127 .selected
128 .bind_to(self_id, registry, BindingLevel::AccessibilityOnly);
129
130 for sig in [&st.scroll_x, &st.scroll_y] {
132 sig.bind_to(self_id, registry, BindingLevel::RepaintOnly);
133 }
134 st.cursor_position
144 .bind_to(self_id, registry, BindingLevel::RepaintOnly);
145 st.cursor_position
146 .bind_to(self_id, registry, BindingLevel::AccessibilityOnly);
147 st.cursor_anchor
148 .bind_to(self_id, registry, BindingLevel::RepaintOnly);
149 st.cursor_anchor
150 .bind_to(self_id, registry, BindingLevel::AccessibilityOnly);
151 st.has_selection
152 .bind_to(self_id, registry, BindingLevel::RepaintOnly);
153 st.caret_count
155 .bind_to(self_id, registry, BindingLevel::RepaintOnly);
156
157 Vec::new()
158 }
159
160 fn layout_response(
161 &self,
162 proposal: SizeProposal,
163 ctx: &LayoutContext,
164 ) -> teksilo_core::widget::LayoutResponse {
165 let w = proposal.width.unwrap_or(200.0).max(0.0);
166
167 if self.min_lines.is_none() && self.max_lines.is_none() {
170 let h = proposal.height.unwrap_or(100.0).max(0.0);
171 return Size::new(w, h).into();
172 }
173
174 let st = self.state.borrow();
178 let line_scale = if st.follow_text_scale {
179 ctx.text_scale
180 } else {
181 1.0
182 };
183 let line_h = st.engine.default_line_height() * line_scale;
184 let content_h = st.engine.content_height();
185 drop(st);
186
187 let min_h = self.min_lines.map(|n| n as f32 * line_h).unwrap_or(0.0);
188 let max_h = self
189 .max_lines
190 .map(|n| n as f32 * line_h)
191 .unwrap_or(f32::INFINITY);
192 Size::new(w, content_h.clamp(min_h, max_h).max(0.0)).into()
193 }
194
195 fn place_children(
196 &self,
197 bounds: Rect,
198 _proposal: SizeProposal,
199 _children: &mut [WidgetPlacement],
200 _ctx: &LayoutContext,
201 ) {
202 self.state.borrow_mut().sync_viewport(bounds);
205 }
206
207 fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
208 use crate::common::editor_runtime::CaretPolicy;
209
210 let mut st = self.state.borrow_mut();
211
212 let new_text = match &st.text_color_prop {
217 Some(p) => p.resolve(ctx.theme, true).to_array(),
218 None => ctx.theme.colors.editor_fg.to_array(),
219 };
220 st.engine.set_text_color(new_text);
221 if st.last_text_color != Some(new_text) {
222 st.last_text_color = Some(new_text);
223 st.pending_full_render = true;
224 }
225
226 let new_caret = match &st.caret_color_prop {
227 Some(p) => p.resolve(ctx.theme, true).to_array(),
228 None => ctx.theme.colors.editor_caret.to_array(),
229 };
230 st.engine.set_cursor_color(new_caret);
231 if st.last_cursor_color != Some(new_caret) {
232 st.last_cursor_color = Some(new_caret);
233 st.pending_full_render = true;
234 }
235
236 let new_sel = if let Some(p) = st.selection_color_prop.as_ref() {
239 p.resolve(ctx.theme, true).to_array()
240 } else if ctx.window_active {
241 ctx.theme.colors.editor_selection_bg.to_array()
242 } else {
243 ctx.theme.colors.selection_bg_inactive.to_array()
244 };
245 if st.last_selection_color != Some(new_sel) {
246 st.engine.set_selection_color(new_sel);
247 st.last_selection_color = Some(new_sel);
248 st.pending_full_render = true;
249 }
250
251 let target_scale = st.effective_font_scale(ctx.text_scale);
254 if st.last_font_scale.is_nan() || (st.last_font_scale - target_scale).abs() > f32::EPSILON {
255 st.last_font_scale = target_scale;
256 st.engine.set_font_scale(target_scale);
257 st.needs_full_layout = true;
258 st.pending_full_render = true;
259 }
260
261 st.sync_viewport(bounds);
265
266 let did_full_layout = st.needs_full_layout || !st.engine.has_full_layout();
267 if did_full_layout {
268 let flow = st.document.snapshot_flow();
269 st.engine.layout_full(&flow);
270 st.needs_full_layout = false;
271 st.content_dirty = true;
272 }
273
274 let caret_on = match st.policy.caret_policy {
275 CaretPolicy::Hidden => false,
276 CaretPolicy::StaticVisible => st.has_focus && st.window_active,
277 CaretPolicy::Blinking => st.caret_visible.get() && st.has_focus && st.window_active,
278 };
279
280 let cursors: Vec<teksilo_text::CursorDisplay> = st
284 .all_carets()
285 .map(|c| teksilo_text::CursorDisplay {
286 position: c.position(),
287 anchor: c.anchor(),
288 affinity: st.cursor_affinity,
289 visible: caret_on,
290 selected_cells: Vec::new(),
291 })
292 .collect();
293 st.engine.set_cursors(&cursors);
294
295 let scroll_y = st.scroll_y.get();
296 st.engine.set_scroll_offset(scroll_y);
297
298 let render_window = if st.window_to_clip {
305 ctx.clip_bounds.map(|clip| {
306 let vis_top = (scroll_y + (clip.y - bounds.y)).max(0.0);
307 let vis_h = clip.height.max(0.0);
308 let margin = vis_h * 0.5;
309 ((vis_top - margin).max(0.0), vis_h + 2.0 * margin)
310 })
311 } else {
312 None
313 };
314 st.engine.set_render_window(render_window);
315
316 canvas.set_clip(bounds);
317
318 let pending_full = std::mem::replace(&mut st.pending_full_render, false);
319 let block_relayout = st.last_relayout_block_id.take();
320
321 let state_ref: &mut CodeEditorState = &mut st;
322 let CodeEditorState {
323 ref mut engine,
324 ref document,
325 ref mut image_cache,
326 ..
327 } = *state_ref;
328 let paint_closure = |frame: &teksilo_text::RenderFrame| {
329 paint_frame(
330 canvas,
331 PaintParams {
332 frame,
333 origin: Point::new(bounds.x, bounds.y),
334 document,
335 image_cache,
336 image_resolver: None,
338 selection: None,
339 selection_color: [0.0; 4],
340 selected_image_out: None,
341 resize_preview: None,
342 draw_caret: caret_on,
343 },
344 );
345 };
346 if did_full_layout || pending_full {
347 engine.with_render_frame(paint_closure);
348 } else if let Some(bid) = block_relayout {
349 engine.with_render_block_only(bid, paint_closure);
350 } else {
351 engine.with_render_cursor_only(paint_closure);
352 }
353
354 canvas.clear_clip();
355 }
356
357 fn accessibility(&self, builder: &mut AccessNodeBuilder) {
358 let st = self.state.borrow();
359
360 a11y::build_editor_a11y(&st, builder);
363
364 if st.completion.has_provider() {
371 use teksilo_core::accessibility::widget_id_to_node_id;
372 use teksilo_core::accesskit::{AutoComplete, HasPopup};
373
374 let inner = builder.inner_mut();
375 inner.set_has_popup(HasPopup::Listbox);
376 inner.set_auto_complete(AutoComplete::List);
377 let open = st.completion.is_open();
378 inner.set_expanded(open);
379 if open {
380 if let Some(pid) = st.completion.panel_id {
381 inner.push_controlled(widget_id_to_node_id(pid));
382 }
383 if let Some(row) = st.completion.active_row.get() {
384 inner.set_active_descendant(widget_id_to_node_id(row));
385 }
386 }
387 }
388 }
389
390 fn clips_children(&self) -> bool {
391 true
392 }
393}
394
395pub(crate) fn construct(
401 document: TextDocument,
402 policy: PolicyBundle,
403 config: CodeConfig,
404 wrap_mode: WrapMode,
405) -> SharedState {
406 let mut engine = RichTextEngine::private_default();
411 engine.set_wrap_mode(wrap_mode);
412 CodeEditorState::new(document, engine, policy, config, wrap_mode)
415}
416
417pub(crate) fn adopt_shared_typesetter(state: &SharedState, ctx: &mut BuildContext) {
423 let Some(shared) = ctx.app_state::<SharedTypesetter>() else {
424 return;
425 };
426 let mut st = state.borrow_mut();
427 let wrap = st.wrap_mode;
428 let typography = st.engine.typography_defaults().clone();
429 let mut engine = RichTextEngine::from_shared(shared.clone());
430 engine.set_wrap_mode(wrap);
431 engine.set_typography_defaults(typography);
432 st.engine = engine;
433 st.needs_full_layout = true;
434}
435
436pub(crate) fn body_for(
438 state: &SharedState,
439 min_lines: Option<u32>,
440 max_lines: Option<u32>,
441) -> CodeEditorBody {
442 CodeEditorBody {
443 state: state.clone(),
444 min_lines,
445 max_lines,
446 }
447}
448
449pub(crate) fn sync_cursor_signals(state: &SharedState) {
455 let mut st = state.borrow_mut();
456 let pos = st.cursor.position();
457 let anchor = st.cursor.anchor();
458 let has_sel = st.all_carets().any(|c| c.has_selection());
459 let count = 1 + st.extra_carets.len();
460
461 let pos_sig = st.cursor_position.clone();
462 let anchor_sig = st.cursor_anchor.clone();
463 let sel_sig = st.has_selection.clone();
464 let count_sig = st.caret_count.clone();
465 let caret_vis = st.caret_visible.clone();
466
467 let bracket_sig = st.bracket_match.clone();
473 let bracket_val = if st.config.match_brackets {
474 semantics::current_bracket_match(&st)
475 } else {
476 None
477 };
478
479 let blink_reset = st.has_focus
483 && matches!(
484 st.policy.caret_policy,
485 crate::common::editor_runtime::CaretPolicy::Blinking
486 );
487 if blink_reset {
488 st.blink.restart();
489 }
490 drop(st);
491
492 pos_sig.set_if_changed(pos);
493 anchor_sig.set_if_changed(anchor);
494 sel_sig.set_if_changed(has_sel);
495 count_sig.set_if_changed(count);
496 bracket_sig.set_if_changed(bracket_val);
497 if blink_reset {
498 caret_vis.set_if_changed(true);
499 }
500}
501
502#[derive(Clone)]
507pub struct CodeEditorHandle {
508 state: SharedState,
509}
510
511impl std::fmt::Debug for CodeEditorHandle {
512 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
513 f.debug_struct("CodeEditorHandle").finish_non_exhaustive()
514 }
515}
516
517impl CodeEditorHandle {
518 pub(crate) fn new(state: SharedState) -> Self {
519 Self { state }
520 }
521
522 pub fn cursor_position(&self) -> usize {
524 self.state.borrow().cursor.position()
525 }
526
527 pub fn cursor_position_signal(&self) -> teksilo_core::Signal<usize> {
531 self.state.borrow().cursor_position.clone()
532 }
533
534 pub fn caret_count(&self) -> teksilo_core::Signal<usize> {
536 self.state.borrow().caret_count.clone()
537 }
538
539 pub fn bracket_match(&self) -> teksilo_core::Signal<Option<(usize, usize)>> {
544 self.state.borrow().bracket_match.clone()
545 }
546
547 pub fn has_selection(&self) -> teksilo_core::Signal<bool> {
548 self.state.borrow().has_selection.clone()
549 }
550
551 pub fn can_undo(&self) -> teksilo_core::Signal<bool> {
552 self.state.borrow().can_undo.clone()
553 }
554
555 pub fn undo(&self) {
562 let st = self.state.borrow();
563 let _ = st.document.undo();
564 }
565
566 pub fn redo(&self) {
568 let st = self.state.borrow();
569 let _ = st.document.redo();
570 }
571
572 pub fn copy(&self, ctx: &teksilo_core::widget::EventContext<'_>) {
574 clipboard::copy(&self.state.borrow(), ctx);
575 }
576
577 pub fn cut(&self, ctx: &teksilo_core::widget::EventContext<'_>) {
579 clipboard::cut(&mut self.state.borrow_mut(), ctx);
580 }
581
582 pub fn paste(&self, ctx: &teksilo_core::widget::EventContext<'_>) {
584 clipboard::paste(&mut self.state.borrow_mut(), ctx);
585 }
586
587 pub fn select_all(&self) {
589 let st = self.state.borrow();
590 st.cursor
591 .select(teksilo_text::text_document::SelectionType::Document);
592 }
593
594 pub fn is_read_only(&self) -> bool {
596 self.state.borrow().policy.is_read_only()
597 }
598
599 pub fn can_redo(&self) -> teksilo_core::Signal<bool> {
600 self.state.borrow().can_redo.clone()
601 }
602
603 pub fn document_version(&self) -> teksilo_core::Signal<u64> {
605 self.state.borrow().document_version.clone()
606 }
607
608 pub fn scroll_y(&self) -> teksilo_core::Signal<f32> {
609 self.state.borrow().scroll_y.clone()
610 }
611
612 #[cfg(test)]
613 pub(crate) fn state_handle(&self) -> SharedState {
614 self.state.clone()
615 }
616}
617
618const _: () = {
620 fn _assert_shared(_: &Rc<std::cell::RefCell<CodeEditorState>>) {}
621};
622
623impl teksilo_core::text_surface::TextSurface for CodeEditorHandle {
626 fn can_undo(&self) -> bool {
627 CodeEditorHandle::can_undo(self).get()
628 }
629
630 fn can_redo(&self) -> bool {
631 CodeEditorHandle::can_redo(self).get()
632 }
633
634 fn undo(&self) {
635 CodeEditorHandle::undo(self);
636 }
637
638 fn redo(&self) {
639 CodeEditorHandle::redo(self);
640 }
641
642 fn has_selection(&self) -> bool {
643 CodeEditorHandle::has_selection(self).get()
644 }
645
646 fn is_read_only(&self) -> bool {
647 CodeEditorHandle::is_read_only(self)
648 }
649
650 fn allows_copy(&self) -> bool {
651 self.state.borrow().policy.clipboard_policy.allows_copy()
652 }
653
654 fn history_frozen(&self) -> bool {
655 !self
656 .state
657 .borrow()
658 .policy
659 .command_filter
660 .accepts(policy::CodeCommand::Undo)
661 }
662
663 fn cut(&self, ctx: &teksilo_core::widget::EventContext<'_>) {
664 CodeEditorHandle::cut(self, ctx);
665 }
666
667 fn copy(&self, ctx: &teksilo_core::widget::EventContext<'_>) {
668 CodeEditorHandle::copy(self, ctx);
669 }
670
671 fn paste(&self, ctx: &teksilo_core::widget::EventContext<'_>) {
672 CodeEditorHandle::paste(self, ctx);
673 }
674
675 fn paste_plain(&self, ctx: &teksilo_core::widget::EventContext<'_>) {
677 CodeEditorHandle::paste(self, ctx);
678 }
679
680 fn select_all(&self) {
681 CodeEditorHandle::select_all(self);
682 }
683}