diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/main.rs | 266 | ||||
| -rw-r--r-- | src/ui/graph.rs | 237 | ||||
| -rw-r--r-- | src/ui/mod.rs | 8 |
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, +} |