From 1acc998d7a7305e7782cff8fb8c5cd3704b7f985 Mon Sep 17 00:00:00 2001 From: Joe Bellus Date: Sat, 15 Oct 2022 17:20:59 -0400 Subject: [PATCH] Sync commit --- abacus-core/src/dataframe.rs | 78 ++++++- abacus-core/src/engine.rs | 59 ++---- abacus-core/src/lib.rs | 1 - abacus-ui/src/app_delegate.rs | 9 +- abacus-ui/src/app_header.rs | 25 ++- abacus-ui/src/data/app_data.rs | 14 ++ abacus-ui/src/data/editor_data.rs | 120 ++++++++++- abacus-ui/src/editor.rs | 16 +- abacus-ui/src/main.rs | 3 +- abacus-ui/src/modal_container.rs | 5 +- abacus-ui/src/output.rs | 1 + abacus-ui/src/output_block.rs | 31 +-- abacus-ui/src/piet_plotters.rs | 336 ++++++++++++++++++++++++++++++ abacus-ui/src/plot_view.rs | 60 ++++++ frame.abacus | 8 - test.abacus | 8 - 16 files changed, 650 insertions(+), 124 deletions(-) create mode 100644 abacus-ui/src/output.rs create mode 100644 abacus-ui/src/piet_plotters.rs create mode 100644 abacus-ui/src/plot_view.rs delete mode 100644 frame.abacus delete mode 100644 test.abacus diff --git a/abacus-core/src/dataframe.rs b/abacus-core/src/dataframe.rs index 889b0a2..c4f6c8e 100644 --- a/abacus-core/src/dataframe.rs +++ b/abacus-core/src/dataframe.rs @@ -14,6 +14,9 @@ pub fn setup_engine(engine: &mut rhai::Engine) { engine.register_fn("dataframe", script_functions::dataframe); engine.register_fn("select", DataFrame::s_select); engine.register_fn("load_csv", DataFrame::load_csv); + engine.register_fn("column", DataFrame::s_column); + engine.register_indexer_get(DataFrame::s_column); + engine.register_indexer_set(DataFrame::append_column); // polar series engine.register_type::(); engine.register_indexer_get(Series::s_get); @@ -22,15 +25,25 @@ pub fn setup_engine(engine: &mut rhai::Engine) { engine.register_fn("to_series", script_functions::to_series_unnamed); engine.register_fn("series", script_functions::series_unnamed); engine.register_fn("head", Series::s_head); - engine.register_fn("+", Series::add); engine.register_fn("sort", Series::s_sort); engine.register_fn("sum", Series::s_sum); engine.register_fn("add", Series::s_op_add); - engine.register_fn("column", DataFrame::s_column); + + engine.register_fn("+", Series::add); engine.register_fn("+", script_functions::add_series_i64); engine.register_fn("+", script_functions::add_series_f64); + + engine.register_fn("-", script_functions::subtract_series_series); engine.register_fn("-", script_functions::subtract_series_i64); engine.register_fn("-", script_functions::subtract_series_f64); + + engine.register_fn("*", script_functions::multiply_series_series); + engine.register_fn("*", script_functions::multiply_series_i64); + engine.register_fn("*", script_functions::multiply_series_f64); + + engine.register_fn("/", script_functions::div_series_series); + engine.register_fn("/", script_functions::div_series_i64); + engine.register_fn("/", script_functions::div_series_f64); // polar expressions engine.register_type::(); engine.register_fn("sum", DataFrameExpression::sum); @@ -54,6 +67,7 @@ pub fn setup_engine(engine: &mut rhai::Engine) { engine.register_fn("first", script_functions::first); let _ = engine.register_custom_operator("gt", 200); let _ = engine.register_custom_operator("gte", 200); + let _ = engine.register_custom_operator("<<", 200); engine.register_fn("gt", script_functions::gt_op); engine.register_fn("gte", script_functions::gte_op); @@ -110,6 +124,12 @@ impl DataFrame { .map_err(|e| e.to_string())?, )) } + + pub fn append_column(&mut self, idx: &str, mut b: Series) -> ScriptResult<()> { + b.0.rename(idx); + self.0.with_column(b.0).map_err(|e| e.to_string())?; + Ok(()) + } } #[derive(Clone, Debug)] @@ -259,6 +279,34 @@ mod script_functions { Series(a.0 - b) } + pub fn subtract_series_series(a: Series, b: Series) -> Series { + Series(a.0 - b.0) + } + + pub fn multiply_series_i64(a: Series, b: i64) -> Series { + Series(a.0 * b) + } + + pub fn multiply_series_f64(a: Series, b: f64) -> Series { + Series(a.0 * b) + } + + pub fn multiply_series_series(a: Series, b: Series) -> Series { + Series(a.0 * b.0) + } + + pub fn div_series_i64(a: Series, b: i64) -> Series { + Series(a.0 / b) + } + + pub fn div_series_f64(a: Series, b: f64) -> Series { + Series(a.0 / b) + } + + pub fn div_series_series(a: Series, b: Series) -> Series { + Series(a.0 / b.0) + } + pub fn column(name: &str) -> DataFrameExpression { DataFrameExpression(polars::prelude::col(name)) } @@ -326,21 +374,21 @@ mod script_functions { polars::series::Series::new( name, arr.into_iter() - .map(|i| i.cast::()) + .filter_map(|i| i.try_cast::()) .collect::>(), ) } else if i.type_id() == TypeId::of::() { polars::series::Series::new( name, arr.into_iter() - .map(|i| i.cast::()) + .filter_map(|i| i.try_cast::()) .collect::>(), ) } else { polars::series::Series::new( name, arr.into_iter() - .map(|i| i.cast::()) + .filter_map(|i| i.try_cast::()) .collect::>(), ) }; @@ -412,12 +460,20 @@ fn implementation_df_select( let select_expressions = select_array .iter() - .map(|s| { - filter_array - .iter() - .fold(polars::prelude::col(&s.to_string()), |acc, i| { - acc.filter(i.0.clone()) - }) + .filter_map(|s| { + let select_expr = if s.is::() { + polars::prelude::col(&s.to_string()) + } else if s.is::() { + s.clone().cast::().0 + } else { + return None; + }; + + Some( + filter_array + .iter() + .fold(select_expr, |acc, i| acc.filter(i.0.clone())), + ) }) .collect::>(); diff --git a/abacus-core/src/engine.rs b/abacus-core/src/engine.rs index 62f2eba..adb6461 100644 --- a/abacus-core/src/engine.rs +++ b/abacus-core/src/engine.rs @@ -1,12 +1,12 @@ use crate::dataframe; #[derive(Debug)] -pub struct Engine { - pub blocks: Vec, +pub struct Engine<'a> { engine: rhai::Engine, + scope: rhai::Scope<'a>, } -impl Default for Engine { +impl<'a> Default for Engine<'a> { fn default() -> Self { let mut engine = rhai::Engine::new(); engine.set_fast_operators(false); @@ -14,42 +14,24 @@ impl Default for Engine { Self { engine, - blocks: vec![Block::default()], + scope: rhai::Scope::new(), } } } -impl Engine { - pub fn process(&mut self) { - for block in self.blocks.iter_mut() { - match self.engine.eval::(&block.script) { - Ok(res) if res.is::() => { - let frame = rhai::Dynamic::cast::(res); - block.output = Output::DataFrame(frame); - } - Ok(res) if res.is::() => { - let frame = rhai::Dynamic::cast::(res); - block.output = Output::Series(frame); - } - Ok(res) => { - block.output = Output::Scalar(res); - } - Err(e) => { - block.output = Output::Error(e.to_string()); - } - } - } - } - +impl<'a> Engine<'a> { pub fn process_script(&mut self, script: &str) -> Output { - match self.engine.eval::(script) { + match self + .engine + .eval_with_scope::(&mut self.scope, script) + { Ok(res) if res.is::() => { let frame = rhai::Dynamic::cast::(res); Output::DataFrame(frame) } Ok(res) if res.is::() => { - let frame = rhai::Dynamic::cast::(res); - Output::Series(frame) + let series = rhai::Dynamic::cast::(res); + Output::Series(series) } Ok(res) => Output::Scalar(res), Err(e) => Output::Error(e.to_string()), @@ -105,29 +87,12 @@ impl Output { } } -#[derive(Debug)] -pub struct Block { - pub script: String, - pub output: Output, -} - -impl Default for Block { - fn default() -> Self { - Self { - script: Default::default(), - output: Output::None, - } - } -} - #[cfg(test)] pub mod tests { use super::*; pub fn process(script: &str) -> Output { let mut engine = Engine::default(); - engine.blocks[0].script = script.into(); - engine.process(); - engine.blocks[0].output.clone() + engine.process_script(script) } } diff --git a/abacus-core/src/lib.rs b/abacus-core/src/lib.rs index b9119eb..6c2f0af 100644 --- a/abacus-core/src/lib.rs +++ b/abacus-core/src/lib.rs @@ -2,7 +2,6 @@ mod dataframe; mod engine; mod save_file; -pub use engine::Block; pub use engine::Engine; pub use engine::Output; pub use save_file::{SaveBlock, SaveFile}; diff --git a/abacus-ui/src/app_delegate.rs b/abacus-ui/src/app_delegate.rs index 8b528a1..3739476 100644 --- a/abacus-ui/src/app_delegate.rs +++ b/abacus-ui/src/app_delegate.rs @@ -18,15 +18,8 @@ impl AppDelegate for Delegate { ) -> druid::Handled { if let Some(file_info) = cmd.get(druid::commands::SAVE_FILE_AS) { let filepath = file_info.path().as_os_str().to_str().unwrap().to_string(); - let _ = abacus_core::SaveFile::new( - filepath.clone(), - data.blocks - .iter() - .map(|d| abacus_core::SaveBlock::new(&d.name, &d.editor_data.content)) - .collect(), - ) - .save(); data.filename = Some(filepath); + data.save(); return druid::Handled::Yes; } diff --git a/abacus-ui/src/app_header.rs b/abacus-ui/src/app_header.rs index f658378..8f2533c 100644 --- a/abacus-ui/src/app_header.rs +++ b/abacus-ui/src/app_header.rs @@ -49,20 +49,23 @@ pub fn app_header_ui() -> impl Widget { .with_child( Container::new(Padding::new(10.0, Svg::new(disk_svg).fix_width(10.0))) .controller(ToolbarButtonController::new(Color::rgb8(50, 50, 50))) - .on_click(|ctx, _, _| { + .on_click(|ctx, data: &mut AppData, _| { let abacus = druid::FileSpec::new("Abacus File", &["abacus"]); let json = druid::FileSpec::new("JSON File", &["json"]); + if data.filename.is_some() { + data.save(); + } else { + let save_dialog_options = druid::FileDialogOptions::new() + .allowed_types(vec![abacus, json]) + .default_type(abacus) + .name_label("Target") + .title("Save workbook") + .button_text("Save"); - let save_dialog_options = druid::FileDialogOptions::new() - .allowed_types(vec![abacus, json]) - .default_type(abacus) - .name_label("Target") - .title("Save workbook") - .button_text("Save"); - - ctx.submit_command( - druid::commands::SHOW_SAVE_PANEL.with(save_dialog_options), - ); + ctx.submit_command( + druid::commands::SHOW_SAVE_PANEL.with(save_dialog_options), + ); + } }), ) .with_spacer(20.0) diff --git a/abacus-ui/src/data/app_data.rs b/abacus-ui/src/data/app_data.rs index e5ec0de..d2d0b5a 100644 --- a/abacus-ui/src/data/app_data.rs +++ b/abacus-ui/src/data/app_data.rs @@ -15,6 +15,19 @@ pub struct AppData { } impl AppData { + pub fn save(&self) { + if let Some(ref filepath) = self.filename { + let _ = abacus_core::SaveFile::new( + filepath.clone(), + self.blocks + .iter() + .map(|d| abacus_core::SaveBlock::new(&d.name, &d.editor_data.content)) + .collect(), + ) + .save(); + } + } + pub fn remove_block(&mut self, idx: usize) { self.blocks.remove(idx); for (idx, block) in self.blocks.iter_mut().enumerate() { @@ -36,6 +49,7 @@ impl Default for AppData { impl AppData { pub fn process(&mut self) { let mut engine = abacus_core::Engine::default(); + for mut block in self.blocks.iter_mut() { let start = std::time::Instant::now(); block.output = engine.process_script(&block.editor_data.content.to_string()); diff --git a/abacus-ui/src/data/editor_data.rs b/abacus-ui/src/data/editor_data.rs index c8a0d6c..6e2ec69 100644 --- a/abacus-ui/src/data/editor_data.rs +++ b/abacus-ui/src/data/editor_data.rs @@ -23,9 +23,9 @@ pub struct EditorData { impl Default for EditorData { fn default() -> Self { Self { - content: Rope::from_str("let x = 1;\nx+1"), - cursor_pos: 5, - selection_pos: 5, + content: Rope::from_str("1234567890\n\n\n123\n\n1234\n1234"), + cursor_pos: 0, + selection_pos: 0, mode: EditMode::Normal, cursor_opactiy: 255.0, cursor_fade: 1.0, @@ -35,7 +35,6 @@ impl Default for EditorData { impl EditorData { pub fn new(content: &str) -> Self { - println!("new block: {}", content); Self { content: Rope::from_str(content), ..Default::default() @@ -43,10 +42,16 @@ impl EditorData { } pub fn move_cursor(&mut self, idx: usize) { + let has_selection = self.cursor_pos != self.selection_pos; if idx <= self.content.len_chars() { self.cursor_pos = idx; + } + if self.mode == EditMode::Normal && self.cursor_pos == 0 { + self.cursor_pos = 1; + } + + if !has_selection { self.deselect(); - // dbg!(self.content.char(self.cursor_pos)); } } @@ -128,6 +133,18 @@ impl EditorData { } } + pub fn select_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.cursor_pos = 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 { @@ -136,14 +153,42 @@ impl EditorData { 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)); + let new_pos = start_of_next_line + line_pos.min(next_line_len); + + if self.cursor_pos == new_pos { + self.move_cursor(new_pos + 1); + } else { + self.move_cursor(new_pos); + } + println!(" "); + dbg!(line_idx, new_pos, start_of_next_line); + } + } + + pub fn select_down(&mut self) { + //12 -> 11 + 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) + 1; + let next_line_len = self.content.line(line_idx + 1).len_chars(); + + self.cursor_pos = + (start_of_next_line + line_pos.min(next_line_len)).min(start_of_next_line); } } pub fn cursor_right(&mut self) { + let has_selection = self.cursor_pos != self.selection_pos; + if self.cursor_pos < self.content.len_chars() { self.move_cursor(self.cursor_pos + 1); } + + if !has_selection { + self.deselect(); + } } pub fn select_to_end_of_line(&mut self) { @@ -191,13 +236,72 @@ impl EditorData { pub fn select_left(&mut self) { if self.cursor_pos > 0 { - self.selection_pos -= 1; + self.cursor_pos -= 1 } } pub fn select_right(&mut self) { if self.cursor_pos < self.content.len_chars() { - self.selection_pos += 1; + self.cursor_pos += 1; } } + + fn current_line(&self) -> usize { + self.content.char_to_line(self.cursor_pos) + } + + fn current_char(&self) -> Option { + self.content.get_char(self.cursor_pos) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn cursor_left_normal_empty_line() { + let mut data = EditorData { + mode: EditMode::Normal, + ..Default::default() + }; + data.push_str("0123456789\n\n1234"); + data.cursor_pos = 12; + data.selection_pos = 12; + data.cursor_left(); + assert_eq!(data.cursor_pos, 11); + data.cursor_left(); + assert_eq!(data.cursor_pos, 10); + } + + #[test] + fn cursor_left_normal_double_empty_line() { + let mut data = EditorData { + mode: EditMode::Normal, + ..Default::default() + }; + data.push_str("0123456789\n\n\n1234"); + data.cursor_pos = 14; + data.selection_pos = 14; + data.cursor_left(); + assert_eq!(data.cursor_pos, 12); + data.cursor_left(); + assert_eq!(data.cursor_pos, 11); + data.cursor_left(); + assert_eq!(data.cursor_pos, 10); + } + + #[test] + fn cursor_left_normal_end_of_line() { + let mut data = EditorData { + mode: EditMode::Normal, + ..Default::default() + }; + data.push_str("0123456789\n1234"); + data.cursor_pos = 12; + data.selection_pos = 12; + assert_eq!(data.current_char().unwrap(), '1'); + data.cursor_left(); + assert_eq!(data.cursor_pos, 10); + } } diff --git a/abacus-ui/src/editor.rs b/abacus-ui/src/editor.rs index ae4e8cb..ee3b7ff 100644 --- a/abacus-ui/src/editor.rs +++ b/abacus-ui/src/editor.rs @@ -35,7 +35,6 @@ impl Default for AbacusEditor { 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); @@ -69,8 +68,10 @@ impl AbacusEditor { } 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 let Some(char_rect) = layout + .rects_for_range(data.cursor_pos..(data.cursor_pos + 1)) + .last() + { if char_rect.width() == 0. { let rect = Rect::new( char_rect.min_x(), @@ -269,6 +270,13 @@ impl Widget for AbacusEditor { druid::keyboard_types::Key::ArrowDown if !e.mods.shift() => { data.cursor_down(); } + + 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(" "); } @@ -322,7 +330,7 @@ impl Widget for AbacusEditor { 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()); + data.selection_pos = pos.idx.min(data.content.len_chars()); } else { data.selection_pos = pos.idx.max(1) - 1; } diff --git a/abacus-ui/src/main.rs b/abacus-ui/src/main.rs index 0733cc4..0c12354 100644 --- a/abacus-ui/src/main.rs +++ b/abacus-ui/src/main.rs @@ -2,9 +2,8 @@ mod app_delegate; mod app_header; mod commands; mod data; -mod modal_container; - mod editor; +mod modal_container; mod output_block; use data::{AppData, Block}; diff --git a/abacus-ui/src/modal_container.rs b/abacus-ui/src/modal_container.rs index 0ebd9fe..5a3132f 100644 --- a/abacus-ui/src/modal_container.rs +++ b/abacus-ui/src/modal_container.rs @@ -39,9 +39,7 @@ impl Widget for ModalContainer { if let druid::Event::Command(c) = event { if let Some(idx) = c.get(crate::commands::RENAME_BLOCK) { - println!("rename block"); data.modals.rename_block = data::RenameBlock { - block_index: *idx, name: data .blocks .get(*idx) @@ -52,6 +50,7 @@ impl Widget for ModalContainer { .get(*idx) .map(|i| i.name.clone()) .unwrap_or_default(), + block_index: *idx, }; self.modal = Some(WidgetPod::new(rename_block()).boxed()); ctx.children_changed(); @@ -106,11 +105,9 @@ impl Widget for ModalContainer { } fn paint(&mut self, ctx: &mut druid::PaintCtx, data: &data::AppData, env: &druid::Env) { - println!("paint modal container"); self.child.paint(ctx, data, env); let full_rect = ctx.size().to_rect(); if let Some(m) = self.modal.as_mut() { - println!("painting modal"); ctx.fill(full_rect, &druid::Color::rgba8(0, 0, 0, 100)); m.paint(ctx, data, env) } diff --git a/abacus-ui/src/output.rs b/abacus-ui/src/output.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/abacus-ui/src/output.rs @@ -0,0 +1 @@ + diff --git a/abacus-ui/src/output_block.rs b/abacus-ui/src/output_block.rs index 09c6eed..56d75f6 100644 --- a/abacus-ui/src/output_block.rs +++ b/abacus-ui/src/output_block.rs @@ -1,6 +1,6 @@ use abacus_core::Output; use druid::{ - widget::{Flex, Label, Padding, ViewSwitcher}, + widget::{Container, Flex, Label, Padding, Scroll, ViewSwitcher}, Color, FontDescriptor, FontFamily, FontWeight, Widget, WidgetExt, }; @@ -64,7 +64,8 @@ pub fn output_block() -> impl Widget { Output::DataFrame(frame) => { let mut flex = Flex::row(); for series in frame.iter() { - let mut col = Flex::column(); + let mut col = Flex::column() + .cross_axis_alignment(druid::widget::CrossAxisAlignment::Fill); col.add_child( Label::new(series.name()) .with_font( @@ -72,9 +73,10 @@ pub fn output_block() -> impl Widget { .with_weight(FontWeight::BLACK), ) .with_text_size(OUTPUT_FONT_SIZE) - .expand_width() + .with_text_color(Color::rgb8(150, 150, 150)) .padding(3.0) - .border(Color::rgb8(60, 60, 60), 1.0), + .border(Color::rgb8(50, 50, 50), 1.0) + .background(Color::rgb8(40, 40, 40)), ); for v in series.iter() { @@ -85,21 +87,25 @@ pub fn output_block() -> impl Widget { .with_weight(FontWeight::MEDIUM), ) .with_text_size(OUTPUT_FONT_SIZE) - .expand_width() .padding(3.0) - .border(Color::rgb8(60, 60, 60), 1.0), + .border(Color::rgb8(50, 50, 50), 1.0), ); } - flex.add_flex_child(col, 1.0); + flex.add_child(col); } Box::new(Padding::new( 25.0, - flex.padding(10.0) - .background(Color::rgb8(30, 30, 30)) - .rounded(4.0) - .expand_width(), + Container::new( + Scroll::new(flex.background(Color::rgb8(30, 30, 30)).expand_width()) + .horizontal() + .expand_width(), + ) + .rounded(4.0) + .border(Color::rgb8(30, 30, 30), 5.0) + .background(Color::rgb8(30, 30, 30)) + .expand_width(), )) } _ => Box::new(Padding::new( @@ -110,7 +116,8 @@ pub fn output_block() -> impl Widget { ) .with_text_size(0.0) .padding(0.0) - .background(Color::TRANSPARENT), + .background(Color::TRANSPARENT) + .expand_width(), )), }, ) diff --git a/abacus-ui/src/piet_plotters.rs b/abacus-ui/src/piet_plotters.rs new file mode 100644 index 0000000..4061904 --- /dev/null +++ b/abacus-ui/src/piet_plotters.rs @@ -0,0 +1,336 @@ +use piet_common::{kurbo, Color, LineCap, Piet, RenderContext, StrokeDash, StrokeStyle}; +use plotters_backend::{BackendColor, BackendCoord, DrawingBackend, DrawingErrorKind}; + +#[derive(Debug, PartialEq, Eq)] +pub struct Error {} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "plotters-piet error") + } +} + +impl std::error::Error for Error {} + +/// The piet backend. +/// +/// Note that the size of the piet context has to be specified here. +pub struct PietBackend<'a, 'b> { + pub size: (u32, u32), + pub render_ctx: &'a mut Piet<'b>, +} + +impl<'a, 'b> std::fmt::Debug for PietBackend<'a, 'b> { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + fmt.debug_struct("PietBackend") + .field("size", &self.size) + .field("render_ctx", &"(not printable)") + .finish() + } +} + +impl<'a, 'b> DrawingBackend for PietBackend<'a, 'b> { + type ErrorType = Error; + + fn get_size(&self) -> (u32, u32) { + self.size + } + + fn ensure_prepared(&mut self) -> Result<(), DrawingErrorKind> { + Ok(()) + } + + fn present(&mut self) -> Result<(), DrawingErrorKind> { + self.render_ctx + .finish() + .map_err(|_| DrawingErrorKind::DrawingError(Error {})) + } + + fn draw_pixel( + &mut self, + point: BackendCoord, + color: BackendColor, + ) -> Result<(), DrawingErrorKind> { + let x = point.0 as f64; + let y = point.1 as f64; + self.render_ctx.fill( + kurbo::Rect::new(x, y, x + 1., y + 1.), + &plotters_color_to_piet(&color), + ); + Ok(()) + } + + fn draw_line( + &mut self, + from: BackendCoord, + to: BackendCoord, + style: &S, + ) -> Result<(), DrawingErrorKind> { + let from = plotters_point_to_kurbo_mid(from); + let to = plotters_point_to_kurbo_mid(to); + + self.render_ctx.stroke_styled( + kurbo::Line::new(from, to), + &plotters_color_to_piet(&style.color()), + style.stroke_width() as f64, + &STROKE_STYLE_SQUARE_CAP, + ); + Ok(()) + } + + fn draw_rect( + &mut self, + upper_left: BackendCoord, + bottom_right: BackendCoord, + style: &S, + fill: bool, + ) -> Result<(), DrawingErrorKind> { + let color = plotters_color_to_piet(&style.color()); + + if fill { + let upper_left = plotters_point_to_kurbo_corner(upper_left); + let mut bottom_right = plotters_point_to_kurbo_corner(bottom_right); + bottom_right.x += 1.; + bottom_right.y += 1.; + let rect = kurbo::Rect::new(upper_left.x, upper_left.y, bottom_right.x, bottom_right.y); + + self.render_ctx.fill(rect, &color); + } else { + let upper_left = plotters_point_to_kurbo_mid(upper_left); + let bottom_right = plotters_point_to_kurbo_mid(bottom_right); + let rect = kurbo::Rect::new(upper_left.x, upper_left.y, bottom_right.x, bottom_right.y); + + self.render_ctx + .stroke(rect, &color, style.stroke_width() as f64); + } + + Ok(()) + } + + fn draw_path>( + &mut self, + path: I, + style: &S, + ) -> Result<(), DrawingErrorKind> { + if style.color().alpha == 0.0 { + return Ok(()); + } + + let path: Vec = plotters_path_to_kurbo(path).collect(); + + self.render_ctx.stroke_styled( + &*path, + &plotters_color_to_piet(&style.color()), + style.stroke_width() as f64, + &STROKE_STYLE_SQUARE_CAP, + ); + Ok(()) + } + + fn draw_circle( + &mut self, + center: BackendCoord, + radius: u32, + style: &S, + fill: bool, + ) -> Result<(), DrawingErrorKind> { + let center = plotters_point_to_kurbo_mid(center); + let color = plotters_color_to_piet(&style.color()); + let circle = kurbo::Circle::new(center, radius as f64); + + if fill { + self.render_ctx.fill(circle, &color); + } else { + self.render_ctx + .stroke(circle, &color, style.stroke_width() as f64); + } + Ok(()) + } + + fn fill_polygon>( + &mut self, + vert: I, + style: &S, + ) -> Result<(), DrawingErrorKind> { + if style.color().alpha == 0.0 { + return Ok(()); + } + + let path: Vec = plotters_path_to_kurbo(vert) + .chain(std::iter::once(kurbo::PathEl::ClosePath)) + .collect(); + self.render_ctx + .fill(&*path, &plotters_color_to_piet(&style.color())); + Ok(()) + } + + // For now we use the default text drawing provided by plotters. This is definitely slower, + // but at least we don't have to worry about matching the font size and offset which turns + // out to be trickier than expected. + // fn draw_text( + // &mut self, + // text: &str, + // style: &TStyle, + // pos: BackendCoord, + // ) -> Result<(), DrawingErrorKind> { + // let pos = plotters_point_to_kurbo(pos); + // let color = plotters_color_to_piet(&style.color()); + + // let text_api = self.render_ctx.text(); + // let font_family = match style.family() { + // plotters_backend::FontFamily::Serif => Ok(FontFamily::SERIF), + // plotters_backend::FontFamily::SansSerif => Ok(FontFamily::SANS_SERIF), + // plotters_backend::FontFamily::Monospace => Ok(FontFamily::MONOSPACE), + // plotters_backend::FontFamily::Name(name) => text_api + // .font_family(name) + // .ok_or(piet_common::Error::MissingFont), + // }; + + // let (font_style, weight) = match style.style() { + // plotters_backend::FontStyle::Normal => (FontStyle::Regular, FontWeight::REGULAR), + // plotters_backend::FontStyle::Oblique => (FontStyle::Italic, FontWeight::REGULAR), + // plotters_backend::FontStyle::Italic => (FontStyle::Italic, FontWeight::REGULAR), + // plotters_backend::FontStyle::Bold => (FontStyle::Regular, FontWeight::BOLD), + // }; + + // let alignment = match style.anchor().h_pos { + // plotters_backend::text_anchor::HPos::Left => TextAlignment::Start, + // plotters_backend::text_anchor::HPos::Right => TextAlignment::End, + // plotters_backend::text_anchor::HPos::Center => TextAlignment::Center, + // }; + + // let layout = text_api + // .new_text_layout(String::from(text)) + // .font(font_family.unwrap(), style.size()) + // .text_color(color) + // .alignment(alignment) + // .default_attribute(TextAttribute::Style(font_style)) + // .default_attribute(TextAttribute::Weight(weight)) + // .build() + // .unwrap(); + + // // todo: style.anchor().v_pos + // // todo: style.transform() + + // self.render_ctx.draw_text(&layout, pos); + // Ok(()) + // } +} + +fn plotters_color_to_piet(col: &BackendColor) -> piet_common::Color { + Color::rgba8(col.rgb.0, col.rgb.1, col.rgb.2, (col.alpha * 256.) as u8) +} + +fn plotters_point_to_kurbo_mid((x, y): BackendCoord) -> kurbo::Point { + kurbo::Point { + x: x as f64 + 0.5, + y: y as f64 + 0.5, + } +} + +fn plotters_point_to_kurbo_corner((x, y): BackendCoord) -> kurbo::Point { + kurbo::Point { + x: x as f64, + y: y as f64, + } +} + +/// This is basically just an iterator map that applies a different function on +/// the first item as on the later items. +/// We need this because the piet direct2d backend doesn't like it if a path +/// consists entirely of `LineTo` entries, it requires the first entry to be +/// a `MoveTo` entry. +struct PlottersPathToKurbo { + iter: I, + first: bool, +} + +impl PlottersPathToKurbo { + fn new(path: I) -> PlottersPathToKurbo { + PlottersPathToKurbo { + iter: path, + first: true, + } + } +} + +impl Iterator for PlottersPathToKurbo +where + I: Iterator, +{ + type Item = kurbo::PathEl; + + fn next(&mut self) -> Option { + self.iter.next().map(|point| { + let point = plotters_point_to_kurbo_mid(point); + + if self.first { + self.first = false; + kurbo::PathEl::MoveTo(point) + } else { + kurbo::PathEl::LineTo(point) + } + }) + } +} + +fn plotters_path_to_kurbo( + path: impl IntoIterator, +) -> impl Iterator { + PlottersPathToKurbo::new(path.into_iter()) +} + +const STROKE_STYLE_SQUARE_CAP: StrokeStyle = StrokeStyle::new().line_cap(LineCap::Square); + +#[cfg(test)] +mod tests { + use super::*; + use piet_common::RenderContext; + use plotters::prelude::*; + + #[test] + fn fill_root_white() { + let width = 3; + let height = 2; + + let mut device = piet_common::Device::new().unwrap(); + let mut bitmap = device.bitmap_target(width, height, 1.0).unwrap(); + + { + let mut render_ctx = bitmap.render_context(); + + let piet_backend = PietBackend { + size: (width as u32, height as u32), + render_ctx: &mut render_ctx, + }; + + let root = piet_backend.into_drawing_area(); + root.fill(&WHITE).unwrap(); + + render_ctx.finish().unwrap(); + } + + let mut buf = [0; 6 * 4]; + bitmap + .copy_raw_pixels(piet_common::ImageFormat::RgbaPremul, &mut buf) + .unwrap(); + + assert_eq!(buf, [255; 6 * 4]); + } + + #[test] + fn test_plotters_path_to_kurbo() { + let path = vec![(1, 2), (3, 4), (5, 6)]; + + let kurbo_path: Vec = plotters_path_to_kurbo(path).collect(); + + assert_eq!( + kurbo_path, + vec![ + kurbo::PathEl::MoveTo(kurbo::Point { x: 1.5, y: 2.5 }), + kurbo::PathEl::LineTo(kurbo::Point { x: 3.5, y: 4.5 }), + kurbo::PathEl::LineTo(kurbo::Point { x: 5.5, y: 6.5 }), + ] + ); + } +} diff --git a/abacus-ui/src/plot_view.rs b/abacus-ui/src/plot_view.rs new file mode 100644 index 0000000..62253a2 --- /dev/null +++ b/abacus-ui/src/plot_view.rs @@ -0,0 +1,60 @@ +use crate::piet_plotters::PietBackend; +use druid::{Data, Widget}; +use plotters::{ + coord::Shift, + prelude::{DrawingArea, IntoDrawingArea}, +}; + +pub struct Plot { + #[allow(clippy::type_complexity)] + plot: Box)>, +} + +impl Plot { + pub fn new(f: impl Fn((u32, u32), &T, &DrawingArea) + 'static) -> Plot { + Plot { plot: Box::new(f) } + } +} + +impl Widget for Plot +where + T: Data, +{ + fn event(&mut self, _: &mut druid::EventCtx, _: &druid::Event, _: &mut T, _: &druid::Env) {} + + fn lifecycle( + &mut self, + _: &mut druid::LifeCycleCtx, + _: &druid::LifeCycle, + _: &T, + _: &druid::Env, + ) { + } + + fn update(&mut self, ctx: &mut druid::UpdateCtx, old_data: &T, data: &T, _env: &druid::Env) { + if !old_data.same(data) { + ctx.request_paint(); + } + } + + fn layout( + &mut self, + _: &mut druid::LayoutCtx, + bc: &druid::BoxConstraints, + _: &T, + _: &druid::Env, + ) -> druid::Size { + bc.max() + } + + fn paint(&mut self, ctx: &mut druid::PaintCtx, data: &T, _: &druid::Env) { + let druid::Size { width, height } = ctx.size(); + let size = (width as u32, height as u32); + let backend = PietBackend { + size, + render_ctx: ctx.render_ctx, + }; + + (self.plot)(size, data, &backend.into_drawing_area()); + } +} diff --git a/frame.abacus b/frame.abacus deleted file mode 100644 index 7caed60..0000000 --- a/frame.abacus +++ /dev/null @@ -1,8 +0,0 @@ -{ - "blocks": [ - { - "name": "Things", - "content": "let df = dataframe(#{\n names: [\"Alice\", \"Charles\", \"Bob\"], \n ages: [18,25,31],\n id: [1,2,3],\n amount: [10,0,12]\n});\n\n//df.select([column(\"ages\").filter(column(\"ages\") gt 20)]);\n\nlet df2 = from df [\"names\", \"ages\"] : [\"ages\" gt 20];\ndf2" - } - ] -} \ No newline at end of file diff --git a/test.abacus b/test.abacus deleted file mode 100644 index 9f42184..0000000 --- a/test.abacus +++ /dev/null @@ -1,8 +0,0 @@ -{ - "blocks": [ - { - "name": "Things", - "content": "let x = 1;\nx+1" - } - ] -} \ No newline at end of file