use teksilo_canvas::{Canvas, Rect, Size, SizeProposal};
use teksilo_core::accessibility::AccessNodeBuilder;
use teksilo_core::build_context::BuildContext;
use teksilo_core::widget::{LayoutContext, PaintContext, Widget, WidgetPlacement};
use teksilo_core::widget_id::WidgetId;
use super::state::SharedState;
const GUTTER_PAD_LEADING: f32 = 8.0;
const GUTTER_PAD_TRAILING: f32 = 12.0;
fn digits(n: usize) -> usize {
let mut d = 1;
let mut v = n;
while v >= 10 {
v /= 10;
d += 1;
}
d
}
pub(crate) struct CodeGutter {
state: SharedState,
}
impl std::fmt::Debug for CodeGutter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CodeGutter").finish_non_exhaustive()
}
}
impl CodeGutter {
pub(crate) fn new(state: &SharedState) -> Self {
Self {
state: state.clone(),
}
}
}
impl Widget for CodeGutter {
fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
use teksilo_core::binding::BindingLevel;
let self_id = ctx.self_id();
let registry = ctx.binding_registry();
let st = self.state.borrow();
st.scroll_y
.bind_to(self_id, registry, BindingLevel::RepaintOnly);
st.cursor_position
.bind_to(self_id, registry, BindingLevel::RepaintOnly);
st.line_count
.bind_to(self_id, registry, BindingLevel::Relayout);
Vec::new()
}
fn layout_response(
&self,
proposal: SizeProposal,
ctx: &LayoutContext,
) -> teksilo_core::widget::LayoutResponse {
let st = self.state.borrow();
let widest = st.line_count.get().max(1);
drop(st);
let style = number_style(ctx.theme);
let label = "9".repeat(digits(widest));
let text_w = match ctx.text_backend {
Some(backend) => {
backend
.borrow_mut()
.layout_single_line(&label, &style, None)
.width
}
None => label.len() as f32 * 8.0,
};
let w = GUTTER_PAD_LEADING + text_w + GUTTER_PAD_TRAILING;
let h = proposal.height.unwrap_or(0.0).max(0.0);
Size::new(w, h).into()
}
fn place_children(
&self,
_bounds: Rect,
_proposal: SizeProposal,
_children: &mut [WidgetPlacement],
_ctx: &LayoutContext,
) {
}
fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
let st = self.state.borrow();
if !st.engine.has_full_layout() {
return;
}
let scroll_y = st.scroll_y.get();
let line_h = st.engine.default_line_height().max(1.0);
let total = st.line_count.get();
if total == 0 || line_h <= 0.0 {
return;
}
let first = (scroll_y / line_h).floor().max(0.0) as usize;
let visible = (bounds.height / line_h).ceil() as usize + 1;
let last = (first + visible).min(total);
let caret_line = caret_line_index(&st, line_h);
let style = number_style(ctx.theme);
let number_color = ctx.theme.colors.editor_gutter_fg;
let current_color = ctx.theme.colors.editor_fg;
let right_edge = bounds.x + bounds.width - GUTTER_PAD_TRAILING;
canvas.set_clip(bounds);
for line in first..last {
let y = line as f32 * line_h - scroll_y + bounds.y;
let label = (line + 1).to_string();
let text_w = match canvas.text_backend() {
Some(b) => {
b.borrow_mut()
.layout_single_line(&label, &style, None)
.width
}
None => return,
};
let slot = Rect::new(right_edge - text_w, y, text_w, line_h);
canvas.draw_text(
&label,
slot,
&style,
if Some(line) == caret_line {
current_color
} else {
number_color
},
);
}
canvas.clear_clip();
}
fn accessibility(&self, builder: &mut AccessNodeBuilder) {
builder.set_hidden();
}
fn clips_children(&self) -> bool {
true
}
}
fn caret_line_index(st: &super::state::CodeEditorState, line_h: f32) -> Option<usize> {
let caret = st
.engine
.caret_rect(st.cursor.position(), st.cursor_affinity);
let y = caret[1];
if line_h <= 0.0 {
return None;
}
Some((y / line_h).floor().max(0.0) as usize)
}
fn number_style(theme: &teksilo_core::styles::Theme) -> teksilo_tokens::TextStyle {
theme.typography.body.clone()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn digit_count_grows_with_the_number() {
assert_eq!(digits(0), 1);
assert_eq!(digits(1), 1);
assert_eq!(digits(9), 1);
assert_eq!(digits(10), 2);
assert_eq!(digits(99), 2);
assert_eq!(digits(100), 3);
assert_eq!(digits(99_999), 5);
assert_eq!(digits(100_000), 6);
}
}