Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 82 additions & 54 deletions examples/plotter/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// SPDX-License-Identifier: MIT

use plotters::prelude::*;
use slint::SharedPixelBuffer;

#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;
Expand All @@ -14,57 +13,61 @@ slint::slint! {
export { MainWindow } from "plotter.slint";
}

fn pdf(x: f64, y: f64, a: f64) -> f64 {
const SDX: f64 = 0.1;
const SDY: f64 = 0.1;
let x = x as f64 / 10.0;
let y = y as f64 / 10.0;
a * (-x * x / 2.0 / SDX / SDX - y * y / 2.0 / SDY / SDY).exp()
#[derive(Debug, Clone, Copy, PartialEq)]
struct PlotVariables {
pitch: f32,
yaw: f32,
amplitude: f32,
width: u32,
height: u32,
}

fn render_plot(pitch: f32, yaw: f32, amplitude: f32) -> slint::Image {
let mut pixel_buffer = SharedPixelBuffer::new(640, 480);
let size = (pixel_buffer.width(), pixel_buffer.height());

let backend = BitMapBackend::with_buffer(pixel_buffer.make_mut_bytes(), size);

// Plotters requires TrueType fonts from the file system to draw axis text - we skip that for
// WASM for now.
#[cfg(target_arch = "wasm32")]
let backend = wasm_backend::BackendWithoutText { backend };

let root = backend.into_drawing_area();

root.fill(&WHITE).expect("error filling drawing area");

let mut chart = ChartBuilder::on(&root)
.build_cartesian_3d(-3.0..3.0, 0.0..6.0, -3.0..3.0)
.expect("error building coordinate system");
chart.with_projection(|mut p| {
p.pitch = pitch as f64;
p.yaw = yaw as f64;
p.scale = 0.7;
p.into_matrix() // build the projection matrix
});

chart.configure_axes().draw().expect("error drawing");

chart
.draw_series(
SurfaceSeries::xoz(
(-15..=15).map(|x| x as f64 / 5.0),
(-15..=15).map(|x| x as f64 / 5.0),
|x, y| pdf(x, y, amplitude as f64),
fn render_plot(vars: &PlotVariables) -> slint::Image {
fn probability_density_function(x: f64, y: f64, a: f64) -> f64 {
const SDX: f64 = 0.1;
const SDY: f64 = 0.1;
let x = x / 10.0;
let y = y / 10.0;
a * (-x * x / 2.0 / SDX / SDX - y * y / 2.0 / SDY / SDY).exp()
}

let mut pixel_buffer = slint::SharedPixelBuffer::new(vars.width, vars.height);
{
let size = (pixel_buffer.width(), pixel_buffer.height());
let backend = BitMapBackend::with_buffer(pixel_buffer.make_mut_bytes(), size);

// Plotters requires TrueType fonts from the file system to draw axis text - we skip that for
// WASM for now.
#[cfg(target_arch = "wasm32")]
let backend = wasm_backend::BackendWithoutText { backend };

let root = backend.into_drawing_area();
root.fill(&WHITE).expect("error filling drawing area");

let mut chart = ChartBuilder::on(&root)
.build_cartesian_3d(-3.0..3.0, 0.0..6.0, -3.0..3.0)
.expect("error building chart");
chart.with_projection(|mut p| {
p.pitch = vars.pitch as f64;
p.yaw = vars.yaw as f64;
p.scale = 0.62;
p.into_matrix()
});
chart.configure_axes().draw().expect("error drawing");
chart
.draw_series(
SurfaceSeries::xoz(
(-30..=30).map(|x| x as f64 / 10.0),
(-30..=30).map(|x| x as f64 / 10.0),
|x, y| probability_density_function(x, y, (vars.amplitude as f64 / 1.0) * 6.0),
)
.style_func(&|&v| {
(&HSLColor(240.0 / 360.0 - 240.0 / 360.0 * v / 5.0, 1.0, 0.7)).into()
}),
)
.style_func(&|&v| {
(&HSLColor(240.0 / 360.0 - 240.0 / 360.0 * v / 5.0, 1.0, 0.7)).into()
}),
)
.expect("error drawing series");

root.present().expect("error presenting");
drop(chart);
drop(root);
.expect("error drawing series");
root.present().expect("error presenting");
}

slint::Image::from_rgb8(pixel_buffer)
}
Expand All @@ -76,9 +79,34 @@ pub fn main() {
#[cfg(all(debug_assertions, target_arch = "wasm32"))]
console_error_panic_hook::set_once();

let main_window = MainWindow::new().unwrap();

main_window.on_render_plot(render_plot);

main_window.run().unwrap();
let main_window = MainWindow::new().expect("Cannot create main window");
let main_window_weak = main_window.as_weak();

let mut plot_variables =
PlotVariables { pitch: -1.0, yaw: -1.0, amplitude: -1.0, width: 0, height: 0 };

main_window
.window()
.set_rendering_notifier(move |state, _| {
if let slint::RenderingState::BeforeRendering = state {
if let Some(main_window) = main_window_weak.upgrade() {
let current_plot_variables = PlotVariables {
pitch: main_window.get_pitch(),
yaw: main_window.get_yaw(),
amplitude: main_window.get_amplitude(),
width: main_window.get_texture_width() as u32,
height: main_window.get_texture_height() as u32,
};

if current_plot_variables != plot_variables {
plot_variables = current_plot_variables;
main_window.set_texture(render_plot(&current_plot_variables));
}
main_window.window().request_redraw();
}
}
})
.expect("Unable to set rendering notifier");

main_window.run().expect("Failed to run main window");
}
74 changes: 38 additions & 36 deletions examples/plotter/plotter.slint
Original file line number Diff line number Diff line change
Expand Up @@ -4,55 +4,57 @@
import { Slider, GroupBox, HorizontalBox, VerticalBox } from "std-widgets.slint";

export component MainWindow inherits Window {
in-out property <float> pitch: 0.15;
in-out property <float> yaw: 0.5;

pure callback render_plot(/* pitch */ float, /* yaw */ float, /* amplitude */ float) -> image;

in property <image> texture <=> image.source;
out property <int> texture-width: image.width / 1phx;
out property <int> texture-height: image.height / 1phx;
out property <float> amplitude <=> amplitude.value;
out property <float> pitch: 0.15;
out property <float> yaw: 0.5;
title: "Slint Plotter Integration Example";
preferred-width: 800px;
preferred-height: 600px;

preferred-width: 999px;
preferred-height: 720px;
VerticalBox {
Text {
font-size: 20px;
text: "2D Gaussian PDF";
text: "2D Gaussian Probability Density Function";
horizontal-alignment: center;
}

Image {
source: root.render_plot(root.pitch, root.yaw, amplitude-slider.value / 10);
touch := TouchArea {
property <float> pressed-pitch;
property <float> pressed-yaw;

pointer-event(event) => {
if (event.button == PointerEventButton.left && event.kind == PointerEventKind.down) {
self.pressed-pitch = root.pitch;
self.pressed-yaw = root.yaw;
HorizontalBox {
image := Image {
touch := TouchArea {
property <float> starting-drag-pitch;
property <float> starting-drag-yaw;
pointer-event(event) => {
if (event.kind == PointerEventKind.down && event.button == PointerEventButton.left) {
self.starting-drag-pitch = root.pitch;
self.starting-drag-yaw = root.yaw;
}
}
}
moved => {
if (self.enabled && self.pressed) {
root.pitch = self.pressed-pitch + (touch.mouse-y - touch.pressed-y) / self.height * 3.14;
root.yaw = self.pressed-yaw - (touch.mouse-x - touch.pressed-x) / self.width * 3.14;
moved => {
if (self.enabled && self.pressed) {
root.pitch = self.starting-drag-pitch + (touch.mouse-y - touch.pressed-y) / self.height * 3.14;
root.yaw = self.starting-drag-yaw - (touch.mouse-x - touch.pressed-x) / self.width * 3.14;
}
}
mouse-cursor: self.pressed ? MouseCursor.grabbing : MouseCursor.grab;
}
mouse-cursor: self.pressed ? MouseCursor.grabbing : MouseCursor.grab;
}
}

HorizontalBox {
Text {
text: "Amplitude:";
font-weight: 600;
vertical-alignment: center;
}
VerticalBox {
amplitude := Slider {
orientation: Orientation.vertical;
transform-rotation: -180deg;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This rotation shouldn't be needed. Instead the we should fix the sliderto be in the right direction.
(I remember some discussion about that in #4705 (comment) )

Copy link
Contributor Author

@nanopink nanopink Oct 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, technically it should be orientation up, down, right or left, or maybe just vertical / horizontal but where min and max are replaced by start and end and it can start at 10 and end at zero
Although ideally in the future a slider could have 2 ends to select a range, this is something else to consider

minimum: 0;
maximum: 1;
value: 0.5;
}

amplitude-slider := Slider {
minimum: 0;
maximum: 100;
value: 50;
Text {
text: "Amplitude";
font-weight: 600;
vertical-alignment: center;
}
}
}
}
Expand Down
1 change: 1 addition & 0 deletions rustfmt.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@

edition = "2021"
use_small_heuristics = "Max"
max_width = 100
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that not the default? why do you change this?

Copy link
Contributor Author

@nanopink nanopink Oct 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the default which you use, but this specification wasn't there so my formatter used the wrong format
It'll prevent CI reformatting for future contributors who do not have this option in their vscode settings

Loading