use std::{fmt::Alignment, sync::Once};
use duat_core::{
buffer::Buffer,
context::Handle,
data::Pass,
form,
hook::{self, BufferUpdated, OnMouseEvent},
mode::{MouseButton, MouseEventKind},
text::{Builder, Spacer, Text, TextMut},
ui::{PushSpecs, Side, Widget},
};
pub struct LineNumbers {
text: Text,
pub relative: bool,
pub align: Alignment,
pub main_align: Alignment,
pub show_wraps: bool,
}
impl LineNumbers {
pub fn builder() -> LineNumbersOpts {
LineNumbersOpts::default()
}
fn calculate_width(&self, pa: &Pass, buffer: &Handle) -> f32 {
let len = buffer.read(pa).text().end_point().line();
len.ilog10() as f32
}
fn form_text(&self, pa: &Pass, buffer: &Handle) -> Text {
let (main_line_num, printed_line_numbers) = {
let printed_line_numbers = buffer.printed_line_numbers(pa);
let buf = buffer.read(pa);
let main_line = if buf.selections().is_empty() {
usize::MAX
} else {
buf.selections().main().caret().line()
};
(main_line, printed_line_numbers)
};
let mut builder = Text::builder();
let mut last_was_ghost = false;
for (idx, line) in printed_line_numbers.iter().enumerate() {
if line.is_ghost {
last_was_ghost = true;
builder.push("\n");
continue;
}
let align = if line.number == main_line_num {
self.main_align
} else {
self.align
};
if align != Alignment::Left {
builder.push(Spacer);
}
let is_wrapped = line.is_wrapped && idx > 0 && !last_was_ghost;
match (line.number == main_line_num, is_wrapped) {
(false, false) => {}
(true, false) => builder.push(form::id_of!("linenum.main")),
(false, true) => builder.push(form::id_of!("linenum.wrapped")),
(true, true) => builder.push(form::id_of!("linenum.wrapped.main")),
}
push_text(&mut builder, line.number, main_line_num, is_wrapped, self);
if align == Alignment::Center {
builder.push(Spacer);
}
builder.push("\n");
builder.push(form::DEFAULT_ID);
last_was_ghost = false;
}
builder.build()
}
}
impl Widget for LineNumbers {
fn text(&self) -> &Text {
&self.text
}
fn text_mut(&mut self) -> TextMut<'_> {
self.text.as_mut()
}
}
#[derive(Clone, Copy, Debug)]
pub struct LineNumbersOpts {
pub relative: bool,
pub align: Alignment,
pub main_align: Alignment,
pub show_wraps: bool,
pub on_the_right: bool,
}
impl LineNumbersOpts {
pub const fn new() -> Self {
Self {
relative: false,
align: Alignment::Left,
main_align: Alignment::Right,
show_wraps: false,
on_the_right: false,
}
}
pub fn push_on(self, pa: &mut Pass, buffer: &Handle) -> Handle<LineNumbers> {
static ONCE: Once = Once::new();
ONCE.call_once(|| {
hook::add::<BufferUpdated>(|pa, buffer| {
for (linenumbers, _) in buffer.get_related::<LineNumbers>(pa) {
let width = linenumbers.read(pa).calculate_width(pa, buffer);
linenumbers.area().set_width(pa, width + 1.0).unwrap();
linenumbers.write(pa).text = linenumbers.read(pa).form_text(pa, buffer);
}
})
.lateness(usize::MAX);
hook::add::<OnMouseEvent<LineNumbers>>(|pa, event| {
let line = |pa, handle: &Handle| {
let lines = handle.printed_line_numbers(pa);
event
.points
.and_then(|tpp| lines.get(tpp.points().real.line()))
.map(|line| line.number)
.unwrap_or(handle.text(pa).end_point().line())
};
let (buffer, _) = event.handle.get_related::<Buffer>(pa).remove(0);
match event.kind {
MouseEventKind::Down(MouseButton::Left) => {
let line = line(pa, &buffer);
buffer.selections_mut(pa).remove_extras();
buffer.edit_main(pa, |mut c| {
c.unset_anchor();
c.move_to_coords(line, 0)
})
}
MouseEventKind::Drag(MouseButton::Left) => {
let line = line(pa, &buffer);
buffer.selections_mut(pa).remove_extras();
buffer.edit_main(pa, |mut c| {
c.set_anchor_if_needed();
c.move_to_coords(line, 0)
})
}
MouseEventKind::ScrollDown => {
let opts = buffer.opts(pa);
let (buf, area) = buffer.write_with_area(pa);
area.scroll_ver(buf.text(), 3, opts);
}
MouseEventKind::ScrollUp => {
let opts = buffer.opts(pa);
let (buf, area) = buffer.write_with_area(pa);
area.scroll_ver(buf.text(), -3, opts);
}
_ => {}
}
});
});
let mut linenumbers = LineNumbers {
text: Text::default(),
relative: self.relative,
align: self.align,
main_align: self.main_align,
show_wraps: self.show_wraps,
};
linenumbers.text = linenumbers.form_text(pa, buffer);
let specs = PushSpecs {
side: if self.on_the_right {
Side::Right
} else {
Side::Left
},
..Default::default()
};
let linenumbers = buffer.push_outer_widget(pa, linenumbers, specs);
let width = linenumbers.read(pa).calculate_width(pa, buffer);
linenumbers.area().set_width(pa, width + 1.0).unwrap();
linenumbers
}
}
impl Default for LineNumbersOpts {
fn default() -> Self {
Self::new()
}
}
fn push_text(b: &mut Builder, line: usize, main: usize, is_wrapped: bool, opts: &LineNumbers) {
if (!is_wrapped || opts.show_wraps) && main != usize::MAX {
b.push(if opts.relative {
if line != main {
line.abs_diff(main)
} else {
line + 1
}
} else {
line + 1
});
}
}