1use std::cell::Cell;
43use std::rc::Rc;
44
45use teksilo_canvas::{Canvas, Point, Rect, Size, SizeProposal};
46use teksilo_core::accessibility::AccessNodeBuilder;
47use teksilo_core::binding::BindingLevel;
48use teksilo_core::build_context::BuildContext;
49use teksilo_core::widget::{
50 CursorIcon, LayoutContext, LayoutResponse, PaintContext, Widget, WidgetPlacement,
51};
52use teksilo_core::widget_builder::HandlerSet;
53use teksilo_core::widget_id::WidgetId;
54use teksilo_text::text_document::TextDocument;
55use teksilo_tokens::Color;
56
57use super::log_stream::{self, LogStreamState};
58use super::policy::CODE_READ_ONLY_PRESET;
59use super::state::{CodeEditorState, SharedState};
60use super::{adopt_shared_typesetter, construct};
61use crate::common::scroll::OverscrollBehavior;
62use crate::rich_text::ScrollPolicy;
63use crate::rich_text::touch_mount::ToolbarIntent;
64use crate::scroll_bar::{ScrollBar, ScrollBarOrientation, ScrollBarVariant};
65
66const SCROLLBAR_THICKNESS: f32 = 12.0;
68
69pub struct LogView {
75 pub(super) state: SharedState,
76 v_scroll_policy: ScrollPolicy,
77 h_scroll_policy: ScrollPolicy,
78 overscroll_behavior: OverscrollBehavior,
79
80 body_id: Option<WidgetId>,
81 v_scrollbar_id: Option<WidgetId>,
82 h_scrollbar_id: Option<WidgetId>,
83 v_scrollbar_bounds: Rc<Cell<Rect>>,
84 h_scrollbar_bounds: Rc<Cell<Rect>>,
85 pub(super) touch: Rc<crate::rich_text::touch_mount::EditorTouch>,
90 default_context_menu_enabled: bool,
92 custom_context_menu: Option<super::context_menu::CodeContextMenuFactory>,
94}
95
96impl std::fmt::Debug for LogView {
97 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98 f.debug_struct("LogView").finish_non_exhaustive()
99 }
100}
101
102impl Default for LogView {
103 fn default() -> Self {
104 Self::new()
105 }
106}
107
108impl LogView {
109 pub fn new() -> Self {
112 let state = construct(
113 TextDocument::new(),
114 CODE_READ_ONLY_PRESET,
115 super::config::CodeConfig::default(),
116 teksilo_text::WrapMode::None,
117 );
118 state.borrow_mut().log = Some(LogStreamState::new());
119 let touch = super::touch::mount_for(state.clone());
120 Self {
121 state,
122 v_scroll_policy: ScrollPolicy::Auto,
123 h_scroll_policy: ScrollPolicy::Auto,
124 overscroll_behavior: OverscrollBehavior::default(),
125 body_id: None,
126 v_scrollbar_id: None,
127 h_scrollbar_id: None,
128 v_scrollbar_bounds: Rc::new(Cell::new(Rect::ZERO)),
129 h_scrollbar_bounds: Rc::new(Cell::new(Rect::ZERO)),
130 touch,
131 default_context_menu_enabled: true,
132 custom_context_menu: None,
133 }
134 }
135
136 pub fn context_menu(
140 mut self,
141 factory: impl Fn(
142 teksilo_canvas::Point,
143 &mut teksilo_core::widget::EventContext,
144 ) -> Option<Box<dyn teksilo_core::widget::Widget>>
145 + 'static,
146 ) -> Self {
147 self.custom_context_menu = Some(Box::new(factory));
148 self
149 }
150
151 pub fn default_context_menu(mut self, enabled: bool) -> Self {
158 self.default_context_menu_enabled = enabled;
159 self
160 }
161
162 pub fn follow_tail(self, follow: bool) -> Self {
165 if let Some(log) = self.state.borrow_mut().log.as_mut() {
166 log.follow_enabled = follow;
167 }
168 self
169 }
170
171 pub fn scrollback_limit(self, limit: usize) -> Self {
180 if let Some(log) = self.state.borrow_mut().log.as_mut() {
181 log.scrollback_limit = Some(limit);
182 }
183 self
184 }
185
186 pub fn severity_highlighter(self, classify: impl Fn(&str) -> Option<Color> + 'static) -> Self {
190 if let Some(log) = self.state.borrow_mut().log.as_mut() {
191 log.severity = Some(Rc::new(classify));
192 }
193 self
194 }
195
196 pub fn announce_appends(self, announce: bool) -> Self {
201 self.state.borrow_mut().announce_appends = announce;
202 self
203 }
204
205 pub fn font_family(self, family: impl Into<String>) -> Self {
208 {
209 let mut st = self.state.borrow_mut();
210 let mut d = st.engine.typography_defaults().clone();
211 d.font_family = Some(family.into());
212 st.engine.set_typography_defaults(d);
213 st.needs_full_layout = true;
214 }
215 self
216 }
217
218 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 v_scroll_policy(mut self, policy: ScrollPolicy) -> Self {
227 self.v_scroll_policy = policy;
228 self
229 }
230
231 pub fn h_scroll_policy(mut self, policy: ScrollPolicy) -> Self {
233 self.h_scroll_policy = policy;
234 self
235 }
236
237 pub fn background(self, color: impl Into<teksilo_core::color_prop::ColorProp>) -> Self {
240 self.state.borrow_mut().background_prop = Some(color.into());
241 self
242 }
243
244 pub fn text_color(self, color: impl Into<teksilo_core::color_prop::ColorProp>) -> Self {
247 self.state.borrow_mut().text_color_prop = Some(color.into());
248 self
249 }
250
251 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 handle(&self) -> LogViewHandle {
259 LogViewHandle {
260 state: self.state.clone(),
261 }
262 }
263}
264
265impl Widget for LogView {
266 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
267 adopt_shared_typesetter(&self.state, ctx);
268
269 {
270 let mut st = self.state.borrow_mut();
271 st.frame_request = Some(ctx.frame_request_handle());
272 st.frame_wake_at = Some(ctx.wake_at_handle());
273 st.self_id = Some(ctx.self_id());
274 }
275 let activation = ctx.activation_signal(ctx.self_id());
279 if activation.get() {
280 ctx.request_frame();
281 }
282
283 {
284 let state = self.state.clone();
285 ctx.effect(&activation, move |&active| {
286 if active {
287 let st = state.borrow();
294 if let Some(handle) = &st.frame_request {
295 handle.set(true);
296 }
297 return;
298 }
299 let mut st = state.borrow_mut();
300 if st.has_focus {
301 st.has_focus = false;
302 st.focus_signal.set_if_changed(false);
303 }
304 });
305 }
306
307 {
310 let state = self.state.clone();
311 let active = activation.clone();
312 let tick_signal = ctx.frame_tick();
313 ctx.effect(&tick_signal, move |delta| {
314 if !active.get() {
315 return;
316 }
317 let mut st = state.borrow_mut();
318 let more = log_stream::tick(&mut st, *delta);
319 if more && let Some(handle) = &st.frame_request {
320 handle.set(true);
321 }
322 });
323 }
324
325 {
329 let state = self.state.clone();
330 let active = activation.clone();
331 let wa_signal = ctx.window_active_signal();
332 ctx.effect(&wa_signal, move |&window_active| {
333 let mut st = state.borrow_mut();
334 st.window_active = window_active;
335 if active.get()
336 && let Some(handle) = &st.frame_request
337 {
338 handle.set(true);
339 }
340 });
341 }
342
343 let mut handlers = HandlerSet::new()
348 .focusable(true)
349 .cursor(CursorIcon::Text)
350 .on_focus({
351 let state = self.state.clone();
352 let touch = self.touch.clone();
353 move |gained, ctx| {
354 state.borrow_mut().focus_signal.set_if_changed(gained);
355 state.borrow_mut().has_focus = gained;
356 if !gained {
357 touch.dismiss();
360 }
361 ctx.request_frame();
362 }
363 })
364 .on_pointer_event({
365 let state = self.state.clone();
366 let touch = self.touch.clone();
367 let v_sb = self.v_scrollbar_bounds.clone();
368 let h_sb = self.h_scrollbar_bounds.clone();
369 move |event, ctx| {
370 super::mouse::handle_pointer_event(&state, &touch, &v_sb, &h_sb, event, ctx)
371 }
372 })
373 .on_long_press({
379 let state = self.state.clone();
380 let touch = self.touch.clone();
381 move |event, ctx| {
382 super::mouse::handle_long_press(&state, &touch, event, ctx);
383 }
384 })
385 .on_key({
386 let state = self.state.clone();
387 let touch = self.touch.clone();
388 move |event, ctx| {
389 let response = log_stream::handle_log_key(&state, event, ctx);
390 touch.refresh(ctx, ToolbarIntent::Keep);
393 response
394 }
395 })
396 .on_double_tap({
397 let state = self.state.clone();
398 let touch = self.touch.clone();
399 move |event, ctx| {
400 super::mouse::handle_double_tap(&state, event.position, ctx);
401 if event.pointer.kind.is_direct() {
402 touch.raise(ctx, ToolbarIntent::Show);
403 }
404 }
405 })
406 .on_triple_tap({
407 let state = self.state.clone();
408 let touch = self.touch.clone();
409 move |event, ctx| {
410 super::mouse::handle_triple_tap(&state, event.position, ctx);
411 if event.pointer.kind.is_direct() {
412 touch.raise(ctx, ToolbarIntent::Show);
413 }
414 }
415 })
416 .on_access_action_request({
417 let state = self.state.clone();
418 let touch = self.touch.clone();
419 move |action, target, data, ctx| {
420 let response =
421 super::a11y::handle_access_action(&state, action, target, data, ctx);
422 touch.refresh(ctx, ToolbarIntent::Keep);
423 response
424 }
425 });
426 if let Some(factory) = super::context_menu::resolve_factory(
428 self.custom_context_menu.take(),
429 self.default_context_menu_enabled,
430 self.state.clone(),
431 ) {
432 handlers = handlers.context_menu(move |pos, ctx| factory(pos, ctx));
433 }
434 {
438 let (x, max_x, y, max_y, scroller) = {
439 let st = self.state.borrow();
440 (
441 st.scroll_x.clone(),
442 st.max_scroll_x.clone(),
443 st.scroll_y.clone(),
444 st.max_scroll_y.clone(),
445 st.scroller.clone(),
446 )
447 };
448 let behavior = crate::common::text_scroll::text_surface_behavior(
449 crate::common::text_scroll::TextScrollState {
450 x,
451 max_x,
452 y,
453 max_y,
454 scroller,
455 },
456 self.overscroll_behavior,
457 ctx.prefers_reduced_motion(),
458 ctx.theme().input.scroll_physics,
459 );
460 handlers = behavior.install(handlers);
461 }
462
463 ctx.apply_self_handlers(handlers);
464
465 let self_id = ctx.self_id();
469 self.touch.build(ctx, self_id);
470
471 let body = log_body_for(&self.state);
472 let body_id = ctx.add(body);
473 self.body_id = Some(body_id);
474
475 {
478 let props = {
479 let st = self.state.borrow();
480 [st.text_color_prop.clone(), st.selection_color_prop.clone()]
481 };
482 let registry = ctx.binding_registry();
483 for prop in props.iter().flatten() {
484 prop.register_if_bound(body_id, registry, BindingLevel::RepaintOnly);
485 }
486 }
487
488 let mut children = Vec::with_capacity(3);
489 children.push(body_id);
490
491 let (scroll_x, scroll_y, max_x, max_y, vr_x, vr_y) = {
492 let st = self.state.borrow();
493 (
494 st.scroll_x.clone(),
495 st.scroll_y.clone(),
496 st.max_scroll_x.clone(),
497 st.max_scroll_y.clone(),
498 st.viewport_ratio_x.clone(),
499 st.viewport_ratio_y.clone(),
500 )
501 };
502 if self.v_scroll_policy != ScrollPolicy::AlwaysOff {
503 let v = ScrollBar::new(
504 ScrollBarOrientation::Vertical,
505 scroll_y,
506 max_y.clone(),
507 vr_y,
508 )
509 .visual(ScrollBarVariant::Overlay);
510 let id = ctx.add(v);
511 self.v_scrollbar_id = Some(id);
512 children.push(id);
513 }
514 if self.h_scroll_policy != ScrollPolicy::AlwaysOff {
515 let h = ScrollBar::new(
516 ScrollBarOrientation::Horizontal,
517 scroll_x,
518 max_x.clone(),
519 vr_x,
520 )
521 .visual(ScrollBarVariant::Overlay);
522 let id = ctx.add(h);
523 self.h_scrollbar_id = Some(id);
524 children.push(id);
525 }
526
527 let self_id = ctx.self_id();
529 let registry = ctx.binding_registry();
530 max_y.bind_to(self_id, registry, BindingLevel::Relayout);
531 max_x.bind_to(self_id, registry, BindingLevel::Relayout);
532
533 children
534 }
535
536 fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
537 let w = proposal.width.unwrap_or(400.0).max(0.0);
540 let h = proposal.height.unwrap_or(300.0).max(0.0);
541 Size::new(w, h).into()
542 }
543
544 fn place_children(
545 &self,
546 bounds: Rect,
547 _proposal: SizeProposal,
548 children: &mut [WidgetPlacement],
549 _ctx: &LayoutContext,
550 ) {
551 self.state.borrow_mut().node_origin = Point::new(bounds.x, bounds.y);
552
553 let (max_y, max_x) = {
554 let st = self.state.borrow();
555 (st.max_scroll_y.get(), st.max_scroll_x.get())
556 };
557 let show_v = match self.v_scroll_policy {
558 ScrollPolicy::AlwaysOn => true,
559 ScrollPolicy::Auto => max_y > 0.0,
560 ScrollPolicy::AlwaysOff => false,
561 };
562 let show_h = match self.h_scroll_policy {
563 ScrollPolicy::AlwaysOn => true,
564 ScrollPolicy::Auto => max_x > 0.0,
565 ScrollPolicy::AlwaysOff => false,
566 };
567
568 let mut v_rect = Rect::ZERO;
569 let mut h_rect = Rect::ZERO;
570 for child in children.iter_mut() {
571 if Some(child.id) == self.body_id {
572 child.origin = Point::new(bounds.x, bounds.y);
573 child.size = Size::new(bounds.width, bounds.height);
574 } else if Some(child.id) == self.v_scrollbar_id {
575 if show_v {
576 let h = if show_h {
577 (bounds.height - SCROLLBAR_THICKNESS).max(0.0)
578 } else {
579 bounds.height
580 };
581 child.origin =
582 Point::new(bounds.x + bounds.width - SCROLLBAR_THICKNESS, bounds.y);
583 child.size = Size::new(SCROLLBAR_THICKNESS, h);
584 v_rect = Rect::new(
585 bounds.width - SCROLLBAR_THICKNESS,
586 0.0,
587 SCROLLBAR_THICKNESS,
588 h,
589 );
590 } else {
591 child.origin = Point::new(bounds.x, bounds.y);
592 child.size = Size::ZERO;
593 }
594 } else if Some(child.id) == self.h_scrollbar_id {
595 if show_h {
596 let w = if show_v {
597 (bounds.width - SCROLLBAR_THICKNESS).max(0.0)
598 } else {
599 bounds.width
600 };
601 child.origin =
602 Point::new(bounds.x, bounds.y + bounds.height - SCROLLBAR_THICKNESS);
603 child.size = Size::new(w, SCROLLBAR_THICKNESS);
604 h_rect = Rect::new(
605 0.0,
606 bounds.height - SCROLLBAR_THICKNESS,
607 w,
608 SCROLLBAR_THICKNESS,
609 );
610 } else {
611 child.origin = Point::new(bounds.x, bounds.y);
612 child.size = Size::ZERO;
613 }
614 }
615 }
616 self.v_scrollbar_bounds.set(v_rect);
617 self.h_scrollbar_bounds.set(h_rect);
618 }
619
620 fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
621 let bg = {
624 let st = self.state.borrow();
625 match &st.background_prop {
626 Some(p) => p.resolve(ctx.theme, true),
627 None => ctx.theme.colors.editor_bg,
628 }
629 };
630 canvas.fill_rect(bounds, bg);
631
632 let focused = self.state.borrow().focus_signal.get();
633 let border = if focused {
634 ctx.theme.colors.border_focused
635 } else {
636 ctx.theme.colors.border
637 };
638 canvas.stroke_rect(bounds, border, 1.0);
639 }
640
641 fn children(&self) -> Vec<WidgetId> {
642 let mut ids = Vec::with_capacity(3);
643 ids.extend(self.body_id);
644 ids.extend(self.v_scrollbar_id);
645 ids.extend(self.h_scrollbar_id);
646 ids
647 }
648
649 fn clips_children(&self) -> bool {
650 true
651 }
652}
653
654pub(crate) struct LogViewBody {
657 state: SharedState,
658}
659
660impl std::fmt::Debug for LogViewBody {
661 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
662 f.debug_struct("LogViewBody").finish_non_exhaustive()
663 }
664}
665
666pub(crate) fn log_body_for(state: &SharedState) -> LogViewBody {
670 LogViewBody {
671 state: state.clone(),
672 }
673}
674
675impl Widget for LogViewBody {
676 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
677 let self_id = ctx.self_id();
678 let registry = ctx.binding_registry();
679 let st = self.state.borrow();
680
681 st.document_version
691 .bind_to(self_id, registry, BindingLevel::RepaintOnly);
692 if let Some(log) = st.log.as_ref() {
693 log.a11y_version
694 .bind_to(self_id, registry, BindingLevel::AccessibilityOnly);
695 }
696 for sig in [&st.scroll_x, &st.scroll_y] {
698 sig.bind_to(self_id, registry, BindingLevel::RepaintOnly);
699 }
700 st.cursor_position
708 .bind_to(self_id, registry, BindingLevel::RepaintOnly);
709 st.cursor_position
710 .bind_to(self_id, registry, BindingLevel::AccessibilityOnly);
711 st.cursor_anchor
712 .bind_to(self_id, registry, BindingLevel::RepaintOnly);
713 st.cursor_anchor
714 .bind_to(self_id, registry, BindingLevel::AccessibilityOnly);
715 st.has_selection
716 .bind_to(self_id, registry, BindingLevel::RepaintOnly);
717
718 Vec::new()
719 }
720
721 fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
722 let w = proposal.width.unwrap_or(200.0).max(0.0);
723 let h = proposal.height.unwrap_or(100.0).max(0.0);
724 Size::new(w, h).into()
725 }
726
727 fn place_children(
728 &self,
729 bounds: Rect,
730 _proposal: SizeProposal,
731 _children: &mut [WidgetPlacement],
732 _ctx: &LayoutContext,
733 ) {
734 self.state.borrow_mut().sync_viewport(bounds);
735 }
736
737 fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
738 let mut st = self.state.borrow_mut();
739
740 let new_text = match &st.text_color_prop {
742 Some(p) => p.resolve(ctx.theme, true).to_array(),
743 None => ctx.theme.colors.editor_fg.to_array(),
744 };
745 st.engine.set_text_color(new_text);
746
747 let new_sel = if let Some(p) = st.selection_color_prop.as_ref() {
748 p.resolve(ctx.theme, true).to_array()
749 } else if ctx.window_active {
750 ctx.theme.colors.editor_selection_bg.to_array()
751 } else {
752 ctx.theme.colors.selection_bg_inactive.to_array()
753 };
754 st.engine.set_selection_color(new_sel);
755
756 let target_scale = st.effective_font_scale(ctx.text_scale);
762 let old_scale = st.last_font_scale;
763 if old_scale.is_nan() || (old_scale - target_scale).abs() > f32::EPSILON {
764 st.last_font_scale = target_scale;
765 st.engine.set_font_scale(target_scale);
766 if old_scale.is_finite() && old_scale > 0.0 {
767 let ratio = target_scale / old_scale;
768 let scaled = st.scroll_y.get() * ratio;
769 st.scroll_y.set_if_changed(scaled);
770 }
771 if let Some(l) = st.log.as_mut() {
772 l.needs_rewindow = true;
773 l.row_height = 0.0;
774 }
775 }
776
777 st.sync_viewport(bounds);
778 log_stream::ensure_window(&mut st, false);
780
781 let scroll_offset = st.scroll_y.get();
784 let affinity = st.cursor_affinity;
785 let cursors: Vec<teksilo_text::CursorDisplay> = st
786 .all_carets()
787 .map(|c| teksilo_text::CursorDisplay {
788 position: c.position(),
789 anchor: c.anchor(),
790 affinity,
791 visible: false,
792 selected_cells: Vec::new(),
793 })
794 .collect();
795 st.engine.set_cursors(&cursors);
796 st.engine.set_scroll_offset(scroll_offset);
797
798 canvas.set_clip(bounds);
799 let CodeEditorState {
800 ref mut engine,
801 ref document,
802 ref mut image_cache,
803 ..
804 } = *st;
805 engine.with_render_frame(|frame| {
806 crate::rich_text::paint::paint_frame(
807 canvas,
808 crate::rich_text::paint::PaintParams {
809 frame,
810 origin: Point::new(bounds.x, bounds.y),
811 document,
812 image_cache,
813 image_resolver: None,
815 selection: None,
816 selection_color: [0.0; 4],
817 selected_image_out: None,
818 resize_preview: None,
819 draw_caret: false,
820 },
821 );
822 });
823 canvas.clear_clip();
824 }
825
826 fn accessibility(&self, builder: &mut AccessNodeBuilder) {
827 use teksilo_core::accesskit::Live;
828
829 let st = self.state.borrow();
830 super::a11y::build_log_a11y(&st, builder);
835
836 if st.announce_appends {
839 builder.inner_mut().set_live(Live::Polite);
840 }
841 }
842
843 fn clips_children(&self) -> bool {
844 true
845 }
846}
847
848#[derive(Clone)]
857pub struct LogViewHandle {
858 state: SharedState,
859}
860
861impl std::fmt::Debug for LogViewHandle {
862 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
863 f.debug_struct("LogViewHandle").finish_non_exhaustive()
864 }
865}
866
867impl LogViewHandle {
868 pub fn append(&self, text: &str) {
872 self.enqueue(text);
873 }
874
875 pub fn append_line(&self, line: &str) {
879 self.enqueue(line);
880 }
881
882 pub fn append_lines<I, S>(&self, lines: I)
884 where
885 I: IntoIterator<Item = S>,
886 S: AsRef<str>,
887 {
888 {
889 let st = self.state.borrow();
890 let Some(log) = st.log.as_ref() else { return };
891 let mut q = log.pending.lock().expect("log append queue poisoned");
892 for line in lines {
893 for piece in line.as_ref().split('\n') {
894 q.push_back(piece.to_string());
895 }
896 }
897 }
898 self.wake();
899 }
900
901 fn enqueue(&self, text: &str) {
902 {
903 let st = self.state.borrow();
904 let Some(log) = st.log.as_ref() else { return };
905 let mut q = log.pending.lock().expect("log append queue poisoned");
906 let body = text.strip_suffix('\n').unwrap_or(text);
908 for piece in body.split('\n') {
909 q.push_back(piece.to_string());
910 }
911 }
912 self.wake();
913 }
914
915 pub fn clear(&self) {
917 {
918 let mut st = self.state.borrow_mut();
919 if let Some(log) = st.log.as_ref() {
920 log.pending
921 .lock()
922 .expect("log append queue poisoned")
923 .clear();
924 }
925 let _ = st.document.set_plain_text("");
926 if let Some(log) = st.log.as_mut() {
927 log.pristine = true;
928 log.total = 0;
929 log.anchor = None;
930 log.last_window = None;
931 log.needs_rewindow = true;
932 }
933 st.line_count.set_if_changed(0);
934 st.scroll_x.set_if_changed(0.0);
935 st.scroll_y.set_if_changed(0.0);
936 }
937 self.wake();
938 }
939
940 pub fn scroll_to_bottom(&self) {
942 {
943 let st = self.state.borrow();
944 let max_y = st.max_scroll_y.get();
945 st.scroll_y.set_if_changed(max_y);
946 }
947 self.wake();
948 }
949
950 pub fn line_count(&self) -> teksilo_core::Signal<usize> {
952 self.state.borrow().line_count.clone()
953 }
954
955 pub fn document_version(&self) -> teksilo_core::Signal<u64> {
957 self.state.borrow().document_version.clone()
958 }
959
960 pub fn scroll_y(&self) -> teksilo_core::Signal<f32> {
963 self.state.borrow().scroll_y.clone()
964 }
965
966 pub fn max_scroll_y(&self) -> teksilo_core::Signal<f32> {
968 self.state.borrow().max_scroll_y.clone()
969 }
970
971 fn wake(&self) {
973 if let Some(handle) = &self.state.borrow().frame_request {
974 handle.set(true);
975 }
976 }
977
978 #[cfg(test)]
979 pub(crate) fn state_handle(&self) -> SharedState {
980 self.state.clone()
981 }
982
983 #[cfg(test)]
984 pub(crate) fn from_state_for_test(state: SharedState) -> Self {
985 Self { state }
986 }
987}