Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 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
153 changes: 95 additions & 58 deletions examples/plotter/main.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
// Copyright © SixtyFPS GmbH <[email protected]>
// SPDX-License-Identifier: MIT

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

#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;

Expand All @@ -14,57 +11,57 @@ 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()
}

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(pitch: f32, yaw: f32, amplitude: f32, width: u32, height: u32) -> 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()
}

use plotters::prelude::*;
let mut pixel_buffer = slint::SharedPixelBuffer::new(width, height);
{
let size = (pixel_buffer.width(), pixel_buffer.height());
let root =
BitMapBackend::with_buffer(pixel_buffer.make_mut_bytes(), size).into_drawing_area();
root.fill(&plotters::style::RGBColor(28, 28, 28)).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()
});

let gray = &plotters::style::RGBColor(46, 46, 46);
chart
.configure_axes()
.label_style(("sans-serif", 12).into_font().color(&WHITE))
.light_grid_style(gray)
.bold_grid_style(gray)
.max_light_lines(4)
.draw()
.expect("error drawing");
let precision = 30;
chart
.draw_series(
SurfaceSeries::xoz(
(-precision..=precision).map(|x| x as f64 / (precision as f64 / 3.0)),
(-precision..=precision).map(|x| x as f64 / (precision as f64 / 3.0)),
|x, y| probability_density_function(x, y, (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 +73,49 @@ 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 current_amplitude = -1.0f32;
let mut current_pitch = -1.0f32;
let mut current_yaw = -1.0f32;
let mut current_width = 0u32;
let mut current_height = 0u32;

main_window
.window()
.set_rendering_notifier(move |state, _graphics_api| {
if let slint::RenderingState::BeforeRendering = state {
if let Some(main_window_strong) = main_window_weak.upgrade() {
let new_pitch = main_window_strong.get_pitch();
let new_yaw = main_window_strong.get_yaw();
let new_amplitude = main_window_strong.get_amplitude();
let new_width = main_window_strong.get_texture_width() as u32;
let new_height = main_window_strong.get_texture_height() as u32;
if current_pitch != new_pitch
|| current_yaw != new_yaw
|| current_amplitude != new_amplitude
|| current_width != new_width
|| current_height != new_height
{
current_pitch = new_pitch;
current_yaw = new_yaw;
current_amplitude = new_amplitude;
current_width = new_width;
current_height = new_height;
main_window_strong.set_texture(render_plot(
new_pitch,
new_yaw,
new_amplitude,
new_width,
new_height,
));
}
main_window_strong.window().request_redraw();
Copy link
Member

Choose a reason for hiding this comment

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

Does that mean that you will force a refresh all the time even if nothing changed?

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.

Yes, no choice there is no resize event in slint and using the texture dimensions requires this
Because Slint also uses the request_redraw so we can't only re-render when we need to because its constantly called anyway by for example the slider thumb animation, and other factors

Even if you were to add for example a callback on width and height change on image it would spam it because the resizing is not instantaneous

As seen in this earlier issue I recorded, the slider spams render window, because the render loop given is tied to the UI rendering, so they both end up inefficient, its a design flaw

Note that on my main project I stopped using Slint for this exact specific issue ironically

On this video we do not request a redraw every frame so the texture is not automatically updated
And as you can see the slider's position change enforce redraw calls multiple times, rather than compute its final position before rendering, this is likely tied with an issue with Rectangle where when an image is inserted inside a rectangle, it affects the parent rectangle's size

rec.mp4

}
}
})
.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