├── .github └── workflows │ └── test.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── animation.rs ├── stock.rs └── window.rs ├── resources └── example.png ├── rust-toolchain.toml └── src ├── figure ├── axes │ ├── mod.rs │ ├── model.rs │ ├── plotters.rs │ └── view.rs ├── figure.rs ├── grid.rs ├── mod.rs ├── plot.rs ├── text.rs └── ticks.rs ├── fps.rs ├── geometry ├── axis.rs ├── line.rs ├── marker.rs ├── mod.rs ├── point.rs ├── size.rs └── text.rs ├── lib.rs └── utils ├── math.rs └── mod.rs /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - "*" 7 | tags: 8 | - "*" 9 | 10 | jobs: 11 | test_macos: 12 | name: Test (MacOS) 13 | if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name 14 | runs-on: macos-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Setup | Cache Cargo 18 | uses: actions/cache@v4 19 | with: 20 | path: | 21 | ~/.cargo/bin/ 22 | ~/.cargo/registry/index/ 23 | ~/.cargo/registry/cache/ 24 | ~/.cargo/git/db/ 25 | target/ 26 | key: macos-test-cargo-${{ hashFiles('**/Cargo.lock') }} 27 | - name: Lint 28 | run: | 29 | cargo clippy -- --deny warnings 30 | - name: Build test 31 | run: cargo build 32 | test_linux: 33 | name: Test (Linux) 34 | if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@v4 38 | - name: Setup | Cache Cargo 39 | uses: actions/cache@v4 40 | with: 41 | path: | 42 | ~/.cargo/bin/ 43 | ~/.cargo/registry/index/ 44 | ~/.cargo/registry/cache/ 45 | ~/.cargo/git/db/ 46 | target/ 47 | key: linux-test-cargo-${{ hashFiles('**/Cargo.lock') }} 48 | - name: Lint 49 | run: | 50 | cargo clippy -- --deny warnings 51 | - name: Build test 52 | run: cargo build 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | # this project is prone to changes, so it's better to leave it out of the gitignore 9 | #Cargo.lock 10 | 11 | # These are backup files generated by rustfmt 12 | **/*.rs.bk 13 | 14 | # MSVC Windows builds of rustc generate these, which store debugging information 15 | *.pdb 16 | 17 | # RustRover 18 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 19 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 20 | # and can be added to the global gitignore or merged into this file. For a more nuclear 21 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 22 | #.idea/ 23 | 24 | # Added by cargo 25 | 26 | /target 27 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gpui-plot" 3 | version = "0.1.1" 4 | edition = "2021" 5 | authors = ["Jakku Sakura "] 6 | repository = "https://github.com/JakkuSakura/gpui-plot" 7 | description = "Plotting with gpui in Rust" 8 | license = "MIT" 9 | 10 | [dependencies] 11 | gpui = { git = "https://github.com/zed-industries/zed", rev = "044eb7b99048e79da5aa3f2dd489ff3ed8f97a32" } 12 | parking_lot = "0.12" 13 | plotters = { version = "0.3.7", features = ["default"], optional = true } 14 | plotters-gpui = { git = "https://github.com/JakkuSakura/plotters-gpui", rev = "b7dc582e2c13f3cc2b63370a0eb7e7b0712f211c", features = ["plotters"], optional = true } 15 | tracing = "0.1" 16 | chrono = "0.4" 17 | 18 | [features] 19 | default = ["plotters"] 20 | plotters = ["dep:plotters-gpui", "dep:plotters"] 21 | 22 | 23 | # because plotters' font-kit might fail 24 | [patch.crates-io] 25 | pathfinder_simd = { git = "https://github.com/theoparis/pathfinder.git" } 26 | plotters = { git = "https://github.com/JakkuSakura/plotters", tag = "v0.3.7-gpui" } 27 | plotters-backend = { git = "https://github.com/JakkuSakura/plotters", tag = "v0.3.7-gpui" } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Jakku Sakura 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gpui-plot 2 | 3 | Native plotting in Rust using the `gpui` library. 4 | 5 | **Pull Requests Welcomed** 6 | 7 | Can also make use of `plotters` for some of the figures. 8 | Zooming and panning is implemented 9 | 10 | Note: the version on crates.io is only a placeholder, as gpui is not published on crates.io yet. Please use the git 11 | version. 12 | 13 | https://github.com/JakkuSakura/gpui-plot 14 | 15 | ## Usage 16 | 17 | ```toml 18 | [dependencies] 19 | gpui-plot = { git = "https://github.com/JakkuSakura/gpui-plot" } 20 | 21 | # because plotters' font-kit might fail 22 | [patch.crates-io] 23 | pathfinder_simd = { git = "https://github.com/theoparis/pathfinder.git" } 24 | plotters = { git = "https://github.com/JakkuSakura/plotters", tag = "v0.3.7-gpui" } 25 | plotters-backend = { git = "https://github.com/JakkuSakura/plotters", tag = "v0.3.7-gpui" } 26 | 27 | ``` 28 | 29 | ## Example 30 | 31 | This is an example of 2 animated plots on same canvas. one is native and the other is using plotters. 32 | You can see the FPS is 120 on my Macbook Pro. 33 | ![Example](resources/example.png) 34 | 35 | ```rust 36 | use gpui::{ 37 | div, prelude::*, px, size, App, AppContext, Application, Bounds, Entity, Window, WindowBounds, 38 | WindowOptions, 39 | }; 40 | use gpui_plot::figure::axes::{AxesContext, AxesModel}; 41 | use gpui_plot::figure::figure::{FigureModel, FigureView}; 42 | use gpui_plot::geometry::{point2, size2, AxesBounds, AxisRange, GeometryAxes, Line}; 43 | use parking_lot::RwLock; 44 | use plotters::prelude::*; 45 | use std::sync::Arc; 46 | 47 | #[allow(unused)] 48 | struct MainView { 49 | model: Arc>, 50 | // animation: Animation, 51 | figure: Entity, 52 | } 53 | 54 | impl MainView { 55 | fn new(_window: &mut Window, cx: &mut App) -> Self { 56 | let model = FigureModel::new("Example Figure".to_string()); 57 | let model = Arc::new(RwLock::new(model)); 58 | let mut animation = Animation::new(0.0, 100.0, 0.1); 59 | 60 | let axes_bounds = AxesBounds::new( 61 | AxisRange::new(0.0, 100.0).unwrap(), 62 | AxisRange::new(0.0, 100.0).unwrap(), 63 | ); 64 | let size = size2(10.0, 10.0); 65 | let axes_model = Arc::new(RwLock::new(AxesModel::new(axes_bounds, size))); 66 | { 67 | let mut model = model.write(); 68 | let mut plot = model.add_plot().write(); 69 | plot.add_axes(axes_model.clone()) 70 | .write() 71 | .plot(animation.clone()); 72 | plot.add_axes_plotters(axes_model.clone(), move |area, cx| { 73 | let mut chart = ChartBuilder::on(&area) 74 | .x_label_area_size(30) 75 | .y_label_area_size(30) 76 | .build_cartesian_2d(cx.axes_bounds.x.to_range(), cx.axes_bounds.y.to_range()) 77 | .unwrap(); 78 | 79 | chart.configure_mesh().draw().unwrap(); 80 | for shift in 10..20 { 81 | let line = animation.next_line((shift * 5) as f64); 82 | 83 | chart 84 | .draw_series(LineSeries::new( 85 | line.points.iter().map(|p| (p.x, p.y)), 86 | &BLACK, 87 | )) 88 | .unwrap(); 89 | } 90 | }) 91 | } 92 | Self { 93 | figure: cx.new(|_| FigureView::new(model.clone())), 94 | model, 95 | // animation, 96 | } 97 | } 98 | } 99 | 100 | impl Render for MainView { 101 | fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { 102 | let id = cx.entity_id(); 103 | cx.defer(move |app| app.notify(id)); 104 | 105 | div() 106 | .size_full() 107 | .flex_col() 108 | .bg(gpui::white()) 109 | .text_color(gpui::black()) 110 | .child(self.figure.clone()) 111 | } 112 | } 113 | #[derive(Clone)] 114 | struct Animation { 115 | start: f64, 116 | end: f64, 117 | step: f64, 118 | time_start: std::time::Instant, 119 | } 120 | impl Animation { 121 | fn new(start: f64, end: f64, step: f64) -> Self { 122 | Self { 123 | start, 124 | end, 125 | step, 126 | time_start: std::time::Instant::now(), 127 | } 128 | } 129 | fn next_line(&mut self, shift: f64) -> Line { 130 | let mut line = Line::new(); 131 | let t = self.time_start.elapsed().as_secs_f64() * 10.0; 132 | let mut x = self.start; 133 | while x <= self.end { 134 | let y = (x + t).sin(); 135 | line.add_point(point2(x, y + shift)); 136 | x += self.step; 137 | } 138 | line 139 | } 140 | } 141 | impl GeometryAxes for Animation { 142 | type X = f64; 143 | type Y = f64; 144 | 145 | fn render_axes(&mut self, cx: &mut AxesContext) { 146 | for shift in 0..10 { 147 | let mut line = self.next_line((shift * 5) as f64); 148 | line.render_axes(cx); 149 | } 150 | } 151 | } 152 | 153 | fn main() { 154 | Application::new().run(|cx: &mut App| { 155 | let bounds = Bounds::centered(None, size(px(800.0), px(600.0)), cx); 156 | cx.open_window( 157 | WindowOptions { 158 | window_bounds: Some(WindowBounds::Windowed(bounds)), 159 | ..Default::default() 160 | }, 161 | |window, cx| cx.new(|cx| MainView::new(window, cx)), 162 | ) 163 | .unwrap(); 164 | }); 165 | } 166 | 167 | ``` 168 | 169 | ## Performance 170 | 171 | gpui-plot try to maintain very good performance compared to that in javascript/python, even better than CPU based 172 | plotting libraries 173 | 174 | ### Metal Performance HUD 175 | 176 | Use the following environment variable to enable Metal Performance HUD: 177 | 178 | ```text 179 | MTL_HUD_ENABLED=1 180 | Enables the Metal Performance HUD. 181 | ``` 182 | 183 | ```shell 184 | MTL_HUD_ENABLED=1 cargo run ... 185 | ``` 186 | -------------------------------------------------------------------------------- /examples/animation.rs: -------------------------------------------------------------------------------- 1 | use gpui::{ 2 | div, prelude::*, px, size, App, AppContext, Application, Bounds, Entity, Hsla, Window, 3 | WindowBounds, WindowOptions, 4 | }; 5 | use gpui_plot::figure::axes::AxesContext; 6 | use gpui_plot::figure::axes::AxesModel; 7 | use gpui_plot::figure::figure::{FigureModel, FigureView}; 8 | use gpui_plot::figure::grid::GridModel; 9 | use gpui_plot::geometry::{ 10 | point2, AxesBounds, AxisRange, GeometryAxes, Line, Marker, MarkerShape, Markers, 11 | }; 12 | use parking_lot::RwLock; 13 | use plotters::prelude::*; 14 | use std::sync::Arc; 15 | 16 | #[allow(unused)] 17 | struct MainView { 18 | model: Arc>, 19 | axes_model: Arc>>, 20 | animation: Animation, 21 | figure: Entity, 22 | } 23 | 24 | impl MainView { 25 | fn new(_window: &mut Window, cx: &mut App) -> Self { 26 | let model = FigureModel::new("Example Figure".to_string()); 27 | let model = Arc::new(RwLock::new(model)); 28 | let animation = Animation::new(0.0, 100.0, 0.1); 29 | 30 | let axes_bounds = AxesBounds::new(AxisRange::new(0.0, 100.0), AxisRange::new(0.0, 100.0)); 31 | let grid = GridModel::from_numbers(10, 10); 32 | let axes_model = Arc::new(RwLock::new(AxesModel::new(axes_bounds, grid))); 33 | 34 | Self { 35 | figure: cx.new(|_| FigureView::new(model.clone())), 36 | axes_model: axes_model.clone(), 37 | model, 38 | animation, 39 | } 40 | } 41 | } 42 | 43 | impl Render for MainView { 44 | fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { 45 | let id = cx.entity_id(); 46 | cx.defer(move |app| app.notify(id)); 47 | 48 | let mut model = self.model.write(); 49 | model.clear_plots(); 50 | model.add_plot_with(|plot| { 51 | plot.add_axes_with(self.axes_model.clone(), |axes| { 52 | axes.clear_elements(); 53 | axes.plot(self.animation.clone()); 54 | 55 | let mut markers = Markers::new(); 56 | markers 57 | .add_marker(Marker::new(point2(0.0, 0.0), px(10.0)).shape(MarkerShape::Circle)); 58 | markers.add_marker( 59 | Marker::new(point2(100.0, 0.0), px(10.0)) 60 | .shape(MarkerShape::Square) 61 | .color(Hsla::red()), 62 | ); 63 | markers.add_marker( 64 | Marker::new(point2(0.0, 100.0), px(10.0)) 65 | .shape(MarkerShape::TriangleUp) 66 | .color(Hsla::green()), 67 | ); 68 | markers.add_marker( 69 | Marker::new(point2(100.0, 100.0), px(10.0)) 70 | .shape(MarkerShape::TriangleDown) 71 | .color(Hsla::blue()), 72 | ); 73 | axes.plot(markers); 74 | }); 75 | let mut animation = self.animation.clone(); 76 | plot.add_axes_plotters(self.axes_model.clone(), move |area, cx| { 77 | let mut chart = ChartBuilder::on(&area) 78 | .x_label_area_size(30) 79 | .y_label_area_size(30) 80 | .build_cartesian_2d(cx.axes_bounds.x.to_range(), cx.axes_bounds.y.to_range()) 81 | .unwrap(); 82 | 83 | chart.configure_mesh().draw().unwrap(); 84 | for shift in 0..20 { 85 | let line = animation.next_line((shift * 5) as f64, false); 86 | 87 | chart 88 | .draw_series(LineSeries::new( 89 | line.points.iter().map(|p| (p.x, p.y)), 90 | &RED, 91 | )) 92 | .unwrap(); 93 | } 94 | }); 95 | // plot.update(); 96 | }); 97 | div() 98 | .size_full() 99 | .flex_col() 100 | .bg(gpui::white()) 101 | .text_color(gpui::black()) 102 | .child(self.figure.clone()) 103 | } 104 | } 105 | #[derive(Clone)] 106 | struct Animation { 107 | start: f64, 108 | end: f64, 109 | step: f64, 110 | time_start: std::time::Instant, 111 | } 112 | impl Animation { 113 | fn new(start: f64, end: f64, step: f64) -> Self { 114 | Self { 115 | start, 116 | end, 117 | step, 118 | time_start: std::time::Instant::now(), 119 | } 120 | } 121 | fn next_line(&mut self, shift: f64, transpose: bool) -> Line { 122 | let mut line = Line::new().color(Hsla::green()); 123 | let t = self.time_start.elapsed().as_secs_f64() * 10.0; 124 | let mut x = self.start; 125 | while x <= self.end { 126 | let y = (x + t).sin(); 127 | let mut point = point2(x, y + shift); 128 | if transpose { 129 | point = point.flip(); 130 | } 131 | line.add_point(point); 132 | x += self.step; 133 | } 134 | line 135 | } 136 | } 137 | impl GeometryAxes for Animation { 138 | type X = f64; 139 | type Y = f64; 140 | 141 | fn render_axes(&mut self, cx: &mut AxesContext) { 142 | for shift in 0..20 { 143 | let mut line = self.next_line((shift * 5) as f64, true); 144 | line.render_axes(cx); 145 | } 146 | } 147 | } 148 | 149 | fn main() { 150 | Application::new().run(|cx: &mut App| { 151 | let bounds = Bounds::centered(None, size(px(800.0), px(600.0)), cx); 152 | cx.open_window( 153 | WindowOptions { 154 | window_bounds: Some(WindowBounds::Windowed(bounds)), 155 | ..Default::default() 156 | }, 157 | |window, cx| cx.new(|cx| MainView::new(window, cx)), 158 | ) 159 | .unwrap(); 160 | }); 161 | } 162 | -------------------------------------------------------------------------------- /examples/stock.rs: -------------------------------------------------------------------------------- 1 | use gpui::{ 2 | div, prelude::*, px, size, App, AppContext, Application, Bounds, Entity, Window, WindowBounds, 3 | WindowOptions, 4 | }; 5 | use gpui_plot::figure::axes::AxesModel; 6 | use gpui_plot::figure::figure::{FigureModel, FigureView}; 7 | use gpui_plot::figure::grid::GridModel; 8 | use gpui_plot::geometry::{AxesBounds, AxisRange}; 9 | use parking_lot::RwLock; 10 | use plotters::prelude::*; 11 | use std::sync::Arc; 12 | 13 | fn parse_time(t: &str) -> chrono::NaiveDate { 14 | chrono::NaiveDate::parse_from_str(t, "%Y-%m-%d").unwrap() 15 | } 16 | 17 | #[derive(Clone)] 18 | struct StockChart { 19 | data: Vec<(&'static str, f32, f32, f32, f32)>, 20 | } 21 | 22 | impl StockChart { 23 | fn new() -> Self { 24 | let data = vec![ 25 | ("2019-04-25", 130.06, 131.37, 128.83, 129.15), 26 | ("2019-04-24", 125.79, 125.85, 124.52, 125.01), 27 | ("2019-04-23", 124.1, 125.58, 123.83, 125.44), 28 | ("2019-04-22", 122.62, 124.0000, 122.57, 123.76), 29 | ("2019-04-18", 122.19, 123.52, 121.3018, 123.37), 30 | ("2019-04-17", 121.24, 121.85, 120.54, 121.77), 31 | ("2019-04-16", 121.64, 121.65, 120.1, 120.77), 32 | ("2019-04-15", 120.94, 121.58, 120.57, 121.05), 33 | ("2019-04-12", 120.64, 120.98, 120.37, 120.95), 34 | ("2019-04-11", 120.54, 120.85, 119.92, 120.33), 35 | ("2019-04-10", 119.76, 120.35, 119.54, 120.19), 36 | ("2019-04-09", 118.63, 119.54, 118.58, 119.28), 37 | ("2019-04-08", 119.81, 120.02, 118.64, 119.93), 38 | ("2019-04-05", 119.39, 120.23, 119.37, 119.89), 39 | ("2019-04-04", 120.1, 120.23, 118.38, 119.36), 40 | ("2019-04-03", 119.86, 120.43, 119.15, 119.97), 41 | ("2019-04-02", 119.06, 119.48, 118.52, 119.19), 42 | ("2019-04-01", 118.95, 119.1085, 118.1, 119.02), 43 | ("2019-03-29", 118.07, 118.32, 116.96, 117.94), 44 | ("2019-03-28", 117.44, 117.58, 116.13, 116.93), 45 | ("2019-03-27", 117.875, 118.21, 115.5215, 116.77), 46 | ("2019-03-26", 118.62, 118.705, 116.85, 117.91), 47 | ("2019-03-25", 116.56, 118.01, 116.3224, 117.66), 48 | ("2019-03-22", 119.5, 119.59, 117.04, 117.05), 49 | ("2019-03-21", 117.135, 120.82, 117.09, 120.22), 50 | ("2019-03-20", 117.39, 118.75, 116.71, 117.52), 51 | ("2019-03-19", 118.09, 118.44, 116.99, 117.65), 52 | ("2019-03-18", 116.17, 117.61, 116.05, 117.57), 53 | ("2019-03-15", 115.34, 117.25, 114.59, 115.91), 54 | ("2019-03-14", 114.54, 115.2, 114.33, 114.59), 55 | ]; 56 | 57 | Self { data } 58 | } 59 | fn filter( 60 | &self, 61 | from: chrono::NaiveDate, 62 | to: chrono::NaiveDate, 63 | ) -> impl Iterator { 64 | self.data.iter().filter(move |(date, _, _, _, _)| { 65 | let date = parse_time(date); 66 | date >= from && date <= to 67 | }) 68 | } 69 | } 70 | 71 | #[allow(unused)] 72 | struct MainView { 73 | axes_model: Arc>>, 74 | model: Arc>, 75 | stock_chart: StockChart, 76 | figure: Entity, 77 | } 78 | 79 | impl MainView { 80 | fn new(_window: &mut Window, cx: &mut App) -> Self { 81 | let figure = FigureModel::new("Example Figure".to_string()); 82 | let model = Arc::new(RwLock::new(figure)); 83 | let stock_chart = StockChart::new(); 84 | let (to_date, from_date) = ( 85 | parse_time(stock_chart.data[0].0) + chrono::Duration::days(1), 86 | parse_time(stock_chart.data[29].0) - chrono::Duration::days(1), 87 | ); 88 | let axes_bounds = AxesBounds::new( 89 | AxisRange::new(from_date, to_date), 90 | AxisRange::new(100.0f32, 140.0f32), 91 | ); 92 | let grid_type = GridModel::from_numbers(10, 10); 93 | let axes_model = Arc::new(RwLock::new(AxesModel::new(axes_bounds, grid_type))); 94 | 95 | Self { 96 | axes_model: axes_model.clone(), 97 | stock_chart, 98 | figure: cx.new(|_| FigureView::new(model.clone())), 99 | model, 100 | } 101 | } 102 | } 103 | 104 | impl Render for MainView { 105 | fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { 106 | let id = cx.entity_id(); 107 | cx.defer(move |app| app.notify(id)); 108 | let mut model = self.model.write(); 109 | model.clear_plots(); 110 | model.add_plot_with(|plot| { 111 | let stock_chart = self.stock_chart.clone(); 112 | plot.add_axes_plotters(self.axes_model.clone(), move |area, cx| { 113 | let mut chart = ChartBuilder::on(&area) 114 | .x_label_area_size(40) 115 | .y_label_area_size(40) 116 | .caption("MSFT Stock Price", ("sans-serif", 50.0).into_font()) 117 | .build_cartesian_2d(cx.axes_bounds.x.to_range(), cx.axes_bounds.y.to_range()) 118 | .unwrap(); 119 | 120 | chart.configure_mesh().draw().unwrap(); 121 | 122 | chart 123 | .configure_mesh() 124 | .light_line_style(WHITE) 125 | .draw() 126 | .unwrap(); 127 | 128 | chart 129 | .draw_series( 130 | stock_chart 131 | .filter(cx.axes_bounds.x.min(), cx.axes_bounds.x.max()) 132 | .map(|x| { 133 | CandleStick::new( 134 | parse_time(x.0), 135 | x.1, 136 | x.2, 137 | x.3, 138 | x.4, 139 | GREEN.filled(), 140 | RED, 141 | 15, 142 | ) 143 | }), 144 | ) 145 | .unwrap(); 146 | }) 147 | }); 148 | div() 149 | .size_full() 150 | .flex_col() 151 | .bg(gpui::white()) 152 | .text_color(gpui::black()) 153 | .child(self.figure.clone()) 154 | } 155 | } 156 | 157 | fn main() { 158 | Application::new().run(|cx: &mut App| { 159 | let bounds = Bounds::centered(None, size(px(800.0), px(600.0)), cx); 160 | cx.open_window( 161 | WindowOptions { 162 | window_bounds: Some(WindowBounds::Windowed(bounds)), 163 | ..Default::default() 164 | }, 165 | |window, cx| cx.new(|cx| MainView::new(window, cx)), 166 | ) 167 | .unwrap(); 168 | }); 169 | } 170 | -------------------------------------------------------------------------------- /examples/window.rs: -------------------------------------------------------------------------------- 1 | use gpui::{ 2 | div, prelude::*, px, rgb, size, App, Application, Bounds, Context, SharedString, Timer, Window, 3 | WindowBounds, WindowKind, WindowOptions, 4 | }; 5 | 6 | struct SubWindow { 7 | custom_titlebar: bool, 8 | } 9 | 10 | fn button(text: &str, on_click: impl Fn(&mut Window, &mut App) + 'static) -> impl IntoElement { 11 | div() 12 | .id(SharedString::from(text.to_string())) 13 | .flex_none() 14 | .px_2() 15 | .bg(rgb(0xf7f7f7)) 16 | .active(|this| this.opacity(0.85)) 17 | .border_1() 18 | .border_color(rgb(0xe0e0e0)) 19 | .rounded_sm() 20 | .cursor_pointer() 21 | .child(text.to_string()) 22 | .on_click(move |_, window, cx| on_click(window, cx)) 23 | } 24 | 25 | impl Render for SubWindow { 26 | fn render(&mut self, _window: &mut Window, _: &mut Context) -> impl IntoElement { 27 | div() 28 | .flex() 29 | .flex_col() 30 | .bg(rgb(0xffffff)) 31 | .size_full() 32 | .gap_2() 33 | .when(self.custom_titlebar, |cx| { 34 | cx.child( 35 | div() 36 | .flex() 37 | .h(px(32.)) 38 | .px_4() 39 | .bg(gpui::blue()) 40 | .text_color(gpui::white()) 41 | .w_full() 42 | .child( 43 | div() 44 | .flex() 45 | .items_center() 46 | .justify_center() 47 | .size_full() 48 | .child("Custom Titlebar"), 49 | ), 50 | ) 51 | }) 52 | .child( 53 | div() 54 | .p_8() 55 | .gap_2() 56 | .child("SubWindow") 57 | .child(button("Close", |window, _| { 58 | window.remove_window(); 59 | })), 60 | ) 61 | } 62 | } 63 | 64 | struct WindowDemo {} 65 | 66 | impl Render for WindowDemo { 67 | fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { 68 | let window_bounds = 69 | WindowBounds::Windowed(Bounds::centered(None, size(px(300.0), px(300.0)), cx)); 70 | 71 | div() 72 | .p_4() 73 | .flex() 74 | .flex_wrap() 75 | .bg(rgb(0xffffff)) 76 | .size_full() 77 | .justify_center() 78 | .items_center() 79 | .gap_2() 80 | .child(button("Normal", move |_, cx| { 81 | cx.open_window( 82 | WindowOptions { 83 | window_bounds: Some(window_bounds), 84 | ..Default::default() 85 | }, 86 | |_, cx| { 87 | cx.new(|_| SubWindow { 88 | custom_titlebar: false, 89 | }) 90 | }, 91 | ) 92 | .unwrap(); 93 | })) 94 | .child(button("Popup", move |_, cx| { 95 | cx.open_window( 96 | WindowOptions { 97 | window_bounds: Some(window_bounds), 98 | kind: WindowKind::PopUp, 99 | ..Default::default() 100 | }, 101 | |_, cx| { 102 | cx.new(|_| SubWindow { 103 | custom_titlebar: false, 104 | }) 105 | }, 106 | ) 107 | .unwrap(); 108 | })) 109 | .child(button("Custom Titlebar", move |_, cx| { 110 | cx.open_window( 111 | WindowOptions { 112 | titlebar: None, 113 | window_bounds: Some(window_bounds), 114 | ..Default::default() 115 | }, 116 | |_, cx| { 117 | cx.new(|_| SubWindow { 118 | custom_titlebar: true, 119 | }) 120 | }, 121 | ) 122 | .unwrap(); 123 | })) 124 | .child(button("Invisible", move |_, cx| { 125 | cx.open_window( 126 | WindowOptions { 127 | show: false, 128 | window_bounds: Some(window_bounds), 129 | ..Default::default() 130 | }, 131 | |_, cx| { 132 | cx.new(|_| SubWindow { 133 | custom_titlebar: false, 134 | }) 135 | }, 136 | ) 137 | .unwrap(); 138 | })) 139 | .child(button("Unmovable", move |_, cx| { 140 | cx.open_window( 141 | WindowOptions { 142 | is_movable: false, 143 | titlebar: None, 144 | window_bounds: Some(window_bounds), 145 | ..Default::default() 146 | }, 147 | |_, cx| { 148 | cx.new(|_| SubWindow { 149 | custom_titlebar: false, 150 | }) 151 | }, 152 | ) 153 | .unwrap(); 154 | })) 155 | .child(button("Hide Application", |window, cx| { 156 | cx.hide(); 157 | 158 | // Restore the application after 3 seconds 159 | window 160 | .spawn(cx, async move |cx| { 161 | Timer::after(std::time::Duration::from_secs(3)).await; 162 | cx.update(|_, cx| { 163 | cx.activate(false); 164 | }) 165 | }) 166 | .detach(); 167 | })) 168 | } 169 | } 170 | 171 | fn main() { 172 | Application::new().run(|cx: &mut App| { 173 | let bounds = Bounds::centered(None, size(px(800.0), px(600.0)), cx); 174 | cx.open_window( 175 | WindowOptions { 176 | window_bounds: Some(WindowBounds::Windowed(bounds)), 177 | ..Default::default() 178 | }, 179 | |_, cx| cx.new(|_| WindowDemo {}), 180 | ) 181 | .unwrap(); 182 | }); 183 | } 184 | -------------------------------------------------------------------------------- /resources/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JakkuSakura/gpui-plot/c6bc65af55cb28af849a198f5862699029727a17/resources/example.png -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly-2025-03-20" 3 | profile = "minimal" 4 | components = ["rustfmt", "clippy"] -------------------------------------------------------------------------------- /src/figure/axes/mod.rs: -------------------------------------------------------------------------------- 1 | mod model; 2 | #[cfg(feature = "plotters")] 3 | mod plotters; 4 | mod view; 5 | 6 | pub use model::*; 7 | #[cfg(feature = "plotters")] 8 | pub use plotters::*; 9 | use std::any::Any; 10 | pub use view::*; 11 | 12 | use crate::geometry::{AxesBounds, AxesBoundsPixels, AxisType, GeometryAxes, Point2}; 13 | use gpui::{App, Bounds, MouseMoveEvent, Pixels, Point, Window}; 14 | 15 | pub trait Axes: Any { 16 | fn update(&mut self); 17 | fn new_render(&mut self); 18 | fn pan_begin(&mut self, position: Point); 19 | fn pan(&mut self, event: &MouseMoveEvent); 20 | fn pan_end(&mut self); 21 | fn zoom_begin(&mut self, position: Point); 22 | fn zoom(&mut self, factor: f64); 23 | fn zoom_end(&mut self); 24 | fn render(&mut self, bounds: Bounds, window: &mut Window, cx: &mut App); 25 | } 26 | 27 | pub struct AxesContext<'a, X: AxisType, Y: AxisType> { 28 | pub axes_bounds: AxesBounds, 29 | pub pixel_bounds: AxesBoundsPixels, 30 | pub cx: Option<(&'a mut Window, &'a mut App)>, 31 | } 32 | impl<'a, X: AxisType, Y: AxisType> AxesContext<'a, X, Y> { 33 | pub fn new(model: &AxesModel, window: &'a mut Window, cx: &'a mut App) -> Self { 34 | Self { 35 | axes_bounds: model.axes_bounds, 36 | pixel_bounds: model.pixel_bounds, 37 | cx: Some((window, cx)), 38 | } 39 | } 40 | pub fn new_without_context(model: &AxesModel) -> Self { 41 | Self { 42 | axes_bounds: model.axes_bounds, 43 | pixel_bounds: model.pixel_bounds, 44 | cx: None, 45 | } 46 | } 47 | pub fn transform_point(&self, point: Point2) -> Point { 48 | self.axes_bounds.transform_point(self.pixel_bounds, point) 49 | } 50 | pub fn plot(&mut self, mut element: impl AsMut) 51 | where 52 | T: GeometryAxes, 53 | { 54 | element.as_mut().render_axes(self); 55 | } 56 | pub fn contains(&self, point: Point2) -> bool { 57 | self.axes_bounds.contains(point) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/figure/axes/model.rs: -------------------------------------------------------------------------------- 1 | use crate::figure::axes::{Axes, AxesContext, AxesView}; 2 | use crate::figure::grid::GridModel; 3 | use crate::geometry::{ 4 | AxesBounds, AxesBoundsPixels, AxisType, GeometryAxes, GeometryAxesFn, GeometryPixels, Point2, 5 | }; 6 | use gpui::{size, App, Bounds, MouseMoveEvent, Pixels, Point, Window}; 7 | use std::fmt::Debug; 8 | 9 | pub(crate) struct PanState { 10 | initial_axes_bounds: AxesBounds, 11 | initial_pan_position: Point, 12 | } 13 | pub(crate) struct ZoomState { 14 | initial_axes_bounds: AxesBounds, 15 | pixel_bounds: AxesBoundsPixels, 16 | initial_zoom_position: Point, 17 | zoom_point: Point, 18 | } 19 | 20 | pub enum ViewUpdateType { 21 | /// Freely movable 22 | Free, 23 | /// Fixed to the current state 24 | Fixed, 25 | /// Automatically updated 26 | Auto, 27 | } 28 | 29 | pub struct AxesModel { 30 | pub axes_bounds: AxesBounds, 31 | pub pixel_bounds: AxesBoundsPixels, 32 | pub grid: GridModel, 33 | pub(crate) pan_state: Option>, 34 | pub(crate) zoom_state: Option>, 35 | pub(crate) event_processed: bool, 36 | pub(crate) elements: Vec>>, 37 | pub update_type: ViewUpdateType, 38 | } 39 | impl Debug for AxesModel { 40 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 41 | f.debug_struct("AxesModel") 42 | .field("axes_bounds", &self.axes_bounds) 43 | .finish() 44 | } 45 | } 46 | 47 | impl AxesModel { 48 | pub fn new(axes_bounds: AxesBounds, grid: GridModel) -> Self { 49 | let mut this = Self { 50 | axes_bounds, 51 | pixel_bounds: AxesBoundsPixels::from_bounds(Bounds::default()), 52 | grid, 53 | pan_state: None, 54 | zoom_state: None, 55 | event_processed: false, 56 | elements: Vec::new(), 57 | update_type: ViewUpdateType::Free, 58 | }; 59 | 60 | let cx1 = AxesContext::new_without_context(&this); 61 | this.grid.try_update_grid(&cx1); 62 | this 63 | } 64 | pub fn clear_elements(&mut self) { 65 | self.elements.clear(); 66 | } 67 | pub fn add_element(&mut self, element: Box>) { 68 | self.elements.push(element); 69 | } 70 | pub fn plot(&mut self, element: impl GeometryAxes + 'static) { 71 | self.elements.push(Box::new(element)); 72 | } 73 | pub fn plot_fn(&mut self, element: impl FnMut(&mut AxesContext) + Send + Sync + 'static) { 74 | self.elements.push(Box::new(GeometryAxesFn::new(element))); 75 | } 76 | 77 | pub fn update_scale(&mut self, shrunk_bounds: Bounds) { 78 | self.pixel_bounds = AxesBoundsPixels::from_bounds(shrunk_bounds); 79 | self.pixel_bounds.x.pixels_per_element = 80 | self.axes_bounds.x.pixels_per_element(self.pixel_bounds.x); 81 | self.pixel_bounds.y.pixels_per_element = 82 | -self.axes_bounds.y.pixels_per_element(self.pixel_bounds.y); 83 | } 84 | pub fn transform_point(&self, point: Point2) -> Point { 85 | self.axes_bounds.transform_point(self.pixel_bounds, point) 86 | } 87 | 88 | pub fn update_range(&mut self) { 89 | self.update_type = ViewUpdateType::Auto; 90 | // update the axes bounds 91 | let mut new_axes_bounds = None; 92 | for element in self.elements.iter_mut() { 93 | let Some(x) = element.get_x_range() else { 94 | continue; 95 | }; 96 | if x.size_in_f64() < 1e-6 { 97 | continue; 98 | } 99 | let Some(y) = element.get_y_range() else { 100 | continue; 101 | }; 102 | if y.size_in_f64() < 1e-6 { 103 | continue; 104 | } 105 | match new_axes_bounds { 106 | None => { 107 | new_axes_bounds = Some(AxesBounds::new(x, y)); 108 | } 109 | Some(ref mut bounds) => { 110 | if let Some(x_union) = bounds.x.union(&x) { 111 | bounds.x = x_union; 112 | } 113 | if let Some(y_union) = bounds.y.union(&y) { 114 | bounds.y = y_union; 115 | } 116 | } 117 | } 118 | } 119 | 120 | let Some(mut new_pixel_bounds) = new_axes_bounds else { 121 | return; 122 | }; 123 | new_pixel_bounds.resize(1.1); 124 | self.axes_bounds = new_pixel_bounds; 125 | } 126 | pub fn update_grid(&mut self) { 127 | let cx1 = AxesContext::new_without_context(self); 128 | self.grid.update_grid(&cx1); 129 | } 130 | pub fn try_update_grid(&mut self) { 131 | let cx1 = AxesContext::new_without_context(self); 132 | self.grid.try_update_grid(&cx1); 133 | } 134 | } 135 | 136 | impl Axes for AxesModel { 137 | fn update(&mut self) { 138 | self.update_range(); 139 | self.update_grid(); 140 | } 141 | 142 | fn new_render(&mut self) { 143 | self.event_processed = false; 144 | self.try_update_grid(); 145 | } 146 | fn pan_begin(&mut self, position: Point) { 147 | if matches!(self.update_type, ViewUpdateType::Fixed) { 148 | return; 149 | } 150 | if self.event_processed { 151 | return; 152 | } 153 | self.pan_state = Some(PanState { 154 | initial_axes_bounds: self.axes_bounds, 155 | initial_pan_position: position, 156 | }); 157 | } 158 | 159 | fn pan(&mut self, event: &MouseMoveEvent) { 160 | if self.event_processed { 161 | return; 162 | } 163 | let Some(pan_state) = &self.pan_state else { 164 | return; 165 | }; 166 | let delta_pixels = event.position - pan_state.initial_pan_position; 167 | let delta_elements = size( 168 | self.axes_bounds 169 | .x 170 | .elements_per_pixels(-delta_pixels.x, self.pixel_bounds.x), 171 | self.axes_bounds 172 | .y 173 | .elements_per_pixels(delta_pixels.y, self.pixel_bounds.y), 174 | ); 175 | self.axes_bounds = pan_state.initial_axes_bounds + delta_elements; 176 | 177 | let cx1 = AxesContext::new_without_context(self); 178 | self.grid.try_update_grid(&cx1); 179 | } 180 | 181 | fn pan_end(&mut self) { 182 | if self.event_processed { 183 | return; 184 | } 185 | self.pan_state = None; 186 | } 187 | fn zoom_begin(&mut self, position: Point) { 188 | if self.event_processed { 189 | return; 190 | } 191 | self.zoom_state = Some(ZoomState { 192 | initial_axes_bounds: self.axes_bounds, 193 | pixel_bounds: self.pixel_bounds, 194 | initial_zoom_position: position, 195 | zoom_point: self 196 | .axes_bounds 197 | .transform_point_reverse_f64(self.pixel_bounds, position), 198 | }); 199 | } 200 | fn zoom(&mut self, factor: f64) { 201 | if self.event_processed { 202 | return; 203 | } 204 | let Some(zoom_state) = &mut self.zoom_state else { 205 | return; 206 | }; 207 | let zoom_point = zoom_state.zoom_point; 208 | let zoom_factor = factor; 209 | 210 | self.axes_bounds.x.min_to_base = 211 | (zoom_state.initial_axes_bounds.x.min_to_base - zoom_point.x) * zoom_factor 212 | + zoom_point.x; 213 | self.axes_bounds.x.max_to_base = 214 | (zoom_state.initial_axes_bounds.x.max_to_base - zoom_point.x) * zoom_factor 215 | + zoom_point.x; 216 | self.axes_bounds.y.min_to_base = 217 | (zoom_state.initial_axes_bounds.y.min_to_base - zoom_point.y) * zoom_factor 218 | + zoom_point.y; 219 | self.axes_bounds.y.max_to_base = 220 | (zoom_state.initial_axes_bounds.y.max_to_base - zoom_point.y) * zoom_factor 221 | + zoom_point.y; 222 | self.pixel_bounds.x.pixels_per_element = 223 | zoom_state.pixel_bounds.x.pixels_per_element / zoom_factor; 224 | self.pixel_bounds.y.pixels_per_element = 225 | zoom_state.pixel_bounds.y.pixels_per_element / zoom_factor; 226 | let afterwards_zoom_point = self 227 | .axes_bounds 228 | .transform_point_reverse_f64(self.pixel_bounds, zoom_state.initial_zoom_position); 229 | let diff = zoom_point - afterwards_zoom_point; 230 | self.axes_bounds.x.min_to_base += diff.x; 231 | self.axes_bounds.x.max_to_base += diff.x; 232 | self.axes_bounds.y.min_to_base += diff.y; 233 | self.axes_bounds.y.max_to_base += diff.y; 234 | // let adjusted_zoom_point = self 235 | // .axes_bounds 236 | // .transform_point_reverse_f64(self.pixel_bounds, zoom_state.initial_zoom_position); 237 | // assert_eq!(self.pixel_bounds.x, adjusted_zoom_point.x); 238 | // assert_eq!(self.pixel_bounds.y, adjusted_zoom_point.y); 239 | 240 | let cx1 = AxesContext::new_without_context(self); 241 | self.grid.try_update_grid(&cx1); 242 | } 243 | 244 | fn zoom_end(&mut self) { 245 | if self.event_processed { 246 | return; 247 | } 248 | self.zoom_state = None; 249 | } 250 | 251 | fn render(&mut self, bounds: Bounds, window: &mut Window, cx: &mut App) { 252 | AxesView::new(self).render_pixels(bounds, window, cx); 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/figure/axes/plotters.rs: -------------------------------------------------------------------------------- 1 | use crate::figure::axes::{Axes, AxesContext, AxesModel}; 2 | use crate::geometry::{AxisType, GeometryPixels}; 3 | use gpui::{px, App, Bounds, Edges, MouseMoveEvent, Pixels, Point, Window}; 4 | use parking_lot::RwLock; 5 | use plotters::coord::Shift; 6 | use plotters::prelude::*; 7 | use plotters_gpui::backend::GpuiBackend; 8 | use std::sync::Arc; 9 | use tracing::error; 10 | 11 | const CONTENT_BOARDER: Pixels = px(30.0); 12 | type ChartFn = Box, &mut AxesContext)>; 13 | pub struct PlottersModel { 14 | pub backend_color: RGBColor, 15 | pub chart: ChartFn, 16 | model: Arc>>, 17 | } 18 | impl PlottersModel { 19 | pub fn new(model: Arc>>, chart: ChartFn) -> Self { 20 | Self { 21 | backend_color: RGBColor(0, 0, 0), 22 | chart, 23 | model, 24 | } 25 | } 26 | } 27 | impl Axes for PlottersModel { 28 | fn update(&mut self) { 29 | self.model.write().update_range(); 30 | } 31 | 32 | fn new_render(&mut self) { 33 | self.model.write().new_render(); 34 | } 35 | 36 | fn pan_begin(&mut self, position: Point) { 37 | self.model.write().pan_begin(position); 38 | } 39 | 40 | fn pan(&mut self, event: &MouseMoveEvent) { 41 | self.model.write().pan(event); 42 | } 43 | 44 | fn pan_end(&mut self) { 45 | self.model.write().pan_end(); 46 | } 47 | fn zoom_begin(&mut self, position: Point) { 48 | self.model.write().zoom_begin(position); 49 | } 50 | fn zoom(&mut self, delta: f64) { 51 | self.model.write().zoom(delta); 52 | } 53 | fn zoom_end(&mut self) { 54 | self.model.write().zoom_end(); 55 | } 56 | fn render(&mut self, bounds: Bounds, window: &mut Window, cx: &mut App) { 57 | PlottersView::new(self).render_pixels(bounds, window, cx); 58 | } 59 | } 60 | pub struct PlottersView<'a, X: AxisType, Y: AxisType> { 61 | pub model: &'a mut PlottersModel, 62 | } 63 | impl<'a, X: AxisType, Y: AxisType> PlottersView<'a, X, Y> { 64 | pub fn new(model: &'a mut PlottersModel) -> Self { 65 | Self { model } 66 | } 67 | 68 | pub fn plot( 69 | &mut self, 70 | bounds: Bounds, 71 | window: &mut Window, 72 | cx: &mut App, 73 | ) -> Result<(), DrawingAreaErrorKind> { 74 | let mut root = GpuiBackend::new(bounds, window, cx).into_drawing_area(); 75 | let base_model = self.model.model.read(); 76 | let cx1 = &mut AxesContext::new_without_context(&base_model); 77 | (self.model.chart)(&mut root, cx1); 78 | root.present()?; 79 | Ok(()) 80 | } 81 | } 82 | impl<'a, X: AxisType, Y: AxisType> GeometryPixels for PlottersView<'a, X, Y> { 83 | fn render_pixels(&mut self, bounds: Bounds, window: &mut Window, cx: &mut App) { 84 | let bounds = bounds.extend(Edges { 85 | top: px(0.0), 86 | right: -CONTENT_BOARDER, 87 | bottom: px(0.0), 88 | left: px(0.0), 89 | }); 90 | let shrunk_bounds = bounds.extend(Edges { 91 | top: px(0.0), 92 | right: px(0.0), 93 | bottom: -CONTENT_BOARDER, 94 | left: -CONTENT_BOARDER, 95 | }); 96 | self.model.model.write().update_scale(shrunk_bounds); 97 | if let Err(err) = self.plot(bounds, window, cx) { 98 | error!("failed to plot: {}", err); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/figure/axes/view.rs: -------------------------------------------------------------------------------- 1 | use crate::figure::axes::model::AxesModel; 2 | use crate::figure::axes::AxesContext; 3 | use crate::figure::grid::GridView; 4 | use crate::figure::ticks::TicksView; 5 | use crate::geometry::{AxisType, GeometryAxes, GeometryPixels, Line}; 6 | use gpui::{px, App, Bounds, Edges, Pixels, Window}; 7 | 8 | pub struct AxesView<'a, X: AxisType, Y: AxisType> { 9 | pub model: &'a mut AxesModel, 10 | } 11 | impl<'a, X: AxisType, Y: AxisType> AxesView<'a, X, Y> { 12 | pub fn new(model: &'a mut AxesModel) -> Self { 13 | Self { model } 14 | } 15 | pub fn paint(&mut self, window: &mut Window, cx: &mut App, bounds: Bounds) { 16 | { 17 | let model = &self.model; 18 | let shrunk_bounds = model.pixel_bounds.into_bounds(); 19 | for (x, y) in [ 20 | (shrunk_bounds.origin, shrunk_bounds.top_right()), 21 | (shrunk_bounds.top_right(), shrunk_bounds.bottom_right()), 22 | (shrunk_bounds.bottom_right(), shrunk_bounds.bottom_left()), 23 | (shrunk_bounds.bottom_left(), shrunk_bounds.origin), 24 | ] { 25 | Line::between_points(x.into(), y.into()).render(window, cx, Some(shrunk_bounds)); 26 | } 27 | } 28 | 29 | let cx1 = &mut AxesContext::new(self.model, window, cx); 30 | 31 | let mut ticks = TicksView::new(self.model); 32 | { 33 | let (window, cx1) = cx1.cx.as_mut().unwrap(); 34 | ticks.render(window, cx1, bounds); 35 | } 36 | let mut grid = GridView::new(&self.model.grid); 37 | { 38 | grid.render_axes(cx1); 39 | } 40 | 41 | for element in self.model.elements.iter_mut() { 42 | element.render_axes(cx1); 43 | } 44 | } 45 | } 46 | 47 | const CONTENT_BOARDER: Pixels = px(30.0); 48 | 49 | impl<'a, X: AxisType, Y: AxisType> GeometryPixels for AxesView<'a, X, Y> { 50 | fn render_pixels(&mut self, bounds: Bounds, window: &mut Window, cx: &mut App) { 51 | let shrunk_bounds = bounds.extend(Edges { 52 | top: px(-0.0), 53 | right: -CONTENT_BOARDER, 54 | bottom: -CONTENT_BOARDER, 55 | left: -CONTENT_BOARDER, 56 | }); 57 | self.model.update_scale(shrunk_bounds); 58 | self.paint(window, cx, bounds); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/figure/figure.rs: -------------------------------------------------------------------------------- 1 | use crate::figure::plot::{PlotModel, PlotView}; 2 | use crate::figure::text::centered_text; 3 | use gpui::{ 4 | div, App, AppContext, Context, Entity, IntoElement, ParentElement, Render, Styled, Window, 5 | }; 6 | use parking_lot::RwLock; 7 | use std::fmt::Debug; 8 | use std::sync::Arc; 9 | 10 | pub struct FigureModel { 11 | pub title: String, 12 | pub plots: Vec>>, 13 | } 14 | impl Debug for FigureModel { 15 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 16 | f.debug_struct("FigureContext") 17 | .field("title", &self.title) 18 | .field("plots", &self.plots) 19 | .finish() 20 | } 21 | } 22 | 23 | impl FigureModel { 24 | pub fn new(title: String) -> Self { 25 | Self { 26 | title, 27 | plots: Vec::new(), 28 | } 29 | } 30 | pub fn clear_plots(&mut self) { 31 | self.plots.clear(); 32 | } 33 | pub fn add_plot(&mut self) -> &mut Arc> { 34 | #[allow(clippy::arc_with_non_send_sync)] 35 | let model = Arc::new(RwLock::new(PlotModel::new())); 36 | self.plots.push(model); 37 | self.plots.last_mut().unwrap() 38 | } 39 | pub fn add_plot_with(&mut self, plot_fn: impl FnOnce(&mut PlotModel)) { 40 | #[allow(clippy::arc_with_non_send_sync)] 41 | let model = Arc::new(RwLock::new(PlotModel::new())); 42 | self.plots.push(model.clone()); 43 | 44 | plot_fn(&mut model.write()); 45 | } 46 | /// Update the figure model. 47 | pub fn update(&mut self) { 48 | for plot in self.plots.iter() { 49 | let mut plot = plot.write(); 50 | plot.update(); 51 | } 52 | } 53 | } 54 | 55 | /// A Figure is per definition of matplotlib: https://matplotlib.org/stable/users/explain/quick_start.html 56 | /// It contains a title, a canvas, 2 axes, and a legend. 57 | /// The canvas is the main area where the plot is drawn. 58 | pub struct FigureView { 59 | pub model: Arc>, 60 | pub plots: Vec>, 61 | } 62 | impl FigureView { 63 | pub fn new(model: Arc>) -> Self { 64 | Self { 65 | model, 66 | plots: Vec::new(), 67 | } 68 | } 69 | fn add_views(&mut self, cx: &mut App) { 70 | for i in self.plots.len()..self.model.read().plots.len() { 71 | let plot_model = self.model.read().plots[i].clone(); 72 | let view = PlotView::new(plot_model.clone()); 73 | let plot = cx.new(move |_| view); 74 | self.plots.push(plot); 75 | } 76 | } 77 | } 78 | impl Render for FigureView { 79 | fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { 80 | self.add_views(cx); 81 | div() 82 | .flex() 83 | .flex_col() 84 | .size_full() 85 | .bg(gpui::white()) 86 | .text_color(gpui::black()) 87 | .child(centered_text(self.model.read().title.clone())) 88 | .children(self.plots.clone()) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/figure/grid.rs: -------------------------------------------------------------------------------- 1 | use crate::figure::axes::AxesContext; 2 | use crate::geometry::{point2, size2, AxisType, GeometryAxes, Line, Size2}; 3 | use gpui::{size, Size}; 4 | 5 | pub enum GridType { 6 | Density(Size2), 7 | Numbers(usize, usize), 8 | } 9 | 10 | pub struct GridModel { 11 | pub ty: GridType, 12 | pub movable: bool, 13 | pub grid_x_lines: Vec, 14 | pub grid_y_lines: Vec, 15 | } 16 | impl GridModel { 17 | pub fn from_density(x: X::Delta, y: Y::Delta) -> Self { 18 | Self::new(GridType::Density(size2(x, y))) 19 | } 20 | pub fn from_numbers(x: usize, y: usize) -> Self { 21 | Self::new(GridType::Numbers(x, y)) 22 | } 23 | pub fn new(ty: GridType) -> Self { 24 | Self { 25 | ty, 26 | movable: true, 27 | grid_x_lines: Vec::new(), 28 | grid_y_lines: Vec::new(), 29 | } 30 | } 31 | pub fn with_fixed(mut self) -> Self { 32 | self.movable = false; 33 | self 34 | } 35 | fn should_update_grid(&self, _axes_bounds: &AxesContext) -> bool { 36 | if self.grid_x_lines.is_empty() || self.grid_y_lines.is_empty() { 37 | return true; 38 | } 39 | !self.movable 40 | } 41 | pub fn try_update_grid(&mut self, axes_bounds: &AxesContext) { 42 | if !self.should_update_grid(axes_bounds) { 43 | return; 44 | } 45 | self.update_grid(axes_bounds); 46 | } 47 | pub fn update_grid(&mut self, axes_bounds: &AxesContext) { 48 | let density = match self.ty { 49 | GridType::Density(density) => density.to_f64(), 50 | GridType::Numbers(x, y) => size( 51 | axes_bounds.axes_bounds.x.size_in_f64() / x as f64, 52 | axes_bounds.axes_bounds.y.size_in_f64() / y as f64, 53 | ), 54 | }; 55 | self.update_grid_by_density(axes_bounds, density); 56 | } 57 | fn update_grid_by_density(&mut self, axes_bounds: &AxesContext, density: Size) { 58 | // TODO: clap beforehand to have better performance 59 | self.grid_x_lines = axes_bounds 60 | .axes_bounds 61 | .x 62 | .iter_step_by_f64(density.width) 63 | .collect(); 64 | self.grid_x_lines 65 | .retain(|x| axes_bounds.axes_bounds.x.contains(*x)); 66 | self.grid_y_lines = axes_bounds 67 | .axes_bounds 68 | .y 69 | .iter_step_by_f64(density.height) 70 | .collect(); 71 | self.grid_y_lines 72 | .retain(|y| axes_bounds.axes_bounds.y.contains(*y)); 73 | } 74 | } 75 | 76 | pub struct GridView<'a, X: AxisType, Y: AxisType> { 77 | model: &'a GridModel, 78 | } 79 | impl<'a, X: AxisType, Y: AxisType> GridView<'a, X, Y> { 80 | pub fn new(model: &'a GridModel) -> Self { 81 | Self { model } 82 | } 83 | } 84 | impl<'a, X: AxisType, Y: AxisType> GeometryAxes for GridView<'a, X, Y> { 85 | type X = X; 86 | type Y = Y; 87 | fn render_axes(&mut self, cx: &mut AxesContext) { 88 | let grid = self.model; 89 | for x in grid.grid_x_lines.iter().cloned() { 90 | let top_point = point2(x, cx.axes_bounds.y.min()); 91 | let bottom_point = point2(x, cx.axes_bounds.y.max()); 92 | let mut line = Line::between_points(top_point, bottom_point); 93 | line.render_axes(cx); 94 | } 95 | 96 | for y in grid.grid_y_lines.iter().cloned() { 97 | let left_point = point2(cx.axes_bounds.x.min(), y); 98 | let right_point = point2(cx.axes_bounds.x.max(), y); 99 | let mut line = Line::between_points(left_point, right_point); 100 | line.render_axes(cx); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/figure/mod.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use parking_lot::RwLock; 3 | 4 | pub mod axes; 5 | #[allow(clippy::module_inception)] 6 | pub mod figure; 7 | pub mod grid; 8 | pub mod plot; 9 | pub mod text; 10 | pub mod ticks; 11 | 12 | // figure -> (sub)plot -> axes(multiple) 13 | pub type SharedModel = Arc>; 14 | -------------------------------------------------------------------------------- /src/figure/plot.rs: -------------------------------------------------------------------------------- 1 | use crate::figure::axes::{Axes, AxesContext, AxesModel, PlottersModel}; 2 | use crate::figure::SharedModel; 3 | use crate::fps::FpsModel; 4 | use crate::geometry::AxisType; 5 | use gpui::{ 6 | canvas, div, Bounds, Context, InteractiveElement, IntoElement, MouseButton, MouseDownEvent, 7 | MouseMoveEvent, ParentElement, Pixels, Point, Render, ScrollDelta, ScrollWheelEvent, Styled, 8 | Window, 9 | }; 10 | use parking_lot::RwLock; 11 | use plotters::coord::Shift; 12 | use plotters::drawing::DrawingArea; 13 | use plotters_gpui::backend::GpuiBackend; 14 | use std::fmt::Debug; 15 | use std::sync::Arc; 16 | use std::time::{Duration, Instant}; 17 | 18 | pub struct PlotModel { 19 | pub panning: bool, 20 | pub zooming: bool, 21 | pub zoom_swipe_precision: f64, 22 | pub zoom_scroll_precision: f64, 23 | pub zoom_rubberband_precision: f64, 24 | pub fps: FpsModel, 25 | pub bounds: Bounds, 26 | pub axes: Vec>, 27 | } 28 | impl Debug for PlotModel { 29 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 30 | f.debug_struct("PlotModel") 31 | .field("panning", &self.panning) 32 | .field("zooming", &self.zooming) 33 | .field("zoom_swipe_precision", &self.zoom_swipe_precision) 34 | .field("zoom_scroll_precision", &self.zoom_scroll_precision) 35 | .field("zoom_rubberband_precision", &self.zoom_rubberband_precision) 36 | .field("bounds", &self.bounds) 37 | .field("axes", &self.axes.len()) 38 | .finish() 39 | } 40 | } 41 | impl Default for PlotModel { 42 | fn default() -> Self { 43 | Self::new() 44 | } 45 | } 46 | impl PlotModel { 47 | pub fn new() -> Self { 48 | Self { 49 | panning: false, 50 | zooming: false, 51 | zoom_swipe_precision: 1.0 / 200.0, 52 | zoom_scroll_precision: 1.0 / 100.0, 53 | zoom_rubberband_precision: 1.0 / 400.0, 54 | fps: FpsModel::new(), 55 | bounds: Bounds::default(), 56 | axes: Vec::new(), 57 | } 58 | } 59 | pub fn clear_axes(&mut self) { 60 | self.axes.clear(); 61 | } 62 | 63 | pub fn add_axes( 64 | &mut self, 65 | model: SharedModel>, 66 | ) -> SharedModel> { 67 | self.axes.push(model.clone() as SharedModel); 68 | model 69 | } 70 | pub fn add_axes_with( 71 | &mut self, 72 | model: SharedModel>, 73 | plot_fn: impl FnOnce(&mut AxesModel), 74 | ) { 75 | plot_fn(&mut model.write()); 76 | self.axes.push(model as SharedModel); 77 | } 78 | 79 | #[cfg(feature = "plotters")] 80 | pub fn add_axes_plotters( 81 | &mut self, 82 | model: SharedModel>, 83 | draw: impl FnMut(&mut DrawingArea, &mut AxesContext) + 'static, 84 | ) { 85 | let axes = PlottersModel::new(model, Box::new(draw)); 86 | 87 | self.axes.push(Arc::new(RwLock::new(axes))); 88 | } 89 | pub fn update(&mut self) { 90 | for axes in self.axes.iter_mut() { 91 | axes.write().update(); 92 | } 93 | } 94 | pub fn pan_begin(&mut self, position: Point) { 95 | if self.panning { 96 | return; 97 | } 98 | self.panning = true; 99 | for axes in self.axes.iter_mut() { 100 | axes.write().pan_begin(position); 101 | } 102 | } 103 | pub fn pan(&mut self, event: &MouseMoveEvent) { 104 | if !self.panning { 105 | return; 106 | } 107 | for axes in self.axes.iter_mut() { 108 | axes.write().pan(event); 109 | } 110 | } 111 | pub fn pan_end(&mut self) { 112 | if !self.panning { 113 | return; 114 | } 115 | self.panning = false; 116 | for axes in self.axes.iter_mut() { 117 | axes.write().pan_end(); 118 | } 119 | } 120 | pub fn zoom_begin(&mut self, position: Point) { 121 | if self.zooming { 122 | return; 123 | } 124 | self.zooming = true; 125 | for axes in self.axes.iter_mut() { 126 | axes.write().zoom_begin(position); 127 | } 128 | } 129 | pub fn zoom(&mut self, factor: f64) { 130 | if !self.zooming { 131 | return; 132 | } 133 | for axes in self.axes.iter_mut() { 134 | axes.write().zoom(factor); 135 | } 136 | } 137 | pub fn zoom_end(&mut self) { 138 | if !self.zooming { 139 | return; 140 | } 141 | self.zooming = false; 142 | for axes in self.axes.iter_mut() { 143 | axes.write().zoom_end(); 144 | } 145 | } 146 | } 147 | 148 | #[derive(Clone)] 149 | pub struct PlotView { 150 | pub model: Arc>, 151 | pub last_zoom_ts: Option, 152 | pub acc_zoom_in: f64, 153 | pub last_zoom_rb: Option>, 154 | } 155 | impl PlotView { 156 | pub fn new(model: Arc>) -> Self { 157 | Self { 158 | model, 159 | last_zoom_ts: None, 160 | acc_zoom_in: 0.0, 161 | last_zoom_rb: None, 162 | } 163 | } 164 | 165 | fn try_clean_zoom(&mut self) { 166 | if let Some(last_time) = self.last_zoom_ts { 167 | if last_time.elapsed() > Duration::from_secs_f32(0.2) { 168 | self.model.write().zoom_end(); 169 | self.last_zoom_ts = None; 170 | self.acc_zoom_in = 0.0; 171 | } 172 | } 173 | } 174 | 175 | pub fn zoom( 176 | &mut self, 177 | zoom_point: Point, 178 | zoom_in: f64, 179 | _window: &mut Window, 180 | cx: &mut Context, 181 | ) { 182 | self.try_clean_zoom(); 183 | let mut model = self.model.write(); 184 | if self.last_zoom_ts.is_none() { 185 | model.zoom_begin(zoom_point); 186 | } 187 | self.last_zoom_ts = Some(Instant::now()); 188 | self.acc_zoom_in += zoom_in; 189 | let factor = self.acc_zoom_in.exp(); 190 | model.zoom(factor); 191 | cx.notify(); 192 | } 193 | pub fn zoom_rubberband( 194 | &mut self, 195 | zoom_point: Point, 196 | _window: &mut Window, 197 | cx: &mut Context, 198 | ) { 199 | let Some(last_zoom_point) = self.last_zoom_rb else { 200 | return; 201 | }; 202 | let delta = zoom_point.y - last_zoom_point.y; 203 | let zoom_in = -delta.0 as f64 * self.model.read().zoom_rubberband_precision; 204 | let factor = zoom_in.exp(); 205 | self.model.write().zoom(factor); 206 | cx.notify() 207 | } 208 | } 209 | impl Render for PlotView { 210 | fn render(&mut self, _window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement { 211 | self.try_clean_zoom(); 212 | let len = self.model.read().axes.len(); 213 | for axes in 0..len { 214 | let axes = self.model.read().axes[axes].clone(); 215 | axes.write().new_render(); 216 | } 217 | 218 | div() 219 | .size_full() 220 | .child( 221 | canvas(|_, _window, _cx| (), { 222 | let this = self.clone(); 223 | move |bounds, _ele: (), window, cx| { 224 | this.model.write().bounds = bounds; 225 | for axes in this.model.write().axes.iter_mut() { 226 | axes.write().render(bounds, window, cx); 227 | } 228 | } 229 | }) 230 | .size_full(), 231 | ) 232 | .on_mouse_down( 233 | MouseButton::Left, 234 | cx.listener(|this, ev: &MouseDownEvent, _window, _cx| { 235 | let mut model = this.model.write(); 236 | model.pan_begin(ev.position); 237 | }), 238 | ) 239 | .on_mouse_down( 240 | MouseButton::Right, 241 | cx.listener(|this, ev: &MouseDownEvent, _window, _cx| { 242 | this.try_clean_zoom(); 243 | this.last_zoom_rb = Some(ev.position); 244 | this.model.write().zoom_begin(ev.position); 245 | }), 246 | ) 247 | .on_mouse_move(cx.listener(|this, ev: &MouseMoveEvent, window, cx| { 248 | match ev.pressed_button { 249 | Some(MouseButton::Left) => { 250 | let mut model = this.model.write(); 251 | model.pan(ev); 252 | cx.notify(); 253 | } 254 | // it won't work on MacOS 255 | Some(MouseButton::Right) => { 256 | this.zoom_rubberband(ev.position, window, cx); 257 | } 258 | _ => {} 259 | } 260 | })) 261 | .on_mouse_up( 262 | MouseButton::Left, 263 | cx.listener(|this, _ev, _window, _cx| { 264 | let mut model = this.model.write(); 265 | model.pan_end(); 266 | }), 267 | ) 268 | .on_mouse_up( 269 | MouseButton::Right, 270 | cx.listener(|this, _ev, _window, cx| { 271 | this.last_zoom_rb = None; 272 | this.model.write().zoom_end(); 273 | cx.notify(); 274 | }), 275 | ) 276 | .on_scroll_wheel(cx.listener(|this, ev: &ScrollWheelEvent, window, cx| { 277 | let model = this.model.read(); 278 | let zoom_in = match ev.delta { 279 | ScrollDelta::Pixels(p) => { 280 | // println!("Scroll event captured: {:?}", p); 281 | // Swipe swipe down to zoom in. This is aligned with Google Maps and some tools like Mac Mouse Fix or Scroll Inverter 282 | -p.y.0 as f64 * model.zoom_swipe_precision 283 | } 284 | ScrollDelta::Lines(l) => { 285 | // println!("Scroll event in lines {:?}, ignoring.",&q); 286 | // Scroll up to zoom in 287 | l.y as f64 * model.zoom_scroll_precision 288 | } 289 | }; 290 | drop(model); 291 | 292 | this.zoom(ev.position, zoom_in, window, cx); 293 | })) 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /src/figure/text.rs: -------------------------------------------------------------------------------- 1 | use gpui::{div, IntoElement, ParentElement, Styled}; 2 | 3 | pub fn centered_text(text: impl Into) -> impl IntoElement { 4 | div() 5 | .flex() 6 | .content_center() 7 | .justify_center() 8 | .child(text.into()) 9 | } 10 | -------------------------------------------------------------------------------- /src/figure/ticks.rs: -------------------------------------------------------------------------------- 1 | use crate::figure::axes::AxesModel; 2 | use crate::geometry::{point2, AxisType, GeometryPixels, Text}; 3 | use gpui::{px, App, Bounds, Pixels, Window}; 4 | 5 | #[derive(Clone)] 6 | pub struct TicksView<'a, X: AxisType, Y: AxisType> { 7 | context: &'a AxesModel, 8 | } 9 | impl<'a, X: AxisType, Y: AxisType> TicksView<'a, X, Y> { 10 | pub fn new(context: &'a AxesModel) -> Self { 11 | Self { context } 12 | } 13 | pub fn render(&mut self, window: &mut Window, cx: &mut App, pixel_bounds: Bounds) { 14 | let context = self.context; 15 | let size = px(12.0); 16 | 17 | for x in context.grid.grid_x_lines.iter().cloned() { 18 | let text = x.format(); 19 | let x_px = context.axes_bounds.x.transform(context.pixel_bounds.x, x) 20 | - size * text.len() / 2.0 * 0.5; 21 | let y_px = context.pixel_bounds.max_y() + px(3.0); 22 | Text { 23 | origin: point2(x_px, y_px), 24 | size, 25 | text, 26 | } 27 | .render(window, cx, Some(pixel_bounds)); 28 | } 29 | for y in context.grid.grid_y_lines.iter().cloned() { 30 | let text = y.format(); 31 | 32 | let x_px = context.pixel_bounds.min_x() - size * text.len() as f32 * 0.5 - px(3.0); 33 | let y_px = context.axes_bounds.y.transform(context.pixel_bounds.y, y) - size / 2.0; 34 | Text { 35 | origin: point2(x_px, y_px), 36 | size, 37 | text, 38 | } 39 | .render(window, cx, Some(pixel_bounds)); 40 | } 41 | } 42 | } 43 | impl<'a, X: AxisType, Y: AxisType> GeometryPixels for TicksView<'a, X, Y> { 44 | fn render_pixels(&mut self, bounds: Bounds, window: &mut Window, cx: &mut App) { 45 | self.render(window, cx, bounds); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/fps.rs: -------------------------------------------------------------------------------- 1 | use gpui::{Context, IntoElement, Render, Window}; 2 | use std::time::Instant; 3 | 4 | pub struct FpsModel { 5 | pub fps: f32, 6 | pub last_time: Instant, 7 | pub last_fps: f64, 8 | pub frame_count: f32, 9 | } 10 | impl Default for FpsModel { 11 | fn default() -> Self { 12 | Self::new() 13 | } 14 | } 15 | impl FpsModel { 16 | pub fn new() -> Self { 17 | Self { 18 | fps: 0.0, 19 | last_time: Instant::now(), 20 | last_fps: 0.0, 21 | frame_count: 0.0, 22 | } 23 | } 24 | pub fn next_fps(&mut self) -> f32 { 25 | let now = Instant::now(); 26 | let delta = now - self.last_time; 27 | self.frame_count += 1.0; 28 | if delta.as_secs_f32() >= 1.0 { 29 | self.fps = self.frame_count / delta.as_secs_f32(); 30 | self.frame_count = 0.0; 31 | self.last_time = now; 32 | } 33 | self.fps 34 | } 35 | } 36 | pub struct FpsView { 37 | pub model: FpsModel, 38 | } 39 | impl Default for FpsView { 40 | fn default() -> Self { 41 | Self::new() 42 | } 43 | } 44 | impl FpsView { 45 | pub fn new() -> Self { 46 | Self { 47 | model: FpsModel::new(), 48 | } 49 | } 50 | } 51 | impl Render for FpsView { 52 | fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { 53 | let fps = self.model.next_fps(); 54 | let text = format!("fps: {:.2}", fps); 55 | text 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/geometry/axis.rs: -------------------------------------------------------------------------------- 1 | use crate::geometry::point::Point2; 2 | use crate::utils::math::display_double_smartly; 3 | use chrono::{NaiveDate, Timelike}; 4 | use gpui::{point, px, Bounds, Pixels, Point, Size}; 5 | use std::cmp::Ordering; 6 | use std::fmt::Debug; 7 | use std::ops::{Add, Range, Sub}; 8 | 9 | pub trait AxisType: 10 | Copy 11 | + Clone 12 | + PartialOrd 13 | + Debug 14 | + Send 15 | + Sync 16 | + Add 17 | + Sub 18 | + Sub 19 | + 'static 20 | { 21 | type Delta: AxisType; 22 | fn format(&self) -> String; 23 | fn to_f64(&self) -> f64; 24 | fn from_f64(value: f64) -> Self; 25 | } 26 | impl AxisType for f32 { 27 | type Delta = f32; 28 | fn format(&self) -> String { 29 | display_double_smartly(*self as f64) 30 | } 31 | fn to_f64(&self) -> f64 { 32 | *self as f64 33 | } 34 | fn from_f64(value: f64) -> Self { 35 | value as f32 36 | } 37 | } 38 | impl AxisType for f64 { 39 | type Delta = f64; 40 | fn format(&self) -> String { 41 | display_double_smartly(*self) 42 | } 43 | fn to_f64(&self) -> f64 { 44 | *self 45 | } 46 | fn from_f64(value: f64) -> Self { 47 | value 48 | } 49 | } 50 | impl AxisType for NaiveDate { 51 | type Delta = chrono::Duration; 52 | fn format(&self) -> String { 53 | self.to_string() 54 | } 55 | fn to_f64(&self) -> f64 { 56 | self.and_hms_opt(0, 0, 0) 57 | .unwrap() 58 | .and_utc() 59 | .timestamp_nanos_opt() 60 | .unwrap() as f64 61 | } 62 | fn from_f64(value: f64) -> Self { 63 | let timestamp = value as i64; 64 | let date = chrono::DateTime::from_timestamp_nanos(timestamp); 65 | date.date_naive() 66 | } 67 | } 68 | 69 | impl AxisType for chrono::NaiveDateTime { 70 | type Delta = chrono::Duration; 71 | fn format(&self) -> String { 72 | self.to_string() 73 | } 74 | fn to_f64(&self) -> f64 { 75 | self.and_utc().timestamp_nanos_opt().unwrap() as f64 76 | } 77 | fn from_f64(value: f64) -> Self { 78 | let timestamp = value as i64; 79 | let date = chrono::DateTime::from_timestamp_nanos(timestamp); 80 | date.naive_utc() 81 | } 82 | } 83 | impl AxisType for chrono::NaiveTime { 84 | type Delta = chrono::Duration; 85 | fn format(&self) -> String { 86 | self.to_string() 87 | } 88 | fn to_f64(&self) -> f64 { 89 | self.num_seconds_from_midnight() as f64 / 1_000_000_000.0 + self.nanosecond() as f64 90 | } 91 | fn from_f64(value: f64) -> Self { 92 | let seconds = value as u32; 93 | let nanoseconds = ((value - seconds as f64) * 1_000_000_000.0) as u32; 94 | let time = chrono::NaiveTime::from_num_seconds_from_midnight_opt(seconds, nanoseconds); 95 | time.expect("out of range") 96 | } 97 | } 98 | impl AxisType for chrono::DateTime { 99 | type Delta = chrono::Duration; 100 | fn format(&self) -> String { 101 | self.to_string() 102 | } 103 | fn to_f64(&self) -> f64 { 104 | self.timestamp_nanos_opt().unwrap() as f64 105 | } 106 | fn from_f64(value: f64) -> Self { 107 | let timestamp = value as i64; 108 | chrono::DateTime::::from_timestamp_nanos(timestamp) 109 | } 110 | } 111 | impl AxisType for chrono::Duration { 112 | type Delta = chrono::Duration; 113 | fn format(&self) -> String { 114 | self.to_string() 115 | } 116 | fn to_f64(&self) -> f64 { 117 | self.num_nanoseconds().expect("out of range") as f64 118 | } 119 | fn from_f64(value: f64) -> Self { 120 | chrono::Duration::nanoseconds(value as i64) 121 | } 122 | } 123 | 124 | impl AxisType for std::time::Duration { 125 | type Delta = std::time::Duration; 126 | fn format(&self) -> String { 127 | format!("{:?}", self) 128 | } 129 | 130 | fn to_f64(&self) -> f64 { 131 | self.as_nanos() as f64 132 | } 133 | 134 | fn from_f64(value: f64) -> Self { 135 | std::time::Duration::from_nanos(value as u64) 136 | } 137 | } 138 | 139 | /// more for internal use 140 | impl AxisType for Pixels { 141 | type Delta = Pixels; 142 | fn format(&self) -> String { 143 | self.to_string() 144 | } 145 | 146 | fn to_f64(&self) -> f64 { 147 | self.0 as f64 148 | } 149 | 150 | fn from_f64(value: f64) -> Self { 151 | Pixels(value as f32) 152 | } 153 | } 154 | #[derive(Clone, Copy, Debug)] 155 | pub struct AxisRangePixels { 156 | min: Pixels, 157 | max: Pixels, 158 | size: f64, 159 | pub(crate) pixels_per_element: f64, 160 | } 161 | impl AxisRangePixels { 162 | pub fn from_bounds(min: Pixels, max: Pixels, size: f64) -> Self { 163 | Self { 164 | min, 165 | max, 166 | size, 167 | pixels_per_element: f64::NAN, 168 | } 169 | } 170 | } 171 | impl Add for AxisRangePixels { 172 | type Output = Self; 173 | 174 | fn add(self, rhs: Pixels) -> Self::Output { 175 | Self { 176 | min: self.min + rhs, 177 | max: self.max + rhs, 178 | size: self.size, 179 | pixels_per_element: self.pixels_per_element, 180 | } 181 | } 182 | } 183 | impl AxesBoundsPixels { 184 | pub fn from_bounds(bounds: Bounds) -> Self { 185 | Self { 186 | x: AxisRangePixels::from_bounds( 187 | bounds.origin.x, 188 | bounds.origin.x + bounds.size.width, 189 | bounds.size.width.0 as f64, 190 | ), 191 | y: AxisRangePixels::from_bounds( 192 | bounds.origin.y + bounds.size.height, 193 | bounds.origin.y, 194 | bounds.size.height.0 as f64, 195 | ), 196 | } 197 | } 198 | pub fn into_bounds(self) -> Bounds { 199 | Bounds { 200 | origin: Point { 201 | x: self.x.min, 202 | y: self.y.max, 203 | }, 204 | size: Size { 205 | width: px(self.x.size as f32), 206 | height: px(self.y.size as f32), 207 | }, 208 | } 209 | } 210 | } 211 | 212 | #[derive(Clone, Copy, Debug)] 213 | pub struct AxisRange { 214 | pub(crate) base: T, 215 | pub(crate) min_to_base: f64, 216 | pub(crate) max_to_base: f64, 217 | } 218 | 219 | impl AxisRange { 220 | /// Only for plotters' usage. Our range is always inclusive. 221 | pub fn to_range(&self) -> Range { 222 | self.min()..self.max() 223 | } 224 | pub fn new(min: T, max: T) -> Self { 225 | let base = T::from_f64((max - min).to_f64() / 2.0); 226 | Self::new_with_base(base, min, max) 227 | } 228 | pub fn new_with_base(base: T, min: T, max: T) -> Self { 229 | Self { 230 | base, 231 | min_to_base: (min - base).to_f64(), 232 | max_to_base: (max - base).to_f64(), 233 | } 234 | } 235 | pub fn new_with_base_f64(base: T, min: f64, max: f64) -> Self { 236 | Self { 237 | base, 238 | min_to_base: min, 239 | max_to_base: max, 240 | } 241 | } 242 | pub fn resize(&mut self, factor: f64) { 243 | let min = self.min_to_base; 244 | let max = self.max_to_base; 245 | let midpoint = (min + max) / 2.0; 246 | let size = (max - min) * factor; 247 | self.min_to_base = midpoint - size / 2.0; 248 | self.max_to_base = midpoint + size / 2.0; 249 | } 250 | pub fn set_min(&mut self, min: T) { 251 | self.min_to_base = (min - self.base).to_f64(); 252 | } 253 | pub fn set_max(&mut self, max: T) { 254 | self.max_to_base = (max - self.base).to_f64(); 255 | } 256 | pub fn min(&self) -> T { 257 | self.base + T::Delta::from_f64(self.min_to_base) 258 | } 259 | pub fn max(&self) -> T { 260 | self.base + T::Delta::from_f64(self.max_to_base) 261 | } 262 | 263 | pub fn contains(&self, value: T) -> bool { 264 | value >= self.min() && value <= self.max() 265 | } 266 | pub fn size_in_f64(&self) -> f64 { 267 | self.max_to_base - self.min_to_base 268 | } 269 | 270 | pub fn pixels_per_element(&self, bounds: AxisRangePixels) -> f64 { 271 | bounds.size / self.size_in_f64() 272 | } 273 | 274 | pub fn elements_per_pixels(&self, delta: Pixels, bounds: AxisRangePixels) -> f64 { 275 | delta.0 as f64 * self.size_in_f64() / bounds.size 276 | } 277 | /// Transform a value from the range `[min, max]` to the range `[bounds.min, bounds.max]` 278 | pub fn transform(&self, bounds: AxisRangePixels, value: T) -> Pixels { 279 | let adjusted_pixels = 280 | (value - self.min()).to_f64() * bounds.pixels_per_element + bounds.min.0 as f64; 281 | Pixels(adjusted_pixels as f32) 282 | } 283 | 284 | pub fn transform_reverse_f64(&self, bounds: AxisRangePixels, value: f64) -> f64 { 285 | self.min_to_base + (value - bounds.min.0 as f64) / bounds.pixels_per_element 286 | } 287 | pub fn iter_step_by(&self, step: T::Delta) -> impl Iterator + '_ { 288 | let mut current = self.min(); 289 | std::iter::from_fn(move || { 290 | if current > self.max() { 291 | return None; 292 | } 293 | let result = current; 294 | current = current + step; 295 | Some(result) 296 | }) 297 | } 298 | pub fn iter_step_by_f64(&self, step: f64) -> impl Iterator + '_ { 299 | let mut current = self.min_to_base; 300 | std::iter::from_fn(move || { 301 | if current > self.max_to_base { 302 | return None; 303 | } 304 | let result = self.base + T::Delta::from_f64(current); 305 | current += step; 306 | Some(result) 307 | }) 308 | } 309 | pub fn union(&self, other: &Self) -> Option { 310 | let base = match self.base.partial_cmp(&other.base)? { 311 | Ordering::Less => self.base, 312 | Ordering::Greater => other.base, 313 | Ordering::Equal => self.base, 314 | }; 315 | let min = match self.min().partial_cmp(&other.min())? { 316 | Ordering::Less => self.min(), 317 | Ordering::Greater => other.min(), 318 | Ordering::Equal => self.min(), 319 | }; 320 | let max = match self.max().partial_cmp(&other.max())? { 321 | Ordering::Less => other.max(), 322 | Ordering::Greater => self.max(), 323 | Ordering::Equal => self.max(), 324 | }; 325 | 326 | Some(Self::new_with_base(base, min, max)) 327 | } 328 | } 329 | 330 | impl Add for AxisRange { 331 | type Output = Self; 332 | 333 | fn add(self, rhs: f64) -> Self::Output { 334 | Self { 335 | base: self.base, 336 | min_to_base: self.min_to_base + rhs, 337 | max_to_base: self.max_to_base + rhs, 338 | } 339 | } 340 | } 341 | 342 | #[derive(Clone, Copy, Debug)] 343 | pub struct AxesBoundsPixels { 344 | pub x: AxisRangePixels, 345 | pub y: AxisRangePixels, 346 | } 347 | impl AxesBoundsPixels { 348 | pub fn min_x(&self) -> Pixels { 349 | self.x.min 350 | } 351 | pub fn max_x(&self) -> Pixels { 352 | self.x.max 353 | } 354 | pub fn width(&self) -> Pixels { 355 | px(self.x.size as f32) 356 | } 357 | pub fn min_y(&self) -> Pixels { 358 | self.y.max 359 | } 360 | pub fn max_y(&self) -> Pixels { 361 | self.y.min 362 | } 363 | pub fn height(&self) -> Pixels { 364 | px(self.y.size as f32) 365 | } 366 | } 367 | impl Add> for AxesBoundsPixels { 368 | type Output = Self; 369 | 370 | fn add(self, rhs: Point) -> Self::Output { 371 | Self { 372 | x: self.x + rhs.x, 373 | y: self.y + rhs.y, 374 | } 375 | } 376 | } 377 | 378 | #[derive(Clone, Copy, Debug)] 379 | pub struct AxesBounds { 380 | pub x: AxisRange, 381 | pub y: AxisRange, 382 | } 383 | 384 | impl AxesBounds { 385 | pub fn new(x: AxisRange, y: AxisRange) -> Self { 386 | Self { x, y } 387 | } 388 | pub fn resize(&mut self, factor: f64) { 389 | self.x.resize(factor); 390 | self.y.resize(factor); 391 | } 392 | 393 | pub fn transform_point(&self, bounds: AxesBoundsPixels, point: Point2) -> Point { 394 | Point { 395 | x: self.x.transform(bounds.x, point.x), 396 | y: self.y.transform(bounds.y, point.y), 397 | } 398 | } 399 | 400 | pub fn transform_point_reverse_f64( 401 | &self, 402 | bounds: AxesBoundsPixels, 403 | p: Point, 404 | ) -> Point { 405 | point( 406 | self.x.transform_reverse_f64(bounds.x, p.x.0 as f64), 407 | self.y.transform_reverse_f64(bounds.y, p.y.0 as f64), 408 | ) 409 | } 410 | 411 | pub fn min_point_f64(&self) -> Point { 412 | point(self.x.min_to_base, self.y.min_to_base) 413 | } 414 | 415 | pub fn max_point_f64(&self) -> Point { 416 | point(self.x.max_to_base, self.y.max_to_base) 417 | } 418 | pub fn contains(&self, point: Point2) -> bool { 419 | self.x.contains(point.x) && self.y.contains(point.y) 420 | } 421 | } 422 | // add Point 423 | impl Add> for AxesBounds { 424 | type Output = Self; 425 | 426 | fn add(mut self, rhs: Size) -> Self::Output { 427 | self.x = self.x + rhs.width; 428 | self.y = self.y + rhs.height; 429 | self 430 | } 431 | } 432 | -------------------------------------------------------------------------------- /src/geometry/line.rs: -------------------------------------------------------------------------------- 1 | use crate::figure::axes::AxesContext; 2 | use crate::geometry::{AxisRange, AxisType, GeometryAxes, GeometryPixels, Point2}; 3 | use gpui::{px, App, Bounds, Hsla, PathBuilder, Pixels, Window}; 4 | use tracing::warn; 5 | 6 | #[derive(Clone, Debug)] 7 | pub struct Line { 8 | pub points: Vec>, 9 | pub width: Pixels, 10 | pub color: Hsla, 11 | } 12 | impl Default for Line { 13 | fn default() -> Self { 14 | Self::new() 15 | } 16 | } 17 | impl Line { 18 | pub fn new() -> Self { 19 | Self { 20 | points: vec![], 21 | width: 1.0.into(), 22 | color: gpui::black(), 23 | } 24 | } 25 | pub fn between_points(start: Point2, end: Point2) -> Self { 26 | let mut line = Self::new(); 27 | line.add_point(start); 28 | line.add_point(end); 29 | line 30 | } 31 | pub fn width(mut self, width: Pixels) -> Self { 32 | self.width = width; 33 | self 34 | } 35 | pub fn color(mut self, color: Hsla) -> Self { 36 | self.color = color; 37 | self 38 | } 39 | pub fn add_point(&mut self, point: Point2) { 40 | self.points.push(point); 41 | } 42 | pub fn clear(&mut self) { 43 | self.points.clear(); 44 | } 45 | } 46 | impl Line { 47 | pub fn render( 48 | &mut self, 49 | window: &mut Window, 50 | _cx: &mut App, 51 | pixel_bounds: Option>, 52 | ) { 53 | match pixel_bounds { 54 | Some(bounds) => { 55 | let mut i = 0; 56 | let mut line = Line::new().width(self.width).color(self.color); 57 | while i < self.points.len() { 58 | while i < self.points.len() { 59 | let point = self.points[i]; 60 | 61 | // Check if the point is within the bounds 62 | if !bounds.contains(&point.into()) { 63 | // break and draw the line 64 | i += 1; 65 | break; 66 | } 67 | 68 | line.add_point(point); 69 | i += 1; 70 | } 71 | if line.points.len() > 1 { 72 | line.render(window, _cx, None); 73 | } 74 | line.clear(); 75 | } 76 | } 77 | None => { 78 | if self.points.is_empty() { 79 | warn!("Line must have at least 1 points to render"); 80 | return; 81 | } 82 | 83 | let mut builder = PathBuilder::stroke(px(self.width.0)); 84 | let Some(first_p) = self.points.first() else { 85 | return; 86 | }; 87 | 88 | builder.move_to((*first_p).into()); 89 | for p in self.points.iter().skip(1) { 90 | builder.line_to((*p).into()); 91 | } 92 | 93 | if let Ok(path) = builder.build() { 94 | window.paint_path(path, self.color); 95 | } 96 | } 97 | } 98 | } 99 | } 100 | impl GeometryPixels for Line { 101 | fn render_pixels(&mut self, bounds: Bounds, window: &mut Window, cx: &mut App) { 102 | self.render(window, cx, Some(bounds)); 103 | } 104 | } 105 | impl GeometryAxes for Line { 106 | type X = X; 107 | type Y = Y; 108 | fn get_x_range(&self) -> Option> { 109 | if self.points.is_empty() { 110 | return None; 111 | } 112 | let mut min = self.points[0].x; 113 | let mut max = self.points[0].x; 114 | for point in self.points.iter() { 115 | if point.x < min { 116 | min = point.x; 117 | } 118 | if point.x > max { 119 | max = point.x; 120 | } 121 | } 122 | Some(AxisRange::new(min, max)) 123 | } 124 | fn get_y_range(&self) -> Option> { 125 | if self.points.is_empty() { 126 | return None; 127 | } 128 | let mut min = self.points[0].y; 129 | let mut max = self.points[0].y; 130 | for point in self.points.iter() { 131 | if point.y < min { 132 | min = point.y; 133 | } 134 | if point.y > max { 135 | max = point.y; 136 | } 137 | } 138 | Some(AxisRange::new(min, max)) 139 | } 140 | fn render_axes(&mut self, cx: &mut AxesContext) { 141 | let mut line = Line::new().width(self.width).color(self.color); 142 | for point in self.points.iter().cloned() { 143 | let point = cx.transform_point(point); 144 | line.add_point(point.into()); 145 | } 146 | let pixel_bounds = cx.pixel_bounds.into_bounds(); 147 | let (window, cx) = cx.cx.as_mut().unwrap(); 148 | line.render(window, cx, Some(pixel_bounds)); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/geometry/marker.rs: -------------------------------------------------------------------------------- 1 | use crate::figure::axes::AxesContext; 2 | use crate::geometry::{AxisRange, AxisType, GeometryAxes, GeometryPixels, Point2}; 3 | use gpui::{point, Hsla, Path, PathBuilder, Pixels}; 4 | 5 | #[derive(Debug, Clone, Copy)] 6 | pub enum MarkerShape { 7 | Circle, 8 | Square, 9 | TriangleUp, 10 | TriangleDown, 11 | } 12 | pub struct Marker { 13 | pub position: Point2, 14 | pub size: Pixels, 15 | pub color: Hsla, 16 | pub shape: MarkerShape, 17 | } 18 | impl Marker { 19 | pub fn new(position: Point2, size: Pixels) -> Self { 20 | Self { 21 | position, 22 | size, 23 | color: gpui::black(), 24 | shape: MarkerShape::Circle, 25 | } 26 | } 27 | pub fn shape(mut self, shape: MarkerShape) -> Self { 28 | self.shape = shape; 29 | self 30 | } 31 | 32 | pub fn color(mut self, color: Hsla) -> Self { 33 | self.color = color; 34 | self 35 | } 36 | pub fn size(mut self, size: Pixels) -> Self { 37 | self.size = size; 38 | self 39 | } 40 | } 41 | impl Marker { 42 | fn get_path(&self) -> Path { 43 | let mut builder = PathBuilder::fill(); 44 | match self.shape { 45 | MarkerShape::Circle => { 46 | for i in 0..16 { 47 | let angle = i as f32 * std::f32::consts::PI / 8.0; 48 | 49 | let x = self.position.x + self.size * angle.cos(); 50 | let y = self.position.y + self.size * angle.sin(); 51 | if i == 0 { 52 | builder.move_to(point(x, y)); 53 | } else { 54 | builder.line_to(point(x, y)); 55 | } 56 | } 57 | builder.close(); 58 | } 59 | MarkerShape::Square => { 60 | builder.move_to(point( 61 | self.position.x - self.size / 2.0, 62 | self.position.y - self.size / 2.0, 63 | )); 64 | builder.line_to(point( 65 | self.position.x + self.size / 2.0, 66 | self.position.y - self.size / 2.0, 67 | )); 68 | builder.line_to(point( 69 | self.position.x + self.size / 2.0, 70 | self.position.y + self.size / 2.0, 71 | )); 72 | builder.line_to(point( 73 | self.position.x - self.size / 2.0, 74 | self.position.y + self.size / 2.0, 75 | )); 76 | builder.close(); 77 | } 78 | MarkerShape::TriangleUp => { 79 | builder.move_to(point(self.position.x, self.position.y)); 80 | builder.line_to(point( 81 | self.position.x + self.size, 82 | self.position.y + self.size, 83 | )); 84 | builder.line_to(point( 85 | self.position.x - self.size, 86 | self.position.y + self.size, 87 | )); 88 | builder.close(); 89 | } 90 | MarkerShape::TriangleDown => { 91 | builder.move_to(point(self.position.x, self.position.y)); 92 | builder.line_to(point( 93 | self.position.x + self.size, 94 | self.position.y - self.size, 95 | )); 96 | builder.line_to(point( 97 | self.position.x - self.size, 98 | self.position.y - self.size, 99 | )); 100 | builder.close(); 101 | } 102 | } 103 | builder.build().unwrap() 104 | } 105 | pub fn render(&self, window: &mut gpui::Window, pixel_bounds: Option>) { 106 | if let Some(bounds) = pixel_bounds { 107 | if !bounds.contains(&self.position.into()) { 108 | return; 109 | } 110 | } 111 | let path = self.get_path(); 112 | window.paint_path(path, self.color); 113 | } 114 | } 115 | impl GeometryPixels for Marker { 116 | fn render_pixels( 117 | &mut self, 118 | bounds: gpui::Bounds, 119 | window: &mut gpui::Window, 120 | _cx: &mut gpui::App, 121 | ) { 122 | self.render(window, Some(bounds)); 123 | } 124 | } 125 | impl GeometryAxes for Marker { 126 | type X = X; 127 | type Y = Y; 128 | fn render_axes(&mut self, cx: &mut AxesContext) { 129 | let pixel_bounds = cx.pixel_bounds.into_bounds(); 130 | let position = cx.transform_point(self.position); 131 | 132 | let mut marker = Marker::new(position.into(), self.size) 133 | .color(self.color) 134 | .shape(self.shape); 135 | let (window, cx) = cx.cx.as_mut().unwrap(); 136 | 137 | marker.render_pixels(pixel_bounds, window, cx); 138 | } 139 | } 140 | 141 | pub struct Markers { 142 | pub markers: Vec>, 143 | } 144 | impl Markers { 145 | pub fn new() -> Self { 146 | Self { markers: vec![] } 147 | } 148 | pub fn add_marker(&mut self, marker: Marker) { 149 | self.markers.push(marker); 150 | } 151 | 152 | pub fn add_markers(&mut self, markers: Vec>) { 153 | self.markers.extend(markers); 154 | } 155 | } 156 | impl GeometryAxes for Markers { 157 | type X = X; 158 | type Y = Y; 159 | fn get_x_range(&self) -> Option> { 160 | if self.markers.is_empty() { 161 | return None; 162 | } 163 | let mut min = self.markers[0].position.x; 164 | let mut max = self.markers[0].position.x; 165 | for marker in self.markers.iter() { 166 | if marker.position.x < min { 167 | min = marker.position.x; 168 | } 169 | if marker.position.x > max { 170 | max = marker.position.x; 171 | } 172 | } 173 | Some(AxisRange::new(min, max)) 174 | } 175 | fn get_y_range(&self) -> Option> { 176 | if self.markers.is_empty() { 177 | return None; 178 | } 179 | let mut min = self.markers[0].position.y; 180 | let mut max = self.markers[0].position.y; 181 | for marker in self.markers.iter() { 182 | if marker.position.y < min { 183 | min = marker.position.y; 184 | } 185 | if marker.position.y > max { 186 | max = marker.position.y; 187 | } 188 | } 189 | Some(AxisRange::new(min, max)) 190 | } 191 | fn render_axes(&mut self, cx: &mut AxesContext) { 192 | for marker in self.markers.iter_mut() { 193 | marker.render_axes(cx); 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/geometry/mod.rs: -------------------------------------------------------------------------------- 1 | //! Useful geometric structures and functions used inside canvas 2 | 3 | use gpui::{App, Bounds, Pixels, Window}; 4 | use std::marker::PhantomData; 5 | 6 | mod axis; 7 | mod line; 8 | mod marker; 9 | mod point; 10 | mod size; 11 | mod text; 12 | 13 | use crate::figure::axes::AxesContext; 14 | use crate::figure::SharedModel; 15 | pub use axis::*; 16 | pub use line::*; 17 | pub use marker::*; 18 | pub use point::*; 19 | pub use size::*; 20 | pub use text::*; 21 | 22 | /// Low-level Geometry 23 | pub trait GeometryPixels { 24 | fn render_pixels(&mut self, bounds: Bounds, window: &mut Window, cx: &mut App); 25 | } 26 | 27 | /// High-level Geometry 28 | pub trait GeometryAxes: Send + Sync { 29 | type X: AxisType; 30 | type Y: AxisType; 31 | fn get_x_range(&self) -> Option> { 32 | None 33 | } 34 | fn get_y_range(&self) -> Option> { 35 | None 36 | } 37 | fn render_axes(&mut self, cx: &mut AxesContext); 38 | } 39 | impl GeometryAxes for SharedModel { 40 | type X = T::X; 41 | type Y = T::Y; 42 | fn get_x_range(&self) -> Option> { 43 | self.read().get_x_range() 44 | } 45 | fn get_y_range(&self) -> Option> { 46 | self.read().get_y_range() 47 | } 48 | fn render_axes(&mut self, cx: &mut AxesContext) { 49 | self.write().render_axes(cx); 50 | } 51 | } 52 | pub struct GeometryAxesFn) + Send + Sync> 53 | { 54 | f: F, 55 | x_range: Option>, 56 | y_range: Option>, 57 | _phantom: PhantomData<(X, Y)>, 58 | } 59 | impl) + Send + Sync> 60 | GeometryAxesFn 61 | { 62 | pub fn new(f: F) -> Self { 63 | Self { 64 | f, 65 | x_range: None, 66 | y_range: None, 67 | _phantom: PhantomData, 68 | } 69 | } 70 | pub fn with_x_range(mut self, x_range: AxisRange) -> Self { 71 | self.x_range = Some(x_range); 72 | self 73 | } 74 | pub fn with_y_range(mut self, y_range: AxisRange) -> Self { 75 | self.y_range = Some(y_range); 76 | self 77 | } 78 | } 79 | impl) + Send + Sync> GeometryAxes 80 | for GeometryAxesFn 81 | { 82 | type X = X; 83 | type Y = Y; 84 | fn get_x_range(&self) -> Option> { 85 | self.x_range 86 | } 87 | fn get_y_range(&self) -> Option> { 88 | self.y_range 89 | } 90 | fn render_axes(&mut self, cx: &mut AxesContext) { 91 | (self.f)(cx); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/geometry/point.rs: -------------------------------------------------------------------------------- 1 | use crate::geometry::{AxisType, Size2}; 2 | use gpui::Point; 3 | use std::ops::{Add, Sub}; 4 | 5 | #[derive(Debug, Clone, Copy, PartialEq)] 6 | pub struct Point2 { 7 | pub x: X, 8 | pub y: Y, 9 | } 10 | impl Point2 { 11 | pub fn new(x: X, y: Y) -> Self { 12 | Self { x, y } 13 | } 14 | } 15 | impl Point2 { 16 | pub fn flip(self) -> Self { 17 | Self { 18 | x: self.y, 19 | y: self.x, 20 | } 21 | } 22 | } 23 | impl From> for (T, T) { 24 | fn from(point: Point2) -> Self { 25 | (point.x, point.y) 26 | } 27 | } 28 | impl From<(T, T)> for Point2 { 29 | fn from((x, y): (T, T)) -> Self { 30 | Self { x, y } 31 | } 32 | } 33 | impl From> for Point2 { 34 | fn from(point: Point) -> Self { 35 | Self { 36 | x: point.x, 37 | y: point.y, 38 | } 39 | } 40 | } 41 | impl From> for Point { 42 | fn from(point: Point2) -> Self { 43 | Self { 44 | x: point.x, 45 | y: point.y, 46 | } 47 | } 48 | } 49 | impl Add> for Point2 { 50 | type Output = Self; 51 | fn add(self, rhs: Size2) -> Self::Output { 52 | Self { 53 | x: self.x + rhs.width, 54 | y: self.y + rhs.height, 55 | } 56 | } 57 | } 58 | impl Sub> for Point2 { 59 | type Output = Self; 60 | fn sub(self, rhs: Size2) -> Self::Output { 61 | Self { 62 | x: self.x - rhs.width, 63 | y: self.y - rhs.height, 64 | } 65 | } 66 | } 67 | impl Sub for Point2 { 68 | type Output = Size2; 69 | fn sub(self, rhs: Self) -> Self::Output { 70 | Size2 { 71 | width: self.x - rhs.x, 72 | height: self.y - rhs.y, 73 | } 74 | } 75 | } 76 | 77 | pub fn point2(x: X, y: Y) -> Point2 { 78 | Point2 { x, y } 79 | } 80 | -------------------------------------------------------------------------------- /src/geometry/size.rs: -------------------------------------------------------------------------------- 1 | use crate::geometry::AxisType; 2 | use gpui::{size, Size}; 3 | use std::ops::Mul; 4 | 5 | #[derive(Debug, Clone, Copy, PartialEq)] 6 | pub struct Size2 { 7 | pub width: X, 8 | pub height: Y, 9 | } 10 | 11 | impl Size2 { 12 | pub fn new(width: X, height: Y) -> Self { 13 | Self { width, height } 14 | } 15 | pub fn to_f64(self) -> Size { 16 | size(self.width.to_f64(), self.height.to_f64()) 17 | } 18 | } 19 | 20 | impl, Y: Mul> Mul for Size2 { 21 | type Output = Size2; 22 | fn mul(self, rhs: f32) -> Self::Output { 23 | Self::Output { 24 | width: self.width * rhs, 25 | height: self.height * rhs, 26 | } 27 | } 28 | } 29 | 30 | pub fn size2(width: X, height: Y) -> Size2 { 31 | Size2 { width, height } 32 | } 33 | -------------------------------------------------------------------------------- /src/geometry/text.rs: -------------------------------------------------------------------------------- 1 | use crate::geometry::{GeometryPixels, Point2}; 2 | use gpui::{App, Bounds, Pixels, SharedString, Window}; 3 | 4 | pub struct Text { 5 | pub origin: Point2, 6 | pub size: Pixels, 7 | pub text: String, 8 | } 9 | impl Text { 10 | pub fn render( 11 | &mut self, 12 | window: &mut Window, 13 | cx: &mut App, 14 | pixel_bounds: Option>, 15 | ) { 16 | if let Some(bounds) = pixel_bounds { 17 | if !bounds.contains(&self.origin.into()) { 18 | return; 19 | } 20 | } 21 | let shared_string = SharedString::from(self.text.clone()); 22 | let shaped_line = window 23 | .text_system() 24 | .shape_line(shared_string, self.size, &[]) 25 | .unwrap(); 26 | shaped_line 27 | .paint(self.origin.into(), self.size, window, cx) 28 | .unwrap(); 29 | } 30 | } 31 | 32 | impl GeometryPixels for Text { 33 | fn render_pixels(&mut self, bounds: Bounds, window: &mut Window, cx: &mut App) { 34 | self.render(window, cx, Some(bounds)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(gen_blocks)] 2 | #![feature(decl_macro)] 3 | extern crate core; 4 | 5 | pub mod figure; 6 | pub mod fps; 7 | pub mod geometry; 8 | pub(crate) mod utils; 9 | -------------------------------------------------------------------------------- /src/utils/math.rs: -------------------------------------------------------------------------------- 1 | pub fn display_double_smartly(num: f64) -> String { 2 | let mut formatted = num.to_string(); 3 | if formatted.contains(".") { 4 | while formatted.ends_with("0") { 5 | formatted.pop(); 6 | } 7 | } 8 | if formatted.ends_with(".") { 9 | formatted.pop(); 10 | } 11 | formatted 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod math; 2 | --------------------------------------------------------------------------------