1mod 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
81pub(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 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 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 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 for sig in [&st.scroll_x, &st.scroll_y] {
136 sig.bind_to(self_id, registry, BindingLevel::RepaintOnly);
137 }
138 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 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 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 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 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 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 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 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 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 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 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 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 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 a11y::build_editor_a11y(&st, builder);
377
378 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
409pub(crate) fn construct(
415 document: TextDocument,
416 policy: PolicyBundle,
417 config: CodeConfig,
418 wrap_mode: WrapMode,
419) -> SharedState {
420 let mut engine = RichTextEngine::private_default();
425 engine.set_wrap_mode(wrap_mode);
426 CodeEditorState::new(document, engine, policy, config, wrap_mode)
429}
430
431pub(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
450pub(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
463pub(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 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 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#[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 pub fn cursor_position(&self) -> usize {
538 self.state.borrow().cursor.position()
539 }
540
541 pub fn cursor_position_signal(&self) -> teksilo_core::Signal<usize> {
545 self.state.borrow().cursor_position.clone()
546 }
547
548 pub fn caret_count(&self) -> teksilo_core::Signal<usize> {
550 self.state.borrow().caret_count.clone()
551 }
552
553 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 pub fn undo(&self) {
576 let st = self.state.borrow();
577 let _ = st.document.undo();
578 }
579
580 pub fn redo(&self) {
582 let st = self.state.borrow();
583 let _ = st.document.redo();
584 }
585
586 pub fn copy(&self, ctx: &teksilo_core::widget::EventContext<'_>) {
588 clipboard::copy(&self.state.borrow(), ctx);
589 }
590
591 pub fn cut(&self, ctx: &teksilo_core::widget::EventContext<'_>) {
593 clipboard::cut(&mut self.state.borrow_mut(), ctx);
594 }
595
596 pub fn paste(&self, ctx: &teksilo_core::widget::EventContext<'_>) {
598 clipboard::paste(&mut self.state.borrow_mut(), ctx);
599 }
600
601 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 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 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
632const _: () = {
634 fn _assert_shared(_: &Rc<std::cell::RefCell<CodeEditorState>>) {}
635};
636
637impl 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 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}