1use std::cell::Cell;
19use std::rc::Rc;
20
21use teksilo_canvas::{Canvas, Point, Rect, Size, SizeProposal};
22use teksilo_core::accessibility::AccessNodeBuilder;
23use teksilo_core::binding::BindingLevel;
24use teksilo_core::build_context::BuildContext;
25use teksilo_core::widget::{
26 CursorIcon, LayoutContext, LayoutResponse, PaintContext, Widget, WidgetPlacement,
27};
28use teksilo_core::widget_builder::HandlerSet;
29use teksilo_core::widget_id::WidgetId;
30use teksilo_text::text_document::TextDocument;
31use teksilo_text::{CursorAffinity, WrapMode};
32
33use super::completion::{self, CompletionContext, CompletionItem, CompletionPanel};
34use super::config::{BracketPair, CodeConfig, IndentStyle};
35use super::gutter::CodeGutter;
36use super::policy::{CODE_EDITOR_PRESET, CODE_READ_ONLY_PRESET};
37use super::state::SharedState;
38use super::{CodeEditorHandle, adopt_shared_typesetter, body_for, construct};
39use crate::common::editor_runtime::CaretPolicy;
40use crate::common::scroll::OverscrollBehavior;
41use crate::rich_text::ScrollPolicy;
42use crate::scroll_bar::{ScrollBar, ScrollBarOrientation, ScrollBarVariant};
43
44const SCROLLBAR_THICKNESS: f32 = 12.0;
46
47pub struct CodeEditor {
54 state: SharedState,
55 v_scroll_policy: ScrollPolicy,
56 h_scroll_policy: ScrollPolicy,
57 overscroll_behavior: OverscrollBehavior,
58 min_lines: Option<u32>,
59 max_lines: Option<u32>,
60 show_gutter: bool,
61
62 gutter_id: Option<WidgetId>,
64 body_id: Option<WidgetId>,
65 v_scrollbar_id: Option<WidgetId>,
66 h_scrollbar_id: Option<WidgetId>,
67 v_scrollbar_bounds: Rc<Cell<Rect>>,
70 h_scrollbar_bounds: Rc<Cell<Rect>>,
71 gutter_width: Rc<Cell<f32>>,
74}
75
76impl std::fmt::Debug for CodeEditor {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 f.debug_struct("CodeEditor")
79 .field("policy", &self.state.borrow().policy)
80 .field("show_gutter", &self.show_gutter)
81 .finish_non_exhaustive()
82 }
83}
84
85impl CodeEditor {
86 pub fn new(document: TextDocument) -> Self {
91 let this = Self::from_state(construct(
92 document,
93 CODE_EDITOR_PRESET,
94 CodeConfig::default(),
95 WrapMode::None,
96 ));
97 this.state.borrow_mut().current_line_highlight = true;
98 this
99 }
100
101 pub fn read_only(document: TextDocument) -> Self {
104 Self::from_state(construct(
105 document,
106 CODE_READ_ONLY_PRESET,
107 CodeConfig::default(),
108 WrapMode::None,
109 ))
110 }
111
112 fn from_state(state: SharedState) -> Self {
113 Self {
114 state,
115 v_scroll_policy: ScrollPolicy::Auto,
116 h_scroll_policy: ScrollPolicy::Auto,
117 overscroll_behavior: OverscrollBehavior::default(),
118 min_lines: None,
119 max_lines: None,
120 show_gutter: true,
121 gutter_id: None,
122 body_id: None,
123 v_scrollbar_id: None,
124 h_scrollbar_id: None,
125 v_scrollbar_bounds: Rc::new(Cell::new(Rect::ZERO)),
126 h_scrollbar_bounds: Rc::new(Cell::new(Rect::ZERO)),
127 gutter_width: Rc::new(Cell::new(0.0)),
128 }
129 }
130
131 pub fn wrap_mode(self, mode: WrapMode) -> Self {
137 {
138 let mut st = self.state.borrow_mut();
139 st.wrap_mode = mode;
140 st.engine.set_wrap_mode(mode);
141 st.needs_full_layout = true;
142 }
143 self
144 }
145
146 pub fn v_scroll_policy(mut self, policy: ScrollPolicy) -> Self {
148 self.v_scroll_policy = policy;
149 self
150 }
151
152 pub fn h_scroll_policy(mut self, policy: ScrollPolicy) -> Self {
154 self.h_scroll_policy = policy;
155 self
156 }
157
158 pub fn overscroll_behavior(mut self, behavior: OverscrollBehavior) -> Self {
161 self.overscroll_behavior = behavior;
162 self
163 }
164
165 pub fn window_to_clip(self, on: bool) -> Self {
172 self.state.borrow_mut().window_to_clip = on;
173 self
174 }
175
176 pub fn min_lines(mut self, lines: u32) -> Self {
180 self.min_lines = Some(lines);
181 self
182 }
183
184 pub fn max_lines(mut self, lines: u32) -> Self {
186 self.max_lines = Some(lines);
187 self
188 }
189
190 pub fn font_family(self, family: impl Into<String>) -> Self {
194 {
195 let mut st = self.state.borrow_mut();
196 let mut d = st.engine.typography_defaults().clone();
197 d.font_family = Some(family.into());
198 st.engine.set_typography_defaults(d);
199 st.needs_full_layout = true;
200 }
201 self
202 }
203
204 pub fn font_size_scale(self, scale: f32) -> Self {
208 {
209 let mut st = self.state.borrow_mut();
210 st.font_size_scale = scale.clamp(0.1, 10.0);
211 st.last_font_scale = f32::NAN;
212 st.needs_full_layout = true;
213 }
214 self
215 }
216
217 pub fn follow_text_scale(self, follow: bool) -> Self {
221 self.state.borrow_mut().follow_text_scale = follow;
222 self
223 }
224
225 pub fn on_change(self, callback: impl Fn() + 'static) -> Self {
227 self.state.borrow_mut().on_change = Some(Rc::new(callback));
228 self
229 }
230
231 pub fn background(self, color: impl Into<teksilo_core::color_prop::ColorProp>) -> Self {
234 self.state.borrow_mut().background_prop = Some(color.into());
235 self
236 }
237
238 pub fn text_color(self, color: impl Into<teksilo_core::color_prop::ColorProp>) -> Self {
240 self.state.borrow_mut().text_color_prop = Some(color.into());
241 self
242 }
243
244 pub fn caret_color(self, color: impl Into<teksilo_core::color_prop::ColorProp>) -> Self {
246 self.state.borrow_mut().caret_color_prop = Some(color.into());
247 self
248 }
249
250 pub fn selection_color(self, color: impl Into<teksilo_core::color_prop::ColorProp>) -> Self {
253 self.state.borrow_mut().selection_color_prop = Some(color.into());
254 self
255 }
256
257 pub fn gutter(mut self, show: bool) -> Self {
261 self.show_gutter = show;
262 self
263 }
264
265 pub fn current_line_highlight(self, on: bool) -> Self {
268 self.state.borrow_mut().current_line_highlight = on;
269 self
270 }
271
272 pub fn indent_style(self, style: IndentStyle) -> Self {
275 self.state.borrow_mut().config.indent = style;
276 self
277 }
278
279 pub fn tab_width(self, width: u8) -> Self {
281 {
282 let mut st = self.state.borrow_mut();
283 st.config.indent = match st.config.indent {
284 IndentStyle::Spaces(_) => IndentStyle::Spaces(width),
285 IndentStyle::Tabs { .. } => IndentStyle::Tabs { width },
286 };
287 }
288 self
289 }
290
291 pub fn use_soft_tabs(self, soft: bool) -> Self {
294 {
295 let mut st = self.state.borrow_mut();
296 let w = st.config.indent.width();
297 st.config.indent = if soft {
298 IndentStyle::Spaces(w)
299 } else {
300 IndentStyle::Tabs { width: w }
301 };
302 }
303 self
304 }
305
306 pub fn auto_indent(self, on: bool) -> Self {
309 self.state.borrow_mut().config.auto_indent = on;
310 self
311 }
312
313 pub fn bracket_pairs(self, pairs: impl Into<Vec<BracketPair>>) -> Self {
316 self.state.borrow_mut().config.brackets = pairs.into();
317 self
318 }
319
320 pub fn auto_close_brackets(self, on: bool) -> Self {
323 self.state.borrow_mut().config.auto_close_brackets = on;
324 self
325 }
326
327 pub fn bracket_matching(self, on: bool) -> Self {
330 self.state.borrow_mut().config.match_brackets = on;
331 self
332 }
333
334 pub fn line_comment(self, token: impl Into<String>) -> Self {
338 self.state.borrow_mut().config.line_comment = Some(token.into());
339 self
340 }
341
342 pub fn completion_provider(
348 self,
349 provider: impl Fn(&CompletionContext) -> Vec<CompletionItem> + 'static,
350 ) -> Self {
351 self.state.borrow_mut().completion.provider = Some(Rc::new(provider));
352 self
353 }
354
355 pub fn auto_complete(self, auto: bool) -> Self {
358 self.state.borrow_mut().completion.auto_trigger = auto;
359 self
360 }
361
362 pub fn handle(&self) -> CodeEditorHandle {
364 CodeEditorHandle::new(self.state.clone())
365 }
366
367 fn caret_line_band(st: &super::state::CodeEditorState) -> Option<(f32, f32)> {
372 if !st.engine.has_full_layout() {
373 return None;
374 }
375 let c = st
376 .engine
377 .caret_rect(st.cursor.position(), st.cursor_affinity);
378 let y = st.viewport_origin.y + c[1] - st.scroll_y.get();
379 Some((y, c[3]))
380 }
381}
382
383impl Widget for CodeEditor {
384 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
385 ctx.register_text_surface(std::rc::Rc::new(self.handle()));
389
390 adopt_shared_typesetter(&self.state, ctx);
393
394 {
395 let mut st = self.state.borrow_mut();
396 st.frame_request = Some(ctx.frame_request_handle());
397 st.frame_wake_at = Some(ctx.wake_at_handle());
398 st.self_id = Some(ctx.self_id());
399 }
400 let activation = ctx.activation_signal(ctx.self_id());
405 if activation.get() {
406 ctx.request_frame();
407 }
408
409 {
410 let state = self.state.clone();
411 ctx.effect(&activation, move |&active| {
412 if active {
413 let st = state.borrow();
427 if let Some(handle) = &st.frame_request {
428 handle.set(true);
429 }
430 return;
431 }
432 let mut st = state.borrow_mut();
433 if st.has_focus {
434 st.has_focus = false;
435 st.focus_signal.set_if_changed(false);
436 }
437 st.caret_visible.set_if_changed(false);
438 st.blink.reset();
439 });
440 }
441
442 {
446 let state = self.state.clone();
447 let active = activation.clone();
448 let tick_signal = ctx.frame_tick();
449 ctx.effect(&tick_signal, move |delta| {
450 if !active.get() {
451 return;
452 }
453 let mut st = state.borrow_mut();
454 let more = super::frame_loop::tick(&mut st, *delta);
455 if more && let Some(handle) = &st.frame_request {
456 handle.set(true);
457 }
458 });
459 }
460
461 {
465 let state = self.state.clone();
466 let active = activation.clone();
467 let wa_signal = ctx.window_active_signal();
468 ctx.effect(&wa_signal, move |&window_active| {
469 let mut st = state.borrow_mut();
470 st.window_active = window_active;
471 if window_active {
472 let show =
473 st.has_focus && !matches!(st.policy.caret_policy, CaretPolicy::Hidden);
474 if show {
475 st.caret_visible.set_if_changed(true);
476 }
477 st.blink.reset();
478 } else {
479 st.caret_visible.set_if_changed(false);
480 st.blink.reset();
481 }
482 if active.get()
483 && let Some(handle) = &st.frame_request
484 {
485 handle.set(true);
486 }
487 });
488 }
489
490 let mut handlers = HandlerSet::new();
492 if !self.state.borrow().policy.is_read_only() {
493 handlers = handlers.ime_input(teksilo_core::ime::ImeContext::text());
494 }
495 handlers = handlers
496 .focusable(true)
497 .cursor(CursorIcon::Text)
498 .on_focus({
499 let state = self.state.clone();
500 move |gained, ctx| {
501 {
502 let mut st = state.borrow_mut();
503 st.has_focus = gained;
504 st.focus_signal.set_if_changed(gained);
505 if gained && matches!(st.policy.caret_policy, CaretPolicy::Blinking) {
506 st.blink.restart();
507 st.caret_visible.set_if_changed(true);
508 }
509 }
510 if gained {
511 super::keyboard::report_ime_cursor_area(&state, ctx);
512 } else {
513 super::keyboard::clear_ime_preedit(&state);
514 completion::close(&state, ctx);
517 let mut st = state.borrow_mut();
518 st.last_ime_area = None;
519 st.last_chase_pos = None;
520 }
521 ctx.request_frame();
522 }
523 })
524 .on_pointer_event({
525 let state = self.state.clone();
526 let v_sb = self.v_scrollbar_bounds.clone();
527 let h_sb = self.h_scrollbar_bounds.clone();
528 move |event, ctx| {
529 super::mouse::handle_pointer_event(&state, &v_sb, &h_sb, event, ctx)
530 }
531 })
532 .on_scroll({
533 let state = self.state.clone();
534 let overscroll = self.overscroll_behavior;
535 move |event, ctx| super::mouse::handle_scroll(&state, overscroll, event, ctx)
536 })
537 .on_key({
538 let state = self.state.clone();
539 move |event, ctx| super::keyboard::handle_key(&state, event, ctx)
540 })
541 .on_double_tap({
542 let state = self.state.clone();
543 move |event, ctx| super::mouse::handle_double_tap(&state, event.position, ctx)
544 })
545 .on_triple_tap({
546 let state = self.state.clone();
547 move |event, ctx| super::mouse::handle_triple_tap(&state, event.position, ctx)
548 })
549 .on_access_action_request({
550 let state = self.state.clone();
551 move |action, target, data, ctx| {
552 super::a11y::handle_access_action(&state, action, target, data, ctx)
553 }
554 });
555 ctx.apply_self_handlers(handlers);
556
557 let body = body_for(&self.state, None, None);
560 let body_id = ctx.add(body);
561 self.body_id = Some(body_id);
562
563 {
567 let props = {
568 let st = self.state.borrow();
569 [
570 st.text_color_prop.clone(),
571 st.caret_color_prop.clone(),
572 st.selection_color_prop.clone(),
573 ]
574 };
575 let registry = ctx.binding_registry();
576 for prop in props.iter().flatten() {
577 prop.register_if_bound(body_id, registry, BindingLevel::RepaintOnly);
578 }
579 }
580
581 let mut children = Vec::with_capacity(4);
582 if self.show_gutter {
583 let gutter_id = ctx.add(CodeGutter::new(&self.state));
584 self.gutter_id = Some(gutter_id);
585 children.push(gutter_id);
586 }
587 children.push(body_id);
588
589 let (scroll_x, scroll_y, max_x, max_y, vr_x, vr_y) = {
591 let st = self.state.borrow();
592 (
593 st.scroll_x.clone(),
594 st.scroll_y.clone(),
595 st.max_scroll_x.clone(),
596 st.max_scroll_y.clone(),
597 st.viewport_ratio_x.clone(),
598 st.viewport_ratio_y.clone(),
599 )
600 };
601 if self.v_scroll_policy != ScrollPolicy::AlwaysOff {
602 let v = ScrollBar::new(
603 ScrollBarOrientation::Vertical,
604 scroll_y,
605 max_y.clone(),
606 vr_y,
607 )
608 .visual(ScrollBarVariant::Overlay);
609 let id = ctx.add(v);
610 self.v_scrollbar_id = Some(id);
611 children.push(id);
612 }
613 if self.h_scroll_policy != ScrollPolicy::AlwaysOff {
614 let h = ScrollBar::new(
615 ScrollBarOrientation::Horizontal,
616 scroll_x,
617 max_x.clone(),
618 vr_x,
619 )
620 .visual(ScrollBarVariant::Overlay);
621 let id = ctx.add(h);
622 self.h_scrollbar_id = Some(id);
623 children.push(id);
624 }
625
626 if self.state.borrow().completion.has_provider() {
631 let open = self.state.borrow().completion.open.clone();
632 let panel_id = ctx.add_deferred(open.clone(), CompletionPanel::new(&self.state));
635 ctx.set_dormant(panel_id);
636 ctx.visible_when(panel_id, open);
637 self.state.borrow_mut().completion.panel_id = Some(panel_id);
638 children.push(panel_id);
639 }
640
641 let self_id = ctx.self_id();
644 let registry = ctx.binding_registry();
645 max_y.bind_to(self_id, registry, BindingLevel::Relayout);
646 max_x.bind_to(self_id, registry, BindingLevel::Relayout);
647
648 children
649 }
650
651 fn layout_response(&self, proposal: SizeProposal, ctx: &LayoutContext) -> LayoutResponse {
652 let w = proposal.width.unwrap_or(400.0).max(0.0);
653
654 if self.min_lines.is_none() && self.max_lines.is_none() {
656 let h = proposal.height.unwrap_or(300.0).max(0.0);
657 return Size::new(w, h).into();
658 }
659
660 let st = self.state.borrow();
661 let line_scale = st.effective_font_scale(ctx.text_scale);
662 let line_h = st.engine.default_line_height() * line_scale;
663 let content_h = st.engine.content_height();
664 drop(st);
665
666 let min_h = self.min_lines.map(|n| n as f32 * line_h).unwrap_or(0.0);
667 let max_h = self
668 .max_lines
669 .map(|n| n as f32 * line_h)
670 .unwrap_or(f32::INFINITY);
671 Size::new(w, content_h.clamp(min_h, max_h).max(0.0)).into()
672 }
673
674 fn place_children(
675 &self,
676 bounds: Rect,
677 _proposal: SizeProposal,
678 children: &mut [WidgetPlacement],
679 ctx: &LayoutContext,
680 ) {
681 self.state.borrow_mut().node_origin = Point::new(bounds.x, bounds.y);
682
683 let gutter_w = self
686 .gutter_id
687 .and_then(|id| ctx.child_size(id, SizeProposal::with_height(bounds.height)))
688 .map(|s| s.width)
689 .unwrap_or(0.0);
690 self.gutter_width.set(gutter_w);
691
692 let body_x = bounds.x + gutter_w;
693 let body_w = (bounds.width - gutter_w).max(0.0);
694
695 let (max_y, max_x) = {
696 let st = self.state.borrow();
697 (st.max_scroll_y.get(), st.max_scroll_x.get())
698 };
699 let show_v = match self.v_scroll_policy {
700 ScrollPolicy::AlwaysOn => true,
701 ScrollPolicy::Auto => max_y > 0.0,
702 ScrollPolicy::AlwaysOff => false,
703 };
704 let show_h = match self.h_scroll_policy {
705 ScrollPolicy::AlwaysOn => true,
706 ScrollPolicy::Auto => max_x > 0.0,
707 ScrollPolicy::AlwaysOff => false,
708 };
709
710 let mut v_rect = Rect::ZERO;
711 let mut h_rect = Rect::ZERO;
712 for child in children.iter_mut() {
713 if Some(child.id) == self.gutter_id {
714 child.origin = Point::new(bounds.x, bounds.y);
715 child.size = Size::new(gutter_w, bounds.height);
716 } else if Some(child.id) == self.body_id {
717 child.origin = Point::new(body_x, bounds.y);
718 child.size = Size::new(body_w, bounds.height);
719 } else if Some(child.id) == self.v_scrollbar_id {
720 if show_v {
721 let h = if show_h {
722 (bounds.height - SCROLLBAR_THICKNESS).max(0.0)
723 } else {
724 bounds.height
725 };
726 child.origin =
727 Point::new(bounds.x + bounds.width - SCROLLBAR_THICKNESS, bounds.y);
728 child.size = Size::new(SCROLLBAR_THICKNESS, h);
729 v_rect = Rect::new(
730 child.origin.x - bounds.x,
731 child.origin.y - bounds.y,
732 SCROLLBAR_THICKNESS,
733 h,
734 );
735 } else {
736 child.origin = Point::new(bounds.x, bounds.y);
737 child.size = Size::ZERO;
738 }
739 } else if Some(child.id) == self.h_scrollbar_id {
740 if show_h {
741 let w = if show_v {
742 (body_w - SCROLLBAR_THICKNESS).max(0.0)
743 } else {
744 body_w
745 };
746 child.origin =
747 Point::new(body_x, bounds.y + bounds.height - SCROLLBAR_THICKNESS);
748 child.size = Size::new(w, SCROLLBAR_THICKNESS);
749 h_rect = Rect::new(
750 child.origin.x - bounds.x,
751 child.origin.y - bounds.y,
752 w,
753 SCROLLBAR_THICKNESS,
754 );
755 } else {
756 child.origin = Point::new(bounds.x, bounds.y);
757 child.size = Size::ZERO;
758 }
759 }
760 }
761 self.v_scrollbar_bounds.set(v_rect);
762 self.h_scrollbar_bounds.set(h_rect);
763 }
764
765 fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
766 let st = self.state.borrow();
771
772 let bg = match &st.background_prop {
773 Some(p) => p.resolve(ctx.theme, true),
774 None => ctx.theme.colors.editor_bg,
775 };
776 canvas.fill_rect(bounds, bg);
777
778 let single_collapsed = st.extra_carets.is_empty() && !st.cursor.has_selection();
782 if st.current_line_highlight
783 && st.has_focus
784 && st.window_active
785 && single_collapsed
786 && let Some((y, h)) = Self::caret_line_band(&st)
787 && y + h > bounds.y
788 && y < bounds.y + bounds.height
789 {
790 let band = Rect::new(bounds.x, y, bounds.width, h);
791 canvas.fill_rect(band, ctx.theme.colors.surface_hover);
792 }
793
794 if let Some((a, b)) = st.bracket_match.get()
796 && st.engine.has_full_layout()
797 {
798 let origin = st.viewport_origin;
799 let scroll_x = st.scroll_x.get();
800 let scroll_y = st.scroll_y.get();
801 for p in [a, b] {
802 let r0 = st.engine.caret_rect(p, CursorAffinity::Downstream);
803 let r1 = st.engine.caret_rect(p + 1, CursorAffinity::Downstream);
804 let x = origin.x + r0[0] - scroll_x;
805 let w = (r1[0] - r0[0]).max(2.0);
806 let y = origin.y + r0[1] - scroll_y;
807 let h = r0[3];
808 if x + w > origin.x && y + h > bounds.y && y < bounds.y + bounds.height {
811 canvas.fill_rect(Rect::new(x, y, w, h), ctx.theme.colors.accent_subtle_bg);
812 }
813 }
814 }
815
816 drop(st);
817
818 let focused = self.state.borrow().focus_signal.get();
821 let border = if focused {
822 ctx.theme.colors.border_focused
823 } else {
824 ctx.theme.colors.border
825 };
826 canvas.stroke_rect(bounds, border, 1.0);
827 }
828
829 fn accessibility(&self, _builder: &mut AccessNodeBuilder) {
830 }
834
835 fn children(&self) -> Vec<WidgetId> {
836 let mut ids = Vec::with_capacity(5);
837 ids.extend(self.gutter_id);
838 ids.extend(self.body_id);
839 ids.extend(self.v_scrollbar_id);
840 ids.extend(self.h_scrollbar_id);
841 ids.extend(self.state.borrow().completion.panel_id);
845 ids
846 }
847
848 fn clips_children(&self) -> bool {
849 true
850 }
851}
852
853#[derive(Debug)]
861pub struct PlainTextEditor {
862 inner: Option<CodeEditor>,
863 inner_id: Option<WidgetId>,
864}
865
866impl PlainTextEditor {
867 pub fn new(document: TextDocument) -> Self {
870 Self::wrap(CodeEditor::new(document))
871 }
872
873 pub fn read_only(document: TextDocument) -> Self {
875 Self::wrap(CodeEditor::read_only(document))
876 }
877
878 fn wrap(editor: CodeEditor) -> Self {
879 let editor = editor
881 .gutter(false)
882 .current_line_highlight(false)
883 .wrap_mode(WrapMode::Word);
884 Self {
885 inner: Some(editor),
886 inner_id: None,
887 }
888 }
889
890 pub fn min_lines(mut self, lines: u32) -> Self {
893 self.map(|e| e.min_lines(lines));
894 self
895 }
896
897 pub fn max_lines(mut self, lines: u32) -> Self {
899 self.map(|e| e.max_lines(lines));
900 self
901 }
902
903 pub fn wrap_mode(mut self, mode: WrapMode) -> Self {
905 self.map(|e| e.wrap_mode(mode));
906 self
907 }
908
909 pub fn font_family(mut self, family: impl Into<String>) -> Self {
911 self.map(|e| e.font_family(family));
912 self
913 }
914
915 pub fn follow_text_scale(mut self, follow: bool) -> Self {
917 self.map(|e| e.follow_text_scale(follow));
918 self
919 }
920
921 pub fn font_size_scale(mut self, scale: f32) -> Self {
923 self.map(|e| e.font_size_scale(scale));
924 self
925 }
926
927 pub fn on_change(mut self, callback: impl Fn() + 'static) -> Self {
929 self.map(|e| e.on_change(callback));
930 self
931 }
932
933 pub fn background(mut self, color: impl Into<teksilo_core::color_prop::ColorProp>) -> Self {
935 self.map(|e| e.background(color));
936 self
937 }
938
939 pub fn handle(&self) -> CodeEditorHandle {
941 self.inner.as_ref().expect("handle() before build").handle()
942 }
943
944 fn map(&mut self, f: impl FnOnce(CodeEditor) -> CodeEditor) {
946 if let Some(e) = self.inner.take() {
947 self.inner = Some(f(e));
948 }
949 }
950}
951
952impl Widget for PlainTextEditor {
953 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
954 let inner = self.inner.take().expect("PlainTextEditor built once");
955 let id = ctx.add(inner);
956 self.inner_id = Some(id);
957 vec![id]
958 }
959
960 fn layout_response(&self, proposal: SizeProposal, ctx: &LayoutContext) -> LayoutResponse {
961 self.inner_id
962 .and_then(|id| ctx.child_size(id, proposal))
963 .unwrap_or_else(|| proposal.resolve(0.0, 0.0))
964 .into()
965 }
966
967 fn place_children(
968 &self,
969 bounds: Rect,
970 _proposal: SizeProposal,
971 children: &mut [WidgetPlacement],
972 _ctx: &LayoutContext,
973 ) {
974 if let Some(child) = children.first_mut() {
975 child.origin = Point::new(bounds.x, bounds.y);
976 child.size = Size::new(bounds.width, bounds.height);
977 }
978 }
979
980 fn children(&self) -> Vec<WidgetId> {
981 self.inner_id.into_iter().collect()
982 }
983}