1use std::cell::Cell;
27use std::rc::Rc;
28
29use teksilo_canvas::{Canvas, Point, Rect, Size, SizeProposal};
30use teksilo_core::accessibility::AccessNodeBuilder;
31use teksilo_core::binding::BindingLevel;
32use teksilo_core::build_context::BuildContext;
33use teksilo_core::widget::{
34 CursorIcon, LayoutContext, LayoutResponse, PaintContext, Widget, WidgetPlacement,
35};
36use teksilo_core::widget_builder::HandlerSet;
37use teksilo_core::widget_id::WidgetId;
38use teksilo_text::text_document::TextDocument;
39use teksilo_tokens::Color;
40
41use super::log_stream::{self, LogStreamState};
42use super::policy::CODE_READ_ONLY_PRESET;
43use super::state::{CodeEditorState, SharedState};
44use super::{adopt_shared_typesetter, construct};
45use crate::common::scroll::OverscrollBehavior;
46use crate::rich_text::ScrollPolicy;
47use crate::scroll_bar::{ScrollBar, ScrollBarOrientation, ScrollBarVariant};
48
49const SCROLLBAR_THICKNESS: f32 = 12.0;
51
52pub struct LogView {
58 state: SharedState,
59 v_scroll_policy: ScrollPolicy,
60 h_scroll_policy: ScrollPolicy,
61 overscroll_behavior: OverscrollBehavior,
62
63 body_id: Option<WidgetId>,
64 v_scrollbar_id: Option<WidgetId>,
65 h_scrollbar_id: Option<WidgetId>,
66 v_scrollbar_bounds: Rc<Cell<Rect>>,
67 h_scrollbar_bounds: Rc<Cell<Rect>>,
68}
69
70impl std::fmt::Debug for LogView {
71 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 f.debug_struct("LogView").finish_non_exhaustive()
73 }
74}
75
76impl Default for LogView {
77 fn default() -> Self {
78 Self::new()
79 }
80}
81
82impl LogView {
83 pub fn new() -> Self {
86 let state = construct(
87 TextDocument::new(),
88 CODE_READ_ONLY_PRESET,
89 super::config::CodeConfig::default(),
90 teksilo_text::WrapMode::None,
91 );
92 state.borrow_mut().log = Some(LogStreamState::new());
93 Self {
94 state,
95 v_scroll_policy: ScrollPolicy::Auto,
96 h_scroll_policy: ScrollPolicy::Auto,
97 overscroll_behavior: OverscrollBehavior::default(),
98 body_id: None,
99 v_scrollbar_id: None,
100 h_scrollbar_id: None,
101 v_scrollbar_bounds: Rc::new(Cell::new(Rect::ZERO)),
102 h_scrollbar_bounds: Rc::new(Cell::new(Rect::ZERO)),
103 }
104 }
105
106 pub fn follow_tail(self, follow: bool) -> Self {
109 if let Some(log) = self.state.borrow_mut().log.as_mut() {
110 log.follow_enabled = follow;
111 }
112 self
113 }
114
115 pub fn scrollback_limit(self, limit: usize) -> Self {
124 if let Some(log) = self.state.borrow_mut().log.as_mut() {
125 log.scrollback_limit = Some(limit);
126 }
127 self
128 }
129
130 pub fn severity_highlighter(self, classify: impl Fn(&str) -> Option<Color> + 'static) -> Self {
134 if let Some(log) = self.state.borrow_mut().log.as_mut() {
135 log.severity = Some(Rc::new(classify));
136 }
137 self
138 }
139
140 pub fn announce_appends(self, announce: bool) -> Self {
145 self.state.borrow_mut().announce_appends = announce;
146 self
147 }
148
149 pub fn font_family(self, family: impl Into<String>) -> Self {
152 {
153 let mut st = self.state.borrow_mut();
154 let mut d = st.engine.typography_defaults().clone();
155 d.font_family = Some(family.into());
156 st.engine.set_typography_defaults(d);
157 st.needs_full_layout = true;
158 }
159 self
160 }
161
162 pub fn follow_text_scale(self, follow: bool) -> Self {
165 self.state.borrow_mut().follow_text_scale = follow;
166 self
167 }
168
169 pub fn v_scroll_policy(mut self, policy: ScrollPolicy) -> Self {
171 self.v_scroll_policy = policy;
172 self
173 }
174
175 pub fn h_scroll_policy(mut self, policy: ScrollPolicy) -> Self {
177 self.h_scroll_policy = policy;
178 self
179 }
180
181 pub fn background(self, color: impl Into<teksilo_core::color_prop::ColorProp>) -> Self {
184 self.state.borrow_mut().background_prop = Some(color.into());
185 self
186 }
187
188 pub fn text_color(self, color: impl Into<teksilo_core::color_prop::ColorProp>) -> Self {
191 self.state.borrow_mut().text_color_prop = Some(color.into());
192 self
193 }
194
195 pub fn selection_color(self, color: impl Into<teksilo_core::color_prop::ColorProp>) -> Self {
197 self.state.borrow_mut().selection_color_prop = Some(color.into());
198 self
199 }
200
201 pub fn handle(&self) -> LogViewHandle {
203 LogViewHandle {
204 state: self.state.clone(),
205 }
206 }
207}
208
209impl Widget for LogView {
210 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
211 adopt_shared_typesetter(&self.state, ctx);
212
213 {
214 let mut st = self.state.borrow_mut();
215 st.frame_request = Some(ctx.frame_request_handle());
216 st.frame_wake_at = Some(ctx.wake_at_handle());
217 st.self_id = Some(ctx.self_id());
218 }
219 let activation = ctx.activation_signal(ctx.self_id());
223 if activation.get() {
224 ctx.request_frame();
225 }
226
227 {
228 let state = self.state.clone();
229 ctx.effect(&activation, move |&active| {
230 if active {
231 let st = state.borrow();
238 if let Some(handle) = &st.frame_request {
239 handle.set(true);
240 }
241 return;
242 }
243 let mut st = state.borrow_mut();
244 if st.has_focus {
245 st.has_focus = false;
246 st.focus_signal.set_if_changed(false);
247 }
248 });
249 }
250
251 {
254 let state = self.state.clone();
255 let active = activation.clone();
256 let tick_signal = ctx.frame_tick();
257 ctx.effect(&tick_signal, move |delta| {
258 if !active.get() {
259 return;
260 }
261 let mut st = state.borrow_mut();
262 let more = log_stream::tick(&mut st, *delta);
263 if more && let Some(handle) = &st.frame_request {
264 handle.set(true);
265 }
266 });
267 }
268
269 {
273 let state = self.state.clone();
274 let active = activation.clone();
275 let wa_signal = ctx.window_active_signal();
276 ctx.effect(&wa_signal, move |&window_active| {
277 let mut st = state.borrow_mut();
278 st.window_active = window_active;
279 if active.get()
280 && let Some(handle) = &st.frame_request
281 {
282 handle.set(true);
283 }
284 });
285 }
286
287 let handlers = HandlerSet::new()
292 .focusable(true)
293 .cursor(CursorIcon::Text)
294 .on_focus({
295 let state = self.state.clone();
296 move |gained, ctx| {
297 state.borrow_mut().focus_signal.set_if_changed(gained);
298 state.borrow_mut().has_focus = gained;
299 ctx.request_frame();
300 }
301 })
302 .on_pointer_event({
303 let state = self.state.clone();
304 let v_sb = self.v_scrollbar_bounds.clone();
305 let h_sb = self.h_scrollbar_bounds.clone();
306 move |event, ctx| {
307 super::mouse::handle_pointer_event(&state, &v_sb, &h_sb, event, ctx)
308 }
309 })
310 .on_scroll({
311 let state = self.state.clone();
312 let overscroll = self.overscroll_behavior;
313 move |event, ctx| super::mouse::handle_scroll(&state, overscroll, event, ctx)
314 })
315 .on_key({
316 let state = self.state.clone();
317 move |event, ctx| log_stream::handle_log_key(&state, event, ctx)
318 })
319 .on_double_tap({
320 let state = self.state.clone();
321 move |event, ctx| super::mouse::handle_double_tap(&state, event.position, ctx)
322 })
323 .on_triple_tap({
324 let state = self.state.clone();
325 move |event, ctx| super::mouse::handle_triple_tap(&state, event.position, ctx)
326 })
327 .on_access_action_request({
328 let state = self.state.clone();
329 move |action, target, data, ctx| {
330 super::a11y::handle_access_action(&state, action, target, data, ctx)
331 }
332 });
333 ctx.apply_self_handlers(handlers);
334
335 let body = log_body_for(&self.state);
336 let body_id = ctx.add(body);
337 self.body_id = Some(body_id);
338
339 {
342 let props = {
343 let st = self.state.borrow();
344 [st.text_color_prop.clone(), st.selection_color_prop.clone()]
345 };
346 let registry = ctx.binding_registry();
347 for prop in props.iter().flatten() {
348 prop.register_if_bound(body_id, registry, BindingLevel::RepaintOnly);
349 }
350 }
351
352 let mut children = Vec::with_capacity(3);
353 children.push(body_id);
354
355 let (scroll_x, scroll_y, max_x, max_y, vr_x, vr_y) = {
356 let st = self.state.borrow();
357 (
358 st.scroll_x.clone(),
359 st.scroll_y.clone(),
360 st.max_scroll_x.clone(),
361 st.max_scroll_y.clone(),
362 st.viewport_ratio_x.clone(),
363 st.viewport_ratio_y.clone(),
364 )
365 };
366 if self.v_scroll_policy != ScrollPolicy::AlwaysOff {
367 let v = ScrollBar::new(
368 ScrollBarOrientation::Vertical,
369 scroll_y,
370 max_y.clone(),
371 vr_y,
372 )
373 .visual(ScrollBarVariant::Overlay);
374 let id = ctx.add(v);
375 self.v_scrollbar_id = Some(id);
376 children.push(id);
377 }
378 if self.h_scroll_policy != ScrollPolicy::AlwaysOff {
379 let h = ScrollBar::new(
380 ScrollBarOrientation::Horizontal,
381 scroll_x,
382 max_x.clone(),
383 vr_x,
384 )
385 .visual(ScrollBarVariant::Overlay);
386 let id = ctx.add(h);
387 self.h_scrollbar_id = Some(id);
388 children.push(id);
389 }
390
391 let self_id = ctx.self_id();
393 let registry = ctx.binding_registry();
394 max_y.bind_to(self_id, registry, BindingLevel::Relayout);
395 max_x.bind_to(self_id, registry, BindingLevel::Relayout);
396
397 children
398 }
399
400 fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
401 let w = proposal.width.unwrap_or(400.0).max(0.0);
404 let h = proposal.height.unwrap_or(300.0).max(0.0);
405 Size::new(w, h).into()
406 }
407
408 fn place_children(
409 &self,
410 bounds: Rect,
411 _proposal: SizeProposal,
412 children: &mut [WidgetPlacement],
413 _ctx: &LayoutContext,
414 ) {
415 self.state.borrow_mut().node_origin = Point::new(bounds.x, bounds.y);
416
417 let (max_y, max_x) = {
418 let st = self.state.borrow();
419 (st.max_scroll_y.get(), st.max_scroll_x.get())
420 };
421 let show_v = match self.v_scroll_policy {
422 ScrollPolicy::AlwaysOn => true,
423 ScrollPolicy::Auto => max_y > 0.0,
424 ScrollPolicy::AlwaysOff => false,
425 };
426 let show_h = match self.h_scroll_policy {
427 ScrollPolicy::AlwaysOn => true,
428 ScrollPolicy::Auto => max_x > 0.0,
429 ScrollPolicy::AlwaysOff => false,
430 };
431
432 let mut v_rect = Rect::ZERO;
433 let mut h_rect = Rect::ZERO;
434 for child in children.iter_mut() {
435 if Some(child.id) == self.body_id {
436 child.origin = Point::new(bounds.x, bounds.y);
437 child.size = Size::new(bounds.width, bounds.height);
438 } else if Some(child.id) == self.v_scrollbar_id {
439 if show_v {
440 let h = if show_h {
441 (bounds.height - SCROLLBAR_THICKNESS).max(0.0)
442 } else {
443 bounds.height
444 };
445 child.origin =
446 Point::new(bounds.x + bounds.width - SCROLLBAR_THICKNESS, bounds.y);
447 child.size = Size::new(SCROLLBAR_THICKNESS, h);
448 v_rect = Rect::new(
449 bounds.width - SCROLLBAR_THICKNESS,
450 0.0,
451 SCROLLBAR_THICKNESS,
452 h,
453 );
454 } else {
455 child.origin = Point::new(bounds.x, bounds.y);
456 child.size = Size::ZERO;
457 }
458 } else if Some(child.id) == self.h_scrollbar_id {
459 if show_h {
460 let w = if show_v {
461 (bounds.width - SCROLLBAR_THICKNESS).max(0.0)
462 } else {
463 bounds.width
464 };
465 child.origin =
466 Point::new(bounds.x, bounds.y + bounds.height - SCROLLBAR_THICKNESS);
467 child.size = Size::new(w, SCROLLBAR_THICKNESS);
468 h_rect = Rect::new(
469 0.0,
470 bounds.height - SCROLLBAR_THICKNESS,
471 w,
472 SCROLLBAR_THICKNESS,
473 );
474 } else {
475 child.origin = Point::new(bounds.x, bounds.y);
476 child.size = Size::ZERO;
477 }
478 }
479 }
480 self.v_scrollbar_bounds.set(v_rect);
481 self.h_scrollbar_bounds.set(h_rect);
482 }
483
484 fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
485 let bg = {
488 let st = self.state.borrow();
489 match &st.background_prop {
490 Some(p) => p.resolve(ctx.theme, true),
491 None => ctx.theme.colors.editor_bg,
492 }
493 };
494 canvas.fill_rect(bounds, bg);
495
496 let focused = self.state.borrow().focus_signal.get();
497 let border = if focused {
498 ctx.theme.colors.border_focused
499 } else {
500 ctx.theme.colors.border
501 };
502 canvas.stroke_rect(bounds, border, 1.0);
503 }
504
505 fn children(&self) -> Vec<WidgetId> {
506 let mut ids = Vec::with_capacity(3);
507 ids.extend(self.body_id);
508 ids.extend(self.v_scrollbar_id);
509 ids.extend(self.h_scrollbar_id);
510 ids
511 }
512
513 fn clips_children(&self) -> bool {
514 true
515 }
516}
517
518pub(crate) struct LogViewBody {
521 state: SharedState,
522}
523
524impl std::fmt::Debug for LogViewBody {
525 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
526 f.debug_struct("LogViewBody").finish_non_exhaustive()
527 }
528}
529
530pub(crate) fn log_body_for(state: &SharedState) -> LogViewBody {
534 LogViewBody {
535 state: state.clone(),
536 }
537}
538
539impl Widget for LogViewBody {
540 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
541 let self_id = ctx.self_id();
542 let registry = ctx.binding_registry();
543 let st = self.state.borrow();
544
545 st.document_version
553 .bind_to(self_id, registry, BindingLevel::RepaintOnly);
554 if let Some(log) = st.log.as_ref() {
555 log.a11y_version
556 .bind_to(self_id, registry, BindingLevel::AccessibilityOnly);
557 }
558 for sig in [&st.scroll_x, &st.scroll_y] {
560 sig.bind_to(self_id, registry, BindingLevel::RepaintOnly);
561 }
562 st.cursor_position
570 .bind_to(self_id, registry, BindingLevel::RepaintOnly);
571 st.cursor_position
572 .bind_to(self_id, registry, BindingLevel::AccessibilityOnly);
573 st.cursor_anchor
574 .bind_to(self_id, registry, BindingLevel::RepaintOnly);
575 st.cursor_anchor
576 .bind_to(self_id, registry, BindingLevel::AccessibilityOnly);
577 st.has_selection
578 .bind_to(self_id, registry, BindingLevel::RepaintOnly);
579
580 Vec::new()
581 }
582
583 fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
584 let w = proposal.width.unwrap_or(200.0).max(0.0);
585 let h = proposal.height.unwrap_or(100.0).max(0.0);
586 Size::new(w, h).into()
587 }
588
589 fn place_children(
590 &self,
591 bounds: Rect,
592 _proposal: SizeProposal,
593 _children: &mut [WidgetPlacement],
594 _ctx: &LayoutContext,
595 ) {
596 self.state.borrow_mut().sync_viewport(bounds);
597 }
598
599 fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
600 let mut st = self.state.borrow_mut();
601
602 let new_text = match &st.text_color_prop {
604 Some(p) => p.resolve(ctx.theme, true).to_array(),
605 None => ctx.theme.colors.editor_fg.to_array(),
606 };
607 st.engine.set_text_color(new_text);
608
609 let new_sel = if let Some(p) = st.selection_color_prop.as_ref() {
610 p.resolve(ctx.theme, true).to_array()
611 } else if ctx.window_active {
612 ctx.theme.colors.editor_selection_bg.to_array()
613 } else {
614 ctx.theme.colors.selection_bg_inactive.to_array()
615 };
616 st.engine.set_selection_color(new_sel);
617
618 let target_scale = st.effective_font_scale(ctx.text_scale);
624 let old_scale = st.last_font_scale;
625 if old_scale.is_nan() || (old_scale - target_scale).abs() > f32::EPSILON {
626 st.last_font_scale = target_scale;
627 st.engine.set_font_scale(target_scale);
628 if old_scale.is_finite() && old_scale > 0.0 {
629 let ratio = target_scale / old_scale;
630 let scaled = st.scroll_y.get() * ratio;
631 st.scroll_y.set_if_changed(scaled);
632 }
633 if let Some(l) = st.log.as_mut() {
634 l.needs_rewindow = true;
635 l.row_height = 0.0;
636 }
637 }
638
639 st.sync_viewport(bounds);
640 log_stream::ensure_window(&mut st, false);
642
643 let scroll_offset = st.scroll_y.get();
646 let affinity = st.cursor_affinity;
647 let cursors: Vec<teksilo_text::CursorDisplay> = st
648 .all_carets()
649 .map(|c| teksilo_text::CursorDisplay {
650 position: c.position(),
651 anchor: c.anchor(),
652 affinity,
653 visible: false,
654 selected_cells: Vec::new(),
655 })
656 .collect();
657 st.engine.set_cursors(&cursors);
658 st.engine.set_scroll_offset(scroll_offset);
659
660 canvas.set_clip(bounds);
661 let CodeEditorState {
662 ref mut engine,
663 ref document,
664 ref mut image_cache,
665 ..
666 } = *st;
667 engine.with_render_frame(|frame| {
668 crate::rich_text::paint::paint_frame(
669 canvas,
670 crate::rich_text::paint::PaintParams {
671 frame,
672 origin: Point::new(bounds.x, bounds.y),
673 document,
674 image_cache,
675 image_resolver: None,
677 selection: None,
678 selection_color: [0.0; 4],
679 selected_image_out: None,
680 resize_preview: None,
681 draw_caret: false,
682 },
683 );
684 });
685 canvas.clear_clip();
686 }
687
688 fn accessibility(&self, builder: &mut AccessNodeBuilder) {
689 use teksilo_core::accesskit::Live;
690
691 let st = self.state.borrow();
692 super::a11y::build_log_a11y(&st, builder);
697
698 if st.announce_appends {
701 builder.inner_mut().set_live(Live::Polite);
702 }
703 }
704
705 fn clips_children(&self) -> bool {
706 true
707 }
708}
709
710#[derive(Clone)]
719pub struct LogViewHandle {
720 state: SharedState,
721}
722
723impl std::fmt::Debug for LogViewHandle {
724 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
725 f.debug_struct("LogViewHandle").finish_non_exhaustive()
726 }
727}
728
729impl LogViewHandle {
730 pub fn append(&self, text: &str) {
734 self.enqueue(text);
735 }
736
737 pub fn append_line(&self, line: &str) {
741 self.enqueue(line);
742 }
743
744 pub fn append_lines<I, S>(&self, lines: I)
746 where
747 I: IntoIterator<Item = S>,
748 S: AsRef<str>,
749 {
750 {
751 let st = self.state.borrow();
752 let Some(log) = st.log.as_ref() else { return };
753 let mut q = log.pending.lock().expect("log append queue poisoned");
754 for line in lines {
755 for piece in line.as_ref().split('\n') {
756 q.push_back(piece.to_string());
757 }
758 }
759 }
760 self.wake();
761 }
762
763 fn enqueue(&self, text: &str) {
764 {
765 let st = self.state.borrow();
766 let Some(log) = st.log.as_ref() else { return };
767 let mut q = log.pending.lock().expect("log append queue poisoned");
768 let body = text.strip_suffix('\n').unwrap_or(text);
770 for piece in body.split('\n') {
771 q.push_back(piece.to_string());
772 }
773 }
774 self.wake();
775 }
776
777 pub fn clear(&self) {
779 {
780 let mut st = self.state.borrow_mut();
781 if let Some(log) = st.log.as_ref() {
782 log.pending
783 .lock()
784 .expect("log append queue poisoned")
785 .clear();
786 }
787 let _ = st.document.set_plain_text("");
788 if let Some(log) = st.log.as_mut() {
789 log.pristine = true;
790 log.total = 0;
791 log.anchor = None;
792 log.last_window = None;
793 log.needs_rewindow = true;
794 }
795 st.line_count.set_if_changed(0);
796 st.scroll_x.set_if_changed(0.0);
797 st.scroll_y.set_if_changed(0.0);
798 }
799 self.wake();
800 }
801
802 pub fn scroll_to_bottom(&self) {
804 {
805 let st = self.state.borrow();
806 let max_y = st.max_scroll_y.get();
807 st.scroll_y.set_if_changed(max_y);
808 }
809 self.wake();
810 }
811
812 pub fn line_count(&self) -> teksilo_core::Signal<usize> {
814 self.state.borrow().line_count.clone()
815 }
816
817 pub fn document_version(&self) -> teksilo_core::Signal<u64> {
819 self.state.borrow().document_version.clone()
820 }
821
822 pub fn scroll_y(&self) -> teksilo_core::Signal<f32> {
825 self.state.borrow().scroll_y.clone()
826 }
827
828 pub fn max_scroll_y(&self) -> teksilo_core::Signal<f32> {
830 self.state.borrow().max_scroll_y.clone()
831 }
832
833 fn wake(&self) {
835 if let Some(handle) = &self.state.borrow().frame_request {
836 handle.set(true);
837 }
838 }
839
840 #[cfg(test)]
841 pub(crate) fn state_handle(&self) -> SharedState {
842 self.state.clone()
843 }
844
845 #[cfg(test)]
846 pub(crate) fn from_state_for_test(state: SharedState) -> Self {
847 Self { state }
848 }
849}