abacus/abacus-ui/src/editor.rs

668 lines
23 KiB
Rust

use std::ops::Range;
use abacus_core::Output;
use clipboard::ClipboardProvider;
use druid::{
piet::{CairoTextLayout, Text, TextAttribute, TextLayout, TextLayoutBuilder},
widget::{Container, Flex, Label, Padding, Svg, SvgData},
Color, Data, Event, FontDescriptor, FontFamily, FontWeight, Lens, LifeCycle, PaintCtx, Rect,
RenderContext, Target, Widget, WidgetExt,
};
use ropey::Rope;
use syntect::easy::HighlightLines;
use syntect::highlighting::{Style, ThemeSet};
use syntect::parsing::SyntaxSet;
use crate::{app_header::ToolbarButtonController, Block};
const FONT_SIZE: f64 = 16.0;
#[derive(Clone, Data, PartialEq, Eq, Debug)]
pub enum EditMode {
Normal,
Insert,
}
#[derive(Data, Lens, Clone, PartialEq, Debug)]
pub struct EditorData {
#[data(same_fn = "PartialEq::eq")]
pub content: Rope,
pub cursor_pos: usize,
pub mode: EditMode,
pub cursor_opactiy: f64,
pub cursor_fade: f64,
pub selection_pos: usize,
}
impl Default for EditorData {
fn default() -> Self {
Self {
content: Rope::from_str("let x = 1;\nx+1"),
cursor_pos: 5,
selection_pos: 5,
mode: EditMode::Normal,
cursor_opactiy: 255.0,
cursor_fade: 1.0,
}
}
}
impl EditorData {
pub fn new(content: &str) -> Self {
println!("new block: {}", content);
Self {
content: Rope::from_str(content),
..Default::default()
}
}
pub fn move_cursor(&mut self, idx: usize) {
if idx <= self.content.len_chars() {
self.cursor_pos = idx;
self.deselect();
// dbg!(self.content.char(self.cursor_pos));
}
}
pub fn select_range(&self) -> Range<usize> {
if self.cursor_pos > self.selection_pos {
self.selection_pos..self.cursor_pos
} else {
self.cursor_pos..self.selection_pos
}
}
pub fn push_str(&mut self, s: &str) {
if self.selection_pos != self.cursor_pos {
self.content.remove(self.select_range());
self.content.insert(self.select_range().start, s);
} else {
self.content.insert(self.cursor_pos, s);
}
self.move_cursor(self.select_range().start + s.len());
}
pub fn push(&mut self, c: char) {
self.content.insert_char(self.cursor_pos, c);
self.cursor_right();
}
// pub fn cursor_to_end(&mut self) {
// self.move_cursor(self.content.len_chars());
// }
pub fn deselect(&mut self) {
self.selection_pos = self.cursor_pos;
}
pub fn delete_char_forward(&mut self) {
if !self.select_range().is_empty() {
let range = self.select_range();
self.content.remove(self.select_range());
self.move_cursor(range.start);
return;
}
if self.cursor_pos < self.content.len_chars() {
self.content.remove((self.cursor_pos)..self.cursor_pos + 1);
}
}
pub fn delete_char_back(&mut self) {
// Delete selection
if !self.select_range().is_empty() {
let range = self.select_range();
self.content.remove(self.select_range());
self.move_cursor(range.start);
return;
}
// Cant delete character sif we are at the start of the buffer
if self.cursor_pos > 0 {
self.content.remove((self.cursor_pos - 1)..self.cursor_pos);
self.cursor_left();
}
}
pub fn cursor_left(&mut self) {
if self.cursor_pos > 0 {
self.move_cursor(self.cursor_pos - 1);
}
}
pub fn cursor_up(&mut self) {
let line_idx = self.content.char_to_line(self.cursor_pos);
if line_idx > 0 {
let start_of_current_line = self.content.line_to_char(line_idx);
let line_pos = self.cursor_pos - start_of_current_line;
let up_line_start = self.content.line_to_char(line_idx - 1);
let up_line = self.content.line(line_idx - 1);
self.move_cursor(up_line_start + line_pos.min(up_line.len_chars() - 1));
}
}
pub fn cursor_down(&mut self) {
let line_idx = self.content.char_to_line(self.cursor_pos);
if line_idx < self.content.len_lines() - 1 {
let start_of_current_line = self.content.line_to_char(line_idx);
let line_pos = self.cursor_pos - start_of_current_line;
let start_of_next_line = self.content.line_to_char(line_idx + 1);
let next_line_len = self.content.line(line_idx + 1).len_chars();
self.move_cursor(start_of_next_line + line_pos.min(next_line_len));
}
}
pub fn cursor_right(&mut self) {
if self.cursor_pos < self.content.len_chars() {
self.move_cursor(self.cursor_pos + 1);
}
}
pub fn select_to_end_of_line(&mut self) {
let line_idx = self.content.char_to_line(self.cursor_pos);
let start_of_line = self.content.line_to_char(line_idx);
let line = self.content.line(line_idx);
if line_idx == self.content.len_lines() - 1 {
self.selection_pos = start_of_line + line.len_chars();
} else {
self.selection_pos = start_of_line + line.len_chars() - 1;
}
}
pub fn cursor_to_end_of_line(&mut self) {
let line_idx = self.content.char_to_line(self.cursor_pos);
let start_of_line = self.content.line_to_char(line_idx);
let line = self.content.line(line_idx);
if line_idx == self.content.len_lines() - 1 {
self.move_cursor(start_of_line + line.len_chars());
} else {
self.move_cursor(start_of_line + line.len_chars() - 1);
}
}
pub fn select_to_start_of_line(&mut self) {
let start_of_line = self
.content
.line_to_char(self.content.char_to_line(self.cursor_pos));
self.selection_pos = start_of_line;
}
pub fn cursor_to_start_of_line(&mut self) {
let start_of_line = self
.content
.line_to_char(self.content.char_to_line(self.cursor_pos));
self.move_cursor(start_of_line);
}
pub fn select_all(&mut self) {
self.selection_pos = self.content.len_chars();
self.cursor_pos = 0;
}
pub fn select_left(&mut self) {
if self.cursor_pos > 0 {
self.selection_pos -= 1;
}
}
pub fn select_right(&mut self) {
if self.cursor_pos < self.content.len_chars() {
self.selection_pos += 1;
}
}
}
pub struct AbacusEditor {
syntax_set: SyntaxSet,
theme_set: ThemeSet,
}
impl Default for AbacusEditor {
fn default() -> Self {
let syntax_set = SyntaxSet::load_defaults_newlines();
let theme_set = ThemeSet::load_defaults();
Self {
syntax_set,
theme_set,
}
}
}
impl AbacusEditor {
fn paint_cursor(&self, ctx: &mut PaintCtx, data: &EditorData, layout: &CairoTextLayout) {
dbg!(data.cursor_pos);
if data.mode == EditMode::Insert {
if data.cursor_pos == 0 {
let rects = layout.rects_for_range(0..1);
let rect = rects.first().unwrap();
let rect = Rect::new(
rect.min_x() + 1.0,
rect.min_y() + 2.0,
rect.min_x() + 3.0,
rect.max_y() - 2.0,
);
ctx.fill(
rect,
&Color::rgba8(255, 255, 255, data.cursor_opactiy.floor() as u8),
);
} else if let Some(char_rect) = layout
.rects_for_range((data.cursor_pos.max(1) - 1)..data.cursor_pos)
.last()
{
let cursor_rect = Rect::new(
char_rect.max_x() + 3.0,
char_rect.min_y() + 2.0,
char_rect.max_x() + 5.0,
char_rect.max_y() - 2.0,
);
ctx.fill(
cursor_rect,
&Color::rgba8(255, 255, 255, data.cursor_opactiy.floor() as u8),
);
}
}
if data.mode == EditMode::Normal {
let c_pos = data.cursor_pos.min(data.content.len_chars() - 1);
if let Some(char_rect) = layout.rects_for_range(c_pos..(c_pos + 1)).last() {
if char_rect.width() == 0. {
let rect = Rect::new(
char_rect.min_x(),
char_rect.min_y(),
char_rect.min_x() + 10.,
char_rect.max_y(),
);
ctx.fill(
rect,
&Color::rgba8(255, 255, 255, data.cursor_opactiy.floor() as u8),
);
} else {
ctx.fill(
char_rect,
&Color::rgba8(255, 255, 255, data.cursor_opactiy.floor() as u8),
);
};
}
}
}
pub fn build_highlighted_layout(
&self,
ctx: &mut PaintCtx,
data: &EditorData,
) -> CairoTextLayout {
let syntax = self.syntax_set.find_syntax_by_extension("rs").unwrap();
let mut h = HighlightLines::new(syntax, &self.theme_set.themes["base16-mocha.dark"]);
let mut layout = ctx
.text()
.new_text_layout(data.content.to_string())
.font(FontFamily::MONOSPACE, FONT_SIZE);
let mut pos = 0;
for line in data.content.lines() {
let s = line.to_string();
let ranges: Vec<(Style, &str)> = h.highlight_line(&s, &self.syntax_set).unwrap();
for (style, txt) in ranges {
layout = layout.range_attribute(
pos..(pos + txt.len()),
TextAttribute::TextColor(Color::rgba8(
style.foreground.r,
style.foreground.g,
style.foreground.b,
style.foreground.a,
)),
);
pos += txt.len();
}
}
layout.build().unwrap()
}
}
impl Widget<EditorData> for AbacusEditor {
fn paint(&mut self, ctx: &mut druid::PaintCtx, data: &EditorData, _env: &druid::Env) {
println!("test");
// let size = ctx.size();
// let rect = size.to_rect();
// if ctx.is_focused() {
// ctx.fill(rect, &Color::rgb8(30, 30, 30));
// } else {
// ctx.fill(rect, &Color::rgb8(20, 20, 20));
// }
if data.content.len_chars() == 0 {
return;
}
let layout = self.build_highlighted_layout(ctx, data);
if ctx.has_focus() {
self.paint_cursor(ctx, data, &layout);
}
if data.selection_pos != data.cursor_pos {
let rects = layout.rects_for_range(data.select_range());
for rect in rects.iter() {
ctx.fill(rect, &Color::rgb8(90, 90, 90));
}
}
ctx.draw_text(&layout, (0.0, 0.0));
}
fn event(
&mut self,
ctx: &mut druid::EventCtx,
event: &druid::Event,
data: &mut EditorData,
_env: &druid::Env,
) {
match event {
Event::KeyDown(e) => {
match &e.key {
druid::keyboard_types::Key::Character(ch) if e.mods.ctrl() => {
match ch.as_ref() {
"a" => {
if e.mods.ctrl() {
data.select_all();
}
}
"c" => {
if data.cursor_pos != data.selection_pos {
let mut cb: clipboard::ClipboardContext =
clipboard::ClipboardProvider::new().unwrap();
if let Some(slice) = data.content.get_slice(data.select_range())
{
let _ = cb.set_contents(slice.to_string());
}
}
}
"v" => {
let mut cb: clipboard::ClipboardContext =
clipboard::ClipboardProvider::new().unwrap();
data.push_str(&cb.get_contents().unwrap_or_default());
ctx.request_paint();
ctx.request_layout();
}
_ => {}
}
}
druid::keyboard_types::Key::Character(c) if data.mode == EditMode::Insert => {
data.push_str(c);
ctx.request_paint();
}
druid::keyboard_types::Key::Character(ch) if data.mode == EditMode::Normal => {
match ch.as_ref() {
"i" => {
data.mode = EditMode::Insert;
}
"O" => {
data.mode = EditMode::Insert;
data.cursor_to_start_of_line();
data.content.insert(data.cursor_pos, "\n");
}
"A" => {
data.mode = EditMode::Insert;
data.cursor_to_end_of_line();
}
"a" => {
if e.mods.ctrl() {
data.select_all();
}
}
"h" => {
data.cursor_left();
}
"j" => {
data.cursor_down();
}
"k" => {
data.cursor_up();
}
"l" => {
data.cursor_right();
}
"x" => {
data.delete_char_forward();
}
_ => {}
}
}
druid::keyboard_types::Key::Enter if e.mods.ctrl() => {
ctx.submit_command(crate::commands::PROCESS_WORKBOOK.to(Target::Global));
}
druid::keyboard_types::Key::Enter => {
data.push('\n');
ctx.request_layout();
ctx.request_paint();
}
druid::keyboard_types::Key::Backspace => {
data.delete_char_back();
ctx.request_layout();
}
druid::keyboard_types::Key::Delete => {
data.delete_char_forward();
ctx.request_layout();
}
druid::keyboard_types::Key::Escape => {
data.mode = EditMode::Normal;
}
druid::keyboard_types::Key::ArrowLeft if e.mods.shift() => {
data.select_left();
}
druid::keyboard_types::Key::ArrowRight if e.mods.shift() => {
data.select_right();
}
druid::keyboard_types::Key::ArrowLeft if !e.mods.shift() => {
data.cursor_left();
}
druid::keyboard_types::Key::ArrowRight if !e.mods.shift() => {
data.cursor_right();
}
druid::keyboard_types::Key::ArrowUp if !e.mods.shift() => {
data.cursor_up();
}
druid::keyboard_types::Key::ArrowDown if !e.mods.shift() => {
data.cursor_down();
}
druid::keyboard_types::Key::Tab => {
data.push_str(" ");
}
druid::keyboard_types::Key::End if e.mods.shift() => {
data.select_to_end_of_line();
}
druid::keyboard_types::Key::Home if e.mods.shift() => {
data.select_to_start_of_line();
}
druid::keyboard_types::Key::End => {
data.cursor_to_end_of_line();
data.deselect();
}
druid::keyboard_types::Key::Home => {
data.cursor_to_start_of_line();
data.deselect();
}
e => {
dbg!(e);
}
}
ctx.request_paint();
}
Event::MouseDown(e) => {
if !ctx.is_focused() {
ctx.request_focus();
}
let layout = ctx
.text()
.new_text_layout(data.content.to_string())
.font(FontFamily::MONOSPACE, FONT_SIZE)
.build()
.unwrap();
let pos = layout.hit_test_point(e.pos);
if pos.idx != data.cursor_pos {
data.cursor_pos = pos.idx;
data.deselect();
ctx.request_paint();
}
}
Event::MouseMove(e) => {
let layout = ctx
.text()
.new_text_layout(data.content.to_string())
.font(FontFamily::MONOSPACE, FONT_SIZE)
.text_color(Color::rgb8(255, 255, 255))
.build()
.unwrap();
let pos = layout.hit_test_point(e.pos);
if e.buttons.has_left() {
let new_pos = (pos.idx + 1).min(data.content.len_chars());
if new_pos != data.selection_pos {
if new_pos > data.cursor_pos {
data.selection_pos = (pos.idx + 1).min(data.content.len_chars());
} else {
data.selection_pos = pos.idx.max(1) - 1;
}
ctx.request_paint();
}
}
}
// Event::AnimFrame(e) => {
// data.cursor_opactiy += ((*e as f64) * 0.00000065) * data.cursor_fade;
// if data.cursor_opactiy >= 255.0 {
// data.cursor_opactiy = 255.0;
// data.cursor_fade *= -1.0;
// } else if data.cursor_opactiy <= 0.0 {
// data.cursor_opactiy = 0.1;
// data.cursor_fade *= -1.0;
// }
// if ctx.has_focus() {
// ctx.request_paint();
// ctx.request_anim_frame();
// }
// }
_ => {}
}
}
fn lifecycle(
&mut self,
ctx: &mut druid::LifeCycleCtx,
event: &druid::LifeCycle,
_data: &EditorData,
_env: &druid::Env,
) {
match event {
LifeCycle::FocusChanged(_) => {
ctx.request_paint();
}
LifeCycle::WidgetAdded => {
// ctx.register_text_input(document)
}
LifeCycle::BuildFocusChain => {
ctx.register_for_focus();
}
_ => {}
}
}
fn update(
&mut self,
ctx: &mut druid::UpdateCtx,
old_data: &EditorData,
data: &EditorData,
_env: &druid::Env,
) {
if old_data != data {
ctx.request_paint();
ctx.request_layout();
}
}
fn layout(
&mut self,
ctx: &mut druid::LayoutCtx,
bc: &druid::BoxConstraints,
data: &EditorData,
_env: &druid::Env,
) -> druid::Size {
let layout = ctx
.text()
.new_text_layout(data.content.to_string())
.font(FontFamily::MONOSPACE, FONT_SIZE)
.build()
.unwrap();
(bc.max().width, layout.size().height).into()
}
}
pub fn editor_header() -> impl Widget<Block> {
let ban_svg = include_str!("../assets/ban.svg")
.parse::<SvgData>()
.unwrap_or_default();
let trash_svg = include_str!("../assets/trash.svg")
.parse::<SvgData>()
.unwrap_or_default();
let run_svg = include_str!("../assets/play.svg")
.parse::<SvgData>()
.unwrap_or_default();
Container::new(
Flex::row()
.must_fill_main_axis(true)
.with_spacer(20.0)
.with_child(
Label::dynamic(|data: &Block, _| data.name.clone())
.with_font(
FontDescriptor::new(FontFamily::SANS_SERIF)
.with_weight(FontWeight::BOLD)
.with_size(14.0),
)
.padding(5.0),
)
.with_flex_spacer(1.0)
.with_child(
Container::new(Padding::new(10.0, Svg::new(run_svg).fix_width(10.0)))
.controller(ToolbarButtonController::new(Color::rgb8(50, 50, 50)))
.on_click(|ctx, data: &mut Block, _env| {
ctx.submit_command(
crate::commands::PROCESS_BLOCK
.with(data.index)
.to(Target::Global),
);
}),
)
.with_spacer(10.0)
.with_child(
Container::new(Padding::new(10.0, Svg::new(ban_svg).fix_width(10.0)))
.controller(ToolbarButtonController::new(Color::rgb8(50, 50, 50)))
.on_click(|_ctx, data: &mut Block, _env| data.output = Output::None),
)
.with_spacer(10.0)
.with_child(
Container::new(Padding::new(10.0, Svg::new(trash_svg).fix_width(10.0)))
.controller(ToolbarButtonController::new(Color::rgb8(50, 50, 50)))
.on_click(|ctx, data: &mut Block, _env| {
ctx.submit_command(
crate::commands::DELETE_BLOCK
.with(data.index)
.to(Target::Global),
);
}),
)
.with_spacer(20.0),
)
.background(Color::rgb8(35, 35, 35))
.fix_height(30.0)
}