From 24ebe795e0ebf9dfc3cf2fafa609d8dc573a80a6 Mon Sep 17 00:00:00 2001 From: Nathan Reiner Date: Sat, 20 Jan 2024 22:37:48 +0100 Subject: make zooming and scrolling with translate and scale of canvas --- src/ui/graph.rs | 237 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 src/ui/graph.rs (limited to 'src/ui/graph.rs') diff --git a/src/ui/graph.rs b/src/ui/graph.rs new file mode 100644 index 0000000..2f3855d --- /dev/null +++ b/src/ui/graph.rs @@ -0,0 +1,237 @@ +use std::sync::Mutex; + +use super::Message; +use iced::{ + event, + mouse::{self, Interaction}, + widget::canvas::{self, stroke, Cache, Frame, Geometry, LineCap, Path, Stroke}, + Color, Point, Rectangle, Renderer, Size, Theme, Vector, +}; + +use crate::{ + function_cache::FunctionCache, + math::{complex::Complex, context::Context, expression_function::ExpressionFunction}, +}; + +#[derive(Default)] +pub struct GraphCanvas { + graph: Cache, + pub func: Mutex, + pub ctx: Context, +} + +impl GraphCanvas { + pub fn new(fstr: &String) -> Self { + let ctx = Context::commonsense(); + let func = ExpressionFunction::from_string(fstr.clone(), ctx.operations()); + Self { + graph: Cache::default(), + func: Mutex::new(FunctionCache::new(func)), + ctx, + } + } + + pub fn clear(&self) { + self.graph.clear(); + } +} + +#[derive(Copy, Clone)] +pub struct GraphState { + interaction: Interaction, + center: Point, + last_position: Option, + scale: f32, +} + +impl GraphState { + fn view_rectangle(&self, frame: &Frame) -> Rectangle { + let s = Size::new(frame.width() / self.scale + 2.0, frame.height() / self.scale + 2.0); + let p = Point::new(-self.center.x / self.scale - s.width / 2.0, -self.center.y / self.scale - s.height / 2.0); + Rectangle::new(p, s) + } + + fn map_coords(&self, x: f32, y: f32) -> Point { + Point::new(x, -y) + } +} + +impl Default for GraphState { + fn default() -> Self { + Self { + interaction: Interaction::default(), + center: Point::default(), + last_position: None, + scale: 50.0, + } + } +} + +impl canvas::Program for GraphCanvas { + type State = GraphState; + + fn draw( + &self, + state: &Self::State, + renderer: &Renderer, + _theme: &Theme, + bounds: Rectangle, + _cursor: mouse::Cursor, + ) -> Vec { + let graph = self.graph.draw(renderer, bounds.size(), |frame| { + let unitline_whole = Stroke { + width: 1.0, + style: stroke::Style::Solid(Color::new(0.5, 0.5, 0.5, 1.0)), + line_cap: LineCap::Round, + ..Stroke::default() + }; + let zeroline = Stroke { + width: 3.0, + style: stroke::Style::Solid(Color::new(0.0, 0.0, 0.8, 1.0)), + line_cap: LineCap::Round, + ..Stroke::default() + }; + + let graphline = Stroke { + width: 3.0, + style: stroke::Style::Solid(Color::new(0.8, 0.0, 0.0, 1.0)), + line_cap: LineCap::Round, + ..Stroke::default() + }; + + frame.translate(Vector { + x: frame.center().x + state.center.x, + y: frame.center().y + state.center.y, + }); + frame.scale(state.scale); + + let rect = state.view_rectangle(frame); + let x_end = (rect.x + rect.width) as i64; + let y_end = (rect.y + rect.height) as i64; + + for y in (rect.y as i64..y_end).step_by(1) { + let line = Path::line( + Point::new(rect.x, y as f32), + Point::new(rect.x + rect.width, y as f32), + ); + frame.stroke(&line, unitline_whole.clone()); + } + + for x in (rect.x as i64..x_end).step_by(1) { + let line = Path::line( + Point::new(x as f32, rect.y), + Point::new(x as f32, rect.y + rect.height), + ); + frame.stroke(&line, unitline_whole.clone()); + } + + frame.stroke( + &Path::line( + Point::new(0.0, rect.y), + Point::new(0.0, rect.y + rect.height), + ), + zeroline.clone(), + ); + + frame.stroke( + &Path::line( + Point::new(rect.x, 0.0), + Point::new(rect.x + rect.width, 0.0), + ), + zeroline.clone(), + ); + + frame.with_save(|frame| { + frame.translate(Vector { x: 1.0, y: 0.0 }); + frame.fill_text("1"); + }); + + let mut y1 = self + .func + .lock() + .unwrap() + .eval(Complex::new(rect.x as f64, 0.0), &self.ctx) + .real as f32; + for x in rect.x as i64..(rect.x + rect.width) as i64 { + for d in 0..10 { + let d = d as f32 / 10.0; + let x1 = x as f32 + d; + let x2 = x as f32 + d + 0.1; + let y2 = self + .func + .lock() + .unwrap() + .eval(Complex::new(x2 as f64, 0.0), &self.ctx) + .real as f32; + if y1.is_finite() && !y1.is_nan() && y2.is_finite() && !y2.is_nan() { + let line = Path::line(state.map_coords(x1, y1), state.map_coords(x2, y2)); + frame.stroke(&line, graphline.clone()) + } + y1 = y2; + } + } + }); + + vec![graph] + } + + fn update( + &self, + state: &mut Self::State, + event: canvas::Event, + bounds: Rectangle, + cursor: iced::advanced::mouse::Cursor, + ) -> (canvas::event::Status, Option) { + if cursor.position_in(bounds).is_none() { + return (event::Status::Ignored, None); + } + if let canvas::Event::Mouse(mouse::Event::ButtonPressed(_)) = event { + state.interaction = Interaction::Grabbing; + state.last_position = cursor.position(); + return (event::Status::Captured, Some(Message::UpdateScreen)); + } + + if let canvas::Event::Mouse(mouse::Event::CursorMoved { position }) = event { + if state.interaction == Interaction::Grabbing { + if let Some(lp) = state.last_position { + state.center.x += position.x - lp.x; + state.center.y += position.y - lp.y; + } + state.last_position = Some(position); + return (event::Status::Captured, Some(Message::UpdateScreen)); + } + } + + if let canvas::Event::Mouse(mouse::Event::ButtonReleased(_)) = event { + state.interaction = Interaction::Pointer; + } + + if let canvas::Event::Mouse(mouse::Event::WheelScrolled { delta }) = event { + match delta { + mouse::ScrollDelta::Lines { y, .. } => { + if y > 0.0 { + state.scale /= 1.1; + } else { + state.scale *= 1.1; + } + state.scale = state.scale.max(0.000001); + return (event::Status::Captured, Some(Message::UpdateScreen)); + } + _ => {} + } + } + (event::Status::Ignored, None) + } + + fn mouse_interaction( + &self, + state: &Self::State, + _bounds: Rectangle, + _cursor: mouse::Cursor, + ) -> mouse::Interaction { + match state.interaction { + Interaction::Grabbing => mouse::Interaction::Grabbing, + _ => mouse::Interaction::Grab, + } + } +} -- cgit v1.2.3-70-g09d2