use std::cell::Cell;
use std::rc::Rc;
use teksilo_canvas::{Canvas, Point, Rect, Size, SizeProposal};
use teksilo_core::accessibility::AccessNodeBuilder;
use teksilo_core::binding::BindingLevel;
use teksilo_core::build_context::BuildContext;
use teksilo_core::widget::{
CursorIcon, LayoutContext, LayoutResponse, PaintContext, Widget, WidgetPlacement,
};
use teksilo_core::widget_builder::HandlerSet;
use teksilo_core::widget_id::WidgetId;
use teksilo_text::text_document::TextDocument;
use teksilo_tokens::Color;
use super::log_stream::{self, LogStreamState};
use super::policy::CODE_READ_ONLY_PRESET;
use super::state::{CodeEditorState, SharedState};
use super::{adopt_shared_typesetter, construct};
use crate::common::scroll::OverscrollBehavior;
use crate::rich_text::ScrollPolicy;
use crate::scroll_bar::{ScrollBar, ScrollBarOrientation, ScrollBarVariant};
const SCROLLBAR_THICKNESS: f32 = 12.0;
pub struct LogView {
state: SharedState,
v_scroll_policy: ScrollPolicy,
h_scroll_policy: ScrollPolicy,
overscroll_behavior: OverscrollBehavior,
body_id: Option<WidgetId>,
v_scrollbar_id: Option<WidgetId>,
h_scrollbar_id: Option<WidgetId>,
v_scrollbar_bounds: Rc<Cell<Rect>>,
h_scrollbar_bounds: Rc<Cell<Rect>>,
}
impl std::fmt::Debug for LogView {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LogView").finish_non_exhaustive()
}
}
impl Default for LogView {
fn default() -> Self {
Self::new()
}
}
impl LogView {
pub fn new() -> Self {
let state = construct(
TextDocument::new(),
CODE_READ_ONLY_PRESET,
super::config::CodeConfig::default(),
teksilo_text::WrapMode::None,
);
state.borrow_mut().log = Some(LogStreamState::new());
Self {
state,
v_scroll_policy: ScrollPolicy::Auto,
h_scroll_policy: ScrollPolicy::Auto,
overscroll_behavior: OverscrollBehavior::default(),
body_id: None,
v_scrollbar_id: None,
h_scrollbar_id: None,
v_scrollbar_bounds: Rc::new(Cell::new(Rect::ZERO)),
h_scrollbar_bounds: Rc::new(Cell::new(Rect::ZERO)),
}
}
pub fn follow_tail(self, follow: bool) -> Self {
if let Some(log) = self.state.borrow_mut().log.as_mut() {
log.follow_enabled = follow;
}
self
}
pub fn scrollback_limit(self, limit: usize) -> Self {
if let Some(log) = self.state.borrow_mut().log.as_mut() {
log.scrollback_limit = Some(limit);
}
self
}
pub fn severity_highlighter(self, classify: impl Fn(&str) -> Option<Color> + 'static) -> Self {
if let Some(log) = self.state.borrow_mut().log.as_mut() {
log.severity = Some(Rc::new(classify));
}
self
}
pub fn announce_appends(self, announce: bool) -> Self {
self.state.borrow_mut().announce_appends = announce;
self
}
pub fn font_family(self, family: impl Into<String>) -> Self {
{
let mut st = self.state.borrow_mut();
let mut d = st.engine.typography_defaults().clone();
d.font_family = Some(family.into());
st.engine.set_typography_defaults(d);
st.needs_full_layout = true;
}
self
}
pub fn follow_text_scale(self, follow: bool) -> Self {
self.state.borrow_mut().follow_text_scale = follow;
self
}
pub fn v_scroll_policy(mut self, policy: ScrollPolicy) -> Self {
self.v_scroll_policy = policy;
self
}
pub fn h_scroll_policy(mut self, policy: ScrollPolicy) -> Self {
self.h_scroll_policy = policy;
self
}
pub fn background(self, color: impl Into<teksilo_core::color_prop::ColorProp>) -> Self {
self.state.borrow_mut().background_prop = Some(color.into());
self
}
pub fn text_color(self, color: impl Into<teksilo_core::color_prop::ColorProp>) -> Self {
self.state.borrow_mut().text_color_prop = Some(color.into());
self
}
pub fn selection_color(self, color: impl Into<teksilo_core::color_prop::ColorProp>) -> Self {
self.state.borrow_mut().selection_color_prop = Some(color.into());
self
}
pub fn handle(&self) -> LogViewHandle {
LogViewHandle {
state: self.state.clone(),
}
}
}
impl Widget for LogView {
fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
adopt_shared_typesetter(&self.state, ctx);
{
let mut st = self.state.borrow_mut();
st.frame_request = Some(ctx.frame_request_handle());
st.frame_wake_at = Some(ctx.wake_at_handle());
st.self_id = Some(ctx.self_id());
}
let activation = ctx.activation_signal(ctx.self_id());
if activation.get() {
ctx.request_frame();
}
{
let state = self.state.clone();
ctx.effect(&activation, move |&active| {
if active {
let st = state.borrow();
if let Some(handle) = &st.frame_request {
handle.set(true);
}
return;
}
let mut st = state.borrow_mut();
if st.has_focus {
st.has_focus = false;
st.focus_signal.set_if_changed(false);
}
});
}
{
let state = self.state.clone();
let active = activation.clone();
let tick_signal = ctx.frame_tick();
ctx.effect(&tick_signal, move |delta| {
if !active.get() {
return;
}
let mut st = state.borrow_mut();
let more = log_stream::tick(&mut st, *delta);
if more && let Some(handle) = &st.frame_request {
handle.set(true);
}
});
}
{
let state = self.state.clone();
let active = activation.clone();
let wa_signal = ctx.window_active_signal();
ctx.effect(&wa_signal, move |&window_active| {
let mut st = state.borrow_mut();
st.window_active = window_active;
if active.get()
&& let Some(handle) = &st.frame_request
{
handle.set(true);
}
});
}
let handlers = HandlerSet::new()
.focusable(true)
.cursor(CursorIcon::Text)
.on_focus({
let state = self.state.clone();
move |gained, ctx| {
state.borrow_mut().focus_signal.set_if_changed(gained);
state.borrow_mut().has_focus = gained;
ctx.request_frame();
}
})
.on_pointer_event({
let state = self.state.clone();
let v_sb = self.v_scrollbar_bounds.clone();
let h_sb = self.h_scrollbar_bounds.clone();
move |event, ctx| {
super::mouse::handle_pointer_event(&state, &v_sb, &h_sb, event, ctx)
}
})
.on_scroll({
let state = self.state.clone();
let overscroll = self.overscroll_behavior;
move |event, ctx| super::mouse::handle_scroll(&state, overscroll, event, ctx)
})
.on_key({
let state = self.state.clone();
move |event, ctx| log_stream::handle_log_key(&state, event, ctx)
})
.on_double_tap({
let state = self.state.clone();
move |event, ctx| super::mouse::handle_double_tap(&state, event.position, ctx)
})
.on_triple_tap({
let state = self.state.clone();
move |event, ctx| super::mouse::handle_triple_tap(&state, event.position, ctx)
})
.on_access_action_request({
let state = self.state.clone();
move |action, target, data, ctx| {
super::a11y::handle_access_action(&state, action, target, data, ctx)
}
});
ctx.apply_self_handlers(handlers);
let body = log_body_for(&self.state);
let body_id = ctx.add(body);
self.body_id = Some(body_id);
{
let props = {
let st = self.state.borrow();
[st.text_color_prop.clone(), st.selection_color_prop.clone()]
};
let registry = ctx.binding_registry();
for prop in props.iter().flatten() {
prop.register_if_bound(body_id, registry, BindingLevel::RepaintOnly);
}
}
let mut children = Vec::with_capacity(3);
children.push(body_id);
let (scroll_x, scroll_y, max_x, max_y, vr_x, vr_y) = {
let st = self.state.borrow();
(
st.scroll_x.clone(),
st.scroll_y.clone(),
st.max_scroll_x.clone(),
st.max_scroll_y.clone(),
st.viewport_ratio_x.clone(),
st.viewport_ratio_y.clone(),
)
};
if self.v_scroll_policy != ScrollPolicy::AlwaysOff {
let v = ScrollBar::new(
ScrollBarOrientation::Vertical,
scroll_y,
max_y.clone(),
vr_y,
)
.visual(ScrollBarVariant::Overlay);
let id = ctx.add(v);
self.v_scrollbar_id = Some(id);
children.push(id);
}
if self.h_scroll_policy != ScrollPolicy::AlwaysOff {
let h = ScrollBar::new(
ScrollBarOrientation::Horizontal,
scroll_x,
max_x.clone(),
vr_x,
)
.visual(ScrollBarVariant::Overlay);
let id = ctx.add(h);
self.h_scrollbar_id = Some(id);
children.push(id);
}
let self_id = ctx.self_id();
let registry = ctx.binding_registry();
max_y.bind_to(self_id, registry, BindingLevel::Relayout);
max_x.bind_to(self_id, registry, BindingLevel::Relayout);
children
}
fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
let w = proposal.width.unwrap_or(400.0).max(0.0);
let h = proposal.height.unwrap_or(300.0).max(0.0);
Size::new(w, h).into()
}
fn place_children(
&self,
bounds: Rect,
_proposal: SizeProposal,
children: &mut [WidgetPlacement],
_ctx: &LayoutContext,
) {
self.state.borrow_mut().node_origin = Point::new(bounds.x, bounds.y);
let (max_y, max_x) = {
let st = self.state.borrow();
(st.max_scroll_y.get(), st.max_scroll_x.get())
};
let show_v = match self.v_scroll_policy {
ScrollPolicy::AlwaysOn => true,
ScrollPolicy::Auto => max_y > 0.0,
ScrollPolicy::AlwaysOff => false,
};
let show_h = match self.h_scroll_policy {
ScrollPolicy::AlwaysOn => true,
ScrollPolicy::Auto => max_x > 0.0,
ScrollPolicy::AlwaysOff => false,
};
let mut v_rect = Rect::ZERO;
let mut h_rect = Rect::ZERO;
for child in children.iter_mut() {
if Some(child.id) == self.body_id {
child.origin = Point::new(bounds.x, bounds.y);
child.size = Size::new(bounds.width, bounds.height);
} else if Some(child.id) == self.v_scrollbar_id {
if show_v {
let h = if show_h {
(bounds.height - SCROLLBAR_THICKNESS).max(0.0)
} else {
bounds.height
};
child.origin =
Point::new(bounds.x + bounds.width - SCROLLBAR_THICKNESS, bounds.y);
child.size = Size::new(SCROLLBAR_THICKNESS, h);
v_rect = Rect::new(
bounds.width - SCROLLBAR_THICKNESS,
0.0,
SCROLLBAR_THICKNESS,
h,
);
} else {
child.origin = Point::new(bounds.x, bounds.y);
child.size = Size::ZERO;
}
} else if Some(child.id) == self.h_scrollbar_id {
if show_h {
let w = if show_v {
(bounds.width - SCROLLBAR_THICKNESS).max(0.0)
} else {
bounds.width
};
child.origin =
Point::new(bounds.x, bounds.y + bounds.height - SCROLLBAR_THICKNESS);
child.size = Size::new(w, SCROLLBAR_THICKNESS);
h_rect = Rect::new(
0.0,
bounds.height - SCROLLBAR_THICKNESS,
w,
SCROLLBAR_THICKNESS,
);
} else {
child.origin = Point::new(bounds.x, bounds.y);
child.size = Size::ZERO;
}
}
}
self.v_scrollbar_bounds.set(v_rect);
self.h_scrollbar_bounds.set(h_rect);
}
fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
let bg = {
let st = self.state.borrow();
match &st.background_prop {
Some(p) => p.resolve(ctx.theme, true),
None => ctx.theme.colors.editor_bg,
}
};
canvas.fill_rect(bounds, bg);
let focused = self.state.borrow().focus_signal.get();
let border = if focused {
ctx.theme.colors.border_focused
} else {
ctx.theme.colors.border
};
canvas.stroke_rect(bounds, border, 1.0);
}
fn children(&self) -> Vec<WidgetId> {
let mut ids = Vec::with_capacity(3);
ids.extend(self.body_id);
ids.extend(self.v_scrollbar_id);
ids.extend(self.h_scrollbar_id);
ids
}
fn clips_children(&self) -> bool {
true
}
}
pub(crate) struct LogViewBody {
state: SharedState,
}
impl std::fmt::Debug for LogViewBody {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LogViewBody").finish_non_exhaustive()
}
}
pub(crate) fn log_body_for(state: &SharedState) -> LogViewBody {
LogViewBody {
state: state.clone(),
}
}
impl Widget for LogViewBody {
fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
let self_id = ctx.self_id();
let registry = ctx.binding_registry();
let st = self.state.borrow();
st.document_version
.bind_to(self_id, registry, BindingLevel::RepaintOnly);
if let Some(log) = st.log.as_ref() {
log.a11y_version
.bind_to(self_id, registry, BindingLevel::AccessibilityOnly);
}
for sig in [&st.scroll_x, &st.scroll_y] {
sig.bind_to(self_id, registry, BindingLevel::RepaintOnly);
}
st.cursor_position
.bind_to(self_id, registry, BindingLevel::RepaintOnly);
st.cursor_position
.bind_to(self_id, registry, BindingLevel::AccessibilityOnly);
st.cursor_anchor
.bind_to(self_id, registry, BindingLevel::RepaintOnly);
st.cursor_anchor
.bind_to(self_id, registry, BindingLevel::AccessibilityOnly);
st.has_selection
.bind_to(self_id, registry, BindingLevel::RepaintOnly);
Vec::new()
}
fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
let w = proposal.width.unwrap_or(200.0).max(0.0);
let h = proposal.height.unwrap_or(100.0).max(0.0);
Size::new(w, h).into()
}
fn place_children(
&self,
bounds: Rect,
_proposal: SizeProposal,
_children: &mut [WidgetPlacement],
_ctx: &LayoutContext,
) {
self.state.borrow_mut().sync_viewport(bounds);
}
fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
let mut st = self.state.borrow_mut();
let new_text = match &st.text_color_prop {
Some(p) => p.resolve(ctx.theme, true).to_array(),
None => ctx.theme.colors.editor_fg.to_array(),
};
st.engine.set_text_color(new_text);
let new_sel = if let Some(p) = st.selection_color_prop.as_ref() {
p.resolve(ctx.theme, true).to_array()
} else if ctx.window_active {
ctx.theme.colors.editor_selection_bg.to_array()
} else {
ctx.theme.colors.selection_bg_inactive.to_array()
};
st.engine.set_selection_color(new_sel);
let target_scale = st.effective_font_scale(ctx.text_scale);
let old_scale = st.last_font_scale;
if old_scale.is_nan() || (old_scale - target_scale).abs() > f32::EPSILON {
st.last_font_scale = target_scale;
st.engine.set_font_scale(target_scale);
if old_scale.is_finite() && old_scale > 0.0 {
let ratio = target_scale / old_scale;
let scaled = st.scroll_y.get() * ratio;
st.scroll_y.set_if_changed(scaled);
}
if let Some(l) = st.log.as_mut() {
l.needs_rewindow = true;
l.row_height = 0.0;
}
}
st.sync_viewport(bounds);
log_stream::ensure_window(&mut st, false);
let scroll_offset = st.scroll_y.get();
let affinity = st.cursor_affinity;
let cursors: Vec<teksilo_text::CursorDisplay> = st
.all_carets()
.map(|c| teksilo_text::CursorDisplay {
position: c.position(),
anchor: c.anchor(),
affinity,
visible: false,
selected_cells: Vec::new(),
})
.collect();
st.engine.set_cursors(&cursors);
st.engine.set_scroll_offset(scroll_offset);
canvas.set_clip(bounds);
let CodeEditorState {
ref mut engine,
ref document,
ref mut image_cache,
..
} = *st;
engine.with_render_frame(|frame| {
crate::rich_text::paint::paint_frame(
canvas,
crate::rich_text::paint::PaintParams {
frame,
origin: Point::new(bounds.x, bounds.y),
document,
image_cache,
image_resolver: None,
selection: None,
selection_color: [0.0; 4],
selected_image_out: None,
resize_preview: None,
draw_caret: false,
},
);
});
canvas.clear_clip();
}
fn accessibility(&self, builder: &mut AccessNodeBuilder) {
use teksilo_core::accesskit::Live;
let st = self.state.borrow();
super::a11y::build_log_a11y(&st, builder);
if st.announce_appends {
builder.inner_mut().set_live(Live::Polite);
}
}
fn clips_children(&self) -> bool {
true
}
}
#[derive(Clone)]
pub struct LogViewHandle {
state: SharedState,
}
impl std::fmt::Debug for LogViewHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LogViewHandle").finish_non_exhaustive()
}
}
impl LogViewHandle {
pub fn append(&self, text: &str) {
self.enqueue(text);
}
pub fn append_line(&self, line: &str) {
self.enqueue(line);
}
pub fn append_lines<I, S>(&self, lines: I)
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
{
let st = self.state.borrow();
let Some(log) = st.log.as_ref() else { return };
let mut q = log.pending.lock().expect("log append queue poisoned");
for line in lines {
for piece in line.as_ref().split('\n') {
q.push_back(piece.to_string());
}
}
}
self.wake();
}
fn enqueue(&self, text: &str) {
{
let st = self.state.borrow();
let Some(log) = st.log.as_ref() else { return };
let mut q = log.pending.lock().expect("log append queue poisoned");
let body = text.strip_suffix('\n').unwrap_or(text);
for piece in body.split('\n') {
q.push_back(piece.to_string());
}
}
self.wake();
}
pub fn clear(&self) {
{
let mut st = self.state.borrow_mut();
if let Some(log) = st.log.as_ref() {
log.pending
.lock()
.expect("log append queue poisoned")
.clear();
}
let _ = st.document.set_plain_text("");
if let Some(log) = st.log.as_mut() {
log.pristine = true;
log.total = 0;
log.anchor = None;
log.last_window = None;
log.needs_rewindow = true;
}
st.line_count.set_if_changed(0);
st.scroll_x.set_if_changed(0.0);
st.scroll_y.set_if_changed(0.0);
}
self.wake();
}
pub fn scroll_to_bottom(&self) {
{
let st = self.state.borrow();
let max_y = st.max_scroll_y.get();
st.scroll_y.set_if_changed(max_y);
}
self.wake();
}
pub fn line_count(&self) -> teksilo_core::Signal<usize> {
self.state.borrow().line_count.clone()
}
pub fn document_version(&self) -> teksilo_core::Signal<u64> {
self.state.borrow().document_version.clone()
}
pub fn scroll_y(&self) -> teksilo_core::Signal<f32> {
self.state.borrow().scroll_y.clone()
}
pub fn max_scroll_y(&self) -> teksilo_core::Signal<f32> {
self.state.borrow().max_scroll_y.clone()
}
fn wake(&self) {
if let Some(handle) = &self.state.borrow().frame_request {
handle.set(true);
}
}
#[cfg(test)]
pub(crate) fn state_handle(&self) -> SharedState {
self.state.clone()
}
#[cfg(test)]
pub(crate) fn from_state_for_test(state: SharedState) -> Self {
Self { state }
}
}