641 lines
23 KiB
Rust
641 lines
23 KiB
Rust
use abacus_core::Output;
|
|
use clipboard::ClipboardProvider;
|
|
|
|
use druid::{
|
|
piet::{PietTextLayout, Text, TextAttribute, TextLayout, TextLayoutBuilder},
|
|
widget::{Container, Flex, Label, Padding, Svg, SvgData},
|
|
Color, Event, FontDescriptor, FontFamily, FontWeight, LifeCycle, PaintCtx, Rect, RenderContext,
|
|
Target, Widget, WidgetExt,
|
|
};
|
|
|
|
use syntect::easy::HighlightLines;
|
|
use syntect::highlighting::{Style, ThemeSet};
|
|
use syntect::parsing::SyntaxSet;
|
|
|
|
use crate::{
|
|
app_header::ToolbarButtonController,
|
|
data::{EditMode, EditorData},
|
|
Block,
|
|
};
|
|
|
|
const FONT_SIZE: f64 = 16.0;
|
|
|
|
mod keymap {
|
|
pub const LINE_ABOVE: &str = "O";
|
|
pub const LINE_BELOW: &str = "o";
|
|
pub const DELETE_LINE: &str = "dd";
|
|
pub const EDIT_EOL: &str = "A";
|
|
pub const EDIT: &str = "i";
|
|
pub const EDIT_BOL: &str = "I";
|
|
pub const CURSOR_LEFT: &str = "h";
|
|
pub const CURSOR_RIGHT: &str = "l";
|
|
pub const CURSOR_UP: &str = "k";
|
|
pub const CURSOR_DOWN: &str = "j";
|
|
pub const DELETE_CHAR: &str = "x";
|
|
pub const DELETE_TO_EOL: &str = "D";
|
|
pub const WORD_FORWARD: &str = "w";
|
|
pub const WORD_BACK: &str = "b";
|
|
pub const SELECT_MODE: &str = "v";
|
|
pub const CHANGE_WORD_FORWARD: &str = "cw";
|
|
pub const CHANGE_WORD_BACKWARD: &str = "cb";
|
|
|
|
#[derive(Debug, Default)]
|
|
pub struct KeyList {
|
|
input_value: String,
|
|
commands: Vec<&'static str>,
|
|
}
|
|
|
|
impl KeyList {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
input_value: String::new(),
|
|
commands: vec![
|
|
LINE_ABOVE,
|
|
LINE_BELOW,
|
|
DELETE_LINE,
|
|
EDIT_EOL,
|
|
EDIT,
|
|
EDIT_BOL,
|
|
CURSOR_LEFT,
|
|
CURSOR_RIGHT,
|
|
CURSOR_UP,
|
|
CURSOR_DOWN,
|
|
DELETE_CHAR,
|
|
DELETE_TO_EOL,
|
|
WORD_FORWARD,
|
|
WORD_BACK,
|
|
SELECT_MODE,
|
|
CHANGE_WORD_FORWARD,
|
|
CHANGE_WORD_BACKWARD,
|
|
],
|
|
}
|
|
}
|
|
|
|
pub fn push(&mut self, s: &str) {
|
|
self.input_value.push_str(s);
|
|
if !self.valid() {
|
|
self.clear();
|
|
}
|
|
}
|
|
|
|
fn valid(&self) -> bool {
|
|
self.commands
|
|
.iter()
|
|
.any(|i| i.starts_with(&self.input_value))
|
|
}
|
|
|
|
pub fn clear(&mut self) {
|
|
self.input_value.clear();
|
|
}
|
|
|
|
pub fn get(&mut self) -> Option<&str> {
|
|
let v = self
|
|
.commands
|
|
.iter()
|
|
.find(|i| **i == self.input_value)
|
|
.cloned();
|
|
if v.is_some() {
|
|
self.clear();
|
|
}
|
|
v
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct AbacusEditor {
|
|
syntax_set: SyntaxSet,
|
|
theme_set: ThemeSet,
|
|
key_list: keymap::KeyList,
|
|
}
|
|
|
|
impl Default for AbacusEditor {
|
|
fn default() -> Self {
|
|
Self {
|
|
syntax_set: SyntaxSet::load_defaults_newlines(),
|
|
theme_set: ThemeSet::load_defaults(),
|
|
key_list: keymap::KeyList::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl AbacusEditor {
|
|
fn paint_cursor(&self, ctx: &mut PaintCtx, data: &EditorData, layout: &PietTextLayout) {
|
|
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(),
|
|
rect.min_x() + 1.0,
|
|
rect.max_y(),
|
|
);
|
|
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() - 1.0,
|
|
char_rect.min_y(),
|
|
char_rect.max_x() + 1.0,
|
|
char_rect.max_y(),
|
|
);
|
|
|
|
ctx.fill(
|
|
cursor_rect,
|
|
&Color::rgba8(255, 255, 255, data.cursor_opactiy.floor() as u8),
|
|
);
|
|
}
|
|
}
|
|
|
|
if data.mode == EditMode::Normal {
|
|
let char_rect = if data.content.len_chars() == 0 {
|
|
let rects = layout.rects_for_range(0..1);
|
|
rects.first().cloned()
|
|
} else if data.current_char() == Some('\n')
|
|
|| data.cursor_pos == data.content.len_chars()
|
|
{
|
|
let range = (data.cursor_pos.max(1) - 1)..data.cursor_pos;
|
|
layout.rects_for_range(range).last().cloned().map(|rect| {
|
|
Rect::new(rect.max_x(), rect.min_y(), rect.max_x() + 10., rect.max_y())
|
|
})
|
|
} else {
|
|
let range = data.cursor_pos..(data.cursor_pos + 1);
|
|
layout.rects_for_range(range).last().cloned()
|
|
};
|
|
|
|
if let Some(char_rect) = char_rect {
|
|
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,
|
|
) -> PietTextLayout {
|
|
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(if data.content.len_chars() == 0 {
|
|
String::from(" ")
|
|
} else {
|
|
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) {
|
|
let layout = self.build_highlighted_layout(ctx, data);
|
|
|
|
if data.selection_pos.is_some() {
|
|
let rects = layout.rects_for_range(data.select_range());
|
|
for rect in rects.iter() {
|
|
if rect.width() == 0.0 {
|
|
ctx.fill(
|
|
Rect::new(
|
|
rect.min_x(),
|
|
rect.min_y(),
|
|
rect.max_x() + 10.0,
|
|
rect.max_y(),
|
|
),
|
|
&Color::rgb8(90, 90, 90),
|
|
);
|
|
} else {
|
|
ctx.fill(rect, &Color::rgb8(90, 90, 90));
|
|
}
|
|
}
|
|
}
|
|
|
|
if ctx.has_focus() {
|
|
self.paint_cursor(ctx, data, &layout);
|
|
}
|
|
|
|
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.selection_pos.is_some() {
|
|
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 => {
|
|
self.key_list.push(ch);
|
|
|
|
match self.key_list.get() {
|
|
Some(keymap::DELETE_LINE) => {
|
|
data.delete_current_line();
|
|
data.deselect();
|
|
}
|
|
Some(keymap::DELETE_CHAR) => {
|
|
data.delete_char_forward();
|
|
data.deselect();
|
|
}
|
|
Some(keymap::EDIT) => {
|
|
data.mode = EditMode::Insert;
|
|
data.deselect();
|
|
}
|
|
Some(keymap::LINE_ABOVE) => {
|
|
data.mode = EditMode::Insert;
|
|
data.cursor_to_start_of_line();
|
|
data.content.insert(data.cursor_pos, "\n");
|
|
data.deselect();
|
|
}
|
|
Some(keymap::LINE_BELOW) => {
|
|
data.mode = EditMode::Insert;
|
|
data.cursor_to_end_of_line();
|
|
data.push_str("\n");
|
|
data.deselect();
|
|
}
|
|
Some(keymap::EDIT_EOL) => {
|
|
data.mode = EditMode::Insert;
|
|
data.cursor_to_end_of_line();
|
|
data.deselect();
|
|
}
|
|
Some(keymap::EDIT_BOL) => {
|
|
data.cursor_to_start_of_line();
|
|
data.mode = EditMode::Insert;
|
|
data.deselect();
|
|
}
|
|
Some(keymap::CURSOR_LEFT) => {
|
|
data.cursor_left();
|
|
}
|
|
Some(keymap::CURSOR_DOWN) => {
|
|
data.cursor_down();
|
|
}
|
|
Some(keymap::CURSOR_UP) => {
|
|
data.cursor_up();
|
|
}
|
|
Some(keymap::CURSOR_RIGHT) => {
|
|
data.cursor_right();
|
|
}
|
|
Some(keymap::DELETE_TO_EOL) => {
|
|
data.delete_to_eol();
|
|
data.deselect();
|
|
}
|
|
Some(keymap::WORD_FORWARD) => {
|
|
data.word_scan_forward();
|
|
}
|
|
Some(keymap::WORD_BACK) => {
|
|
data.word_scan_backward();
|
|
}
|
|
Some(keymap::SELECT_MODE) => {
|
|
data.selection_pos = Some(data.cursor_pos);
|
|
}
|
|
Some(keymap::CHANGE_WORD_FORWARD) => {
|
|
data.delete_word_forward();
|
|
data.insert_mode();
|
|
}
|
|
Some(keymap::CHANGE_WORD_BACKWARD) => {
|
|
data.delete_word_backward();
|
|
data.insert_mode();
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
druid::keyboard_types::Key::Enter if e.mods.ctrl() => {
|
|
ctx.submit_command(crate::commands::PROCESS_WORKBOOK.to(Target::Global));
|
|
}
|
|
druid::keyboard_types::Key::Enter if data.mode == EditMode::Insert => {
|
|
data.push('\n');
|
|
ctx.request_layout();
|
|
data.deselect();
|
|
}
|
|
druid::keyboard_types::Key::Enter if data.mode == EditMode::Normal => {
|
|
data.cursor_down();
|
|
data.deselect();
|
|
}
|
|
druid::keyboard_types::Key::Backspace => {
|
|
data.delete_char_back();
|
|
ctx.request_layout();
|
|
data.deselect();
|
|
}
|
|
druid::keyboard_types::Key::Delete => {
|
|
data.delete_char_forward();
|
|
ctx.request_layout();
|
|
data.deselect();
|
|
}
|
|
druid::keyboard_types::Key::Escape => {
|
|
data.normal_mode();
|
|
data.deselect();
|
|
}
|
|
|
|
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();
|
|
data.deselect();
|
|
}
|
|
druid::keyboard_types::Key::ArrowUp if !e.mods.shift() => {
|
|
data.cursor_up();
|
|
data.deselect();
|
|
}
|
|
druid::keyboard_types::Key::ArrowDown if !e.mods.shift() => {
|
|
data.cursor_down();
|
|
data.deselect();
|
|
}
|
|
|
|
druid::keyboard_types::Key::ArrowUp if e.mods.shift() => {
|
|
data.select_up();
|
|
}
|
|
druid::keyboard_types::Key::ArrowDown if e.mods.shift() => {
|
|
data.select_down();
|
|
}
|
|
druid::keyboard_types::Key::Tab => {
|
|
data.push_str(" ");
|
|
data.deselect();
|
|
}
|
|
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() {
|
|
if data.selection_pos.is_none() {
|
|
data.selection_pos = Some(data.cursor_pos);
|
|
}
|
|
let new_pos = (pos.idx + 1).min(data.content.len_chars());
|
|
if new_pos > data.cursor_pos {
|
|
data.cursor_pos = pos.idx.min(data.content.len_chars());
|
|
} else {
|
|
data.cursor_pos = pos.idx.max(1) - 1;
|
|
}
|
|
ctx.request_paint();
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
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)
|
|
.on_click(|ctx, data, _| {
|
|
ctx.submit_command(crate::commands::RENAME_BLOCK.with(data.index));
|
|
}),
|
|
)
|
|
.with_flex_spacer(1.0)
|
|
.with_child(
|
|
Label::dynamic(|data: &Block, _| {
|
|
match data.editor_data.mode {
|
|
EditMode::Insert => "EDIT",
|
|
EditMode::Normal => "",
|
|
}
|
|
.to_string()
|
|
})
|
|
.with_font(
|
|
FontDescriptor::new(FontFamily::SANS_SERIF)
|
|
.with_size(14.0)
|
|
.with_weight(FontWeight::BOLD),
|
|
)
|
|
.padding(5.0),
|
|
)
|
|
.with_spacer(10.0)
|
|
.with_child(
|
|
Label::dynamic(|data: &Block, _| {
|
|
format!(
|
|
"{}:{}",
|
|
data.editor_data.current_line_index() + 1,
|
|
data.editor_data.current_column() + 1,
|
|
)
|
|
})
|
|
.with_font(FontDescriptor::new(FontFamily::SANS_SERIF).with_size(14.0))
|
|
.padding(5.0),
|
|
)
|
|
.with_spacer(20.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)
|
|
}
|