aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNathan Reiner <nathan@nathanreiner.xyz>2024-01-20 22:37:48 +0100
committerNathan Reiner <nathan@nathanreiner.xyz>2024-01-20 22:37:48 +0100
commit24ebe795e0ebf9dfc3cf2fafa609d8dc573a80a6 (patch)
tree3e63187af686279e91d8e2686374b9e63d640d44
parentc5ab96ffc8e0ffa97b1bd7e72894ea39433b053f (diff)
make zooming and scrolling with translate and scale of canvas
-rw-r--r--src/main.rs266
-rw-r--r--src/ui/graph.rs237
-rw-r--r--src/ui/mod.rs8
3 files changed, 258 insertions, 253 deletions
diff --git a/src/main.rs b/src/main.rs
index 7521440..d2effe6 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,24 +1,17 @@
pub mod function_cache;
pub mod math;
+pub mod ui;
use function_cache::FunctionCache;
-use iced::event;
use iced::executor;
-use iced::mouse;
-use iced::mouse::Interaction;
-use iced::widget::canvas::{stroke, Cache, Geometry, LineCap, Path, Stroke};
use iced::widget::column;
use iced::widget::text_input;
use iced::widget::{canvas, container};
-use iced::{
- Application, Color, Command, Element, Length, Point, Rectangle, Renderer, Settings, Theme,
- Vector,
-};
-use math::context::Context;
+use iced::{Application, Command, Element, Length, Settings, Theme};
use math::expression_function::ExpressionFunction;
use std::sync::Mutex;
-
-use crate::math::complex::Complex;
+use ui::graph::GraphCanvas;
+use ui::Message;
pub fn main() -> iced::Result {
Graph::run(Settings {
@@ -29,17 +22,8 @@ pub fn main() -> iced::Result {
#[derive(Default)]
struct Graph {
- graph: Cache,
- func: Mutex<FunctionCache>,
- ctx: Context,
input_state: String,
-}
-
-#[derive(Debug, Clone)]
-enum Message {
- InputChanged(String),
- UpdateFunction,
- UpdateScreen,
+ graph_canvas: GraphCanvas,
}
impl Application for Graph {
@@ -49,13 +33,11 @@ impl Application for Graph {
type Flags = ();
fn new(_flags: ()) -> (Self, Command<Message>) {
- let ctx = Context::commonsense();
- let func = ExpressionFunction::from_string("f(x) = x^2".to_string(), ctx.operations());
+ let default_func = "f(x) = x^2".to_string();
(
Graph {
- input_state: "f(x) = x^2".to_string(),
- func: Mutex::new(FunctionCache::new(func)),
- ctx,
+ graph_canvas: GraphCanvas::new(&default_func),
+ input_state: default_func,
..Default::default()
},
Command::none(),
@@ -74,13 +56,13 @@ impl Application for Graph {
Message::UpdateFunction => {
let func = ExpressionFunction::from_string(
self.input_state.clone(),
- self.ctx.operations(),
+ self.graph_canvas.ctx.operations(),
);
- self.func = Mutex::new(FunctionCache::new(func));
- self.graph.clear();
+ self.graph_canvas.func = Mutex::new(FunctionCache::new(func));
+ self.graph_canvas.clear();
}
Message::UpdateScreen => {
- self.graph.clear();
+ self.graph_canvas.clear();
}
}
Command::none()
@@ -92,7 +74,7 @@ impl Application for Graph {
.on_submit(Message::UpdateFunction)
.padding(15)
.size(30);
- let canvas = canvas(self as &Self)
+ let canvas = canvas(&self.graph_canvas)
.width(Length::Fill)
.height(Length::Fill);
@@ -104,225 +86,3 @@ impl Application for Graph {
column![input, container].into()
}
}
-
-#[derive(Copy, Clone)]
-struct MouseState {
- interaction: Interaction,
- center: Point,
- last_position: Option<Point>,
- scale: f32,
-}
-
-impl MouseState {
- fn map_coords(&self, x: f32, y: f32) -> Point {
- Point::new(
- (self.center.x + x) * self.scale,
- (self.center.y - y) * self.scale,
- )
- }
-}
-
-impl Default for MouseState {
- fn default() -> Self {
- Self {
- interaction: Interaction::default(),
- center: Point::default(),
- last_position: None,
- scale: 50.0,
- }
- }
-}
-
-impl canvas::Program<Message, Renderer> for Graph {
- type State = MouseState;
-
- fn draw(
- &self,
- state: &Self::State,
- renderer: &Renderer,
- _theme: &Theme,
- bounds: Rectangle,
- _cursor: mouse::Cursor,
- ) -> Vec<Geometry> {
- let graph = self.graph.draw(renderer, bounds.size(), |frame| {
- let unitline = Stroke {
- width: 2.0,
- style: stroke::Style::Solid(Color::new(0.0, 0.0, 0.0, 1.0)),
- line_cap: LineCap::Round,
- ..Stroke::default()
- };
- 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,
- y: frame.center().y,
- });
-
- frame.with_save(|frame| {
- let start_y = ((state.center.y - frame.height() / 2.0) / state.scale) as i64
- * state.scale as i64;
- let start_x = ((state.center.x - frame.width() / 2.0) / state.scale) as i64
- * state.scale as i64;
- let end_y = ((state.center.y + frame.height() / 2.0) / state.scale) as i64
- * state.scale as i64;
- let end_x = ((state.center.x + frame.width() / 2.0) / state.scale) as i64
- * state.scale as i64;
-
- for y in (start_y..end_y + state.scale as i64).step_by(1) {
- let line = Path::line(
- state.map_coords(start_x as f32, y as f32),
- state.map_coords(end_x as f32, y as f32),
- );
- frame.stroke(&line, unitline_whole.clone());
- }
-
- for x in (start_x..end_x + state.scale as i64).step_by(1) {
- let line = Path::line(
- state.map_coords(x as f32, start_y as f32),
- state.map_coords(x as f32, end_y as f32),
- );
- frame.stroke(&line, unitline_whole.clone());
- }
-
- let start_y_100 = ((state.center.y - frame.height() / 2.0) / state.scale * 10.0)
- as i64
- * 10
- * state.scale as i64;
- let start_x_100 = ((state.center.x - frame.width() / 2.0) / state.scale * 10.0)
- as i64
- * 10
- * state.scale as i64;
-
- for y in (start_y_100..end_y).step_by(10) {
- let line = Path::line(
- state.map_coords(start_x as f32, y as f32),
- state.map_coords(end_x as f32, y as f32),
- );
- frame.stroke(&line, unitline.clone());
- }
-
- for x in (start_x_100..end_x).step_by(10) {
- let line = Path::line(
- state.map_coords(x as f32, start_y as f32),
- state.map_coords(x as f32, end_y as f32),
- );
- frame.stroke(&line, unitline.clone());
- }
-
- let line = Path::line(
- state.map_coords(0.0, start_y as f32),
- state.map_coords(0.0, end_y as f32),
- );
- frame.stroke(&line, zeroline.clone());
-
- let line = Path::line(
- state.map_coords(start_x as f32, 0.0),
- state.map_coords(end_x as f32, 0.0),
- );
- frame.stroke(&line, zeroline.clone());
- let mut y1 = self
- .func
- .lock()
- .unwrap()
- .eval(Complex::new(start_x as f64, 0.0), &self.ctx)
- .real as f32;
- for x in start_x..end_x {
- 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<Message>) {
- 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.scale;
- state.center.y += (position.y - lp.y) / state.scale;
- }
- 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,
- }
- }
-}
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<FunctionCache>,
+ 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<Point>,
+ 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<Message, Renderer> for GraphCanvas {
+ type State = GraphState;
+
+ fn draw(
+ &self,
+ state: &Self::State,
+ renderer: &Renderer,
+ _theme: &Theme,
+ bounds: Rectangle,
+ _cursor: mouse::Cursor,
+ ) -> Vec<Geometry> {
+ 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<Message>) {
+ 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,
+ }
+ }
+}
diff --git a/src/ui/mod.rs b/src/ui/mod.rs
new file mode 100644
index 0000000..1e7fffe
--- /dev/null
+++ b/src/ui/mod.rs
@@ -0,0 +1,8 @@
+pub mod graph;
+
+#[derive(Debug, Clone)]
+pub enum Message {
+ InputChanged(String),
+ UpdateFunction,
+ UpdateScreen,
+}