Skip to content

Instantly share code, notes, and snippets.

@shock
Last active November 9, 2024 00:01
Show Gist options
  • Save shock/67bcb28fbff5f6a6a5eda0add4311efc to your computer and use it in GitHub Desktop.
Save shock/67bcb28fbff5f6a6a5eda0add4311efc to your computer and use it in GitHub Desktop.
A Slint / Rust demonstration showing some surprising (to me) behavior
use std::{cell::RefCell, rc::Rc, time::Duration};
use slint::{Image, Rgba8Pixel, SharedPixelBuffer, Timer, TimerMode, SharedString};
slint::slint! {
import { LineEdit, HorizontalBox, Slider, VerticalBox, CheckBox, SpinBox, TextEdit } from "std-widgets.slint";
export global Properties { // trying separate global structs just in case:
in-out property <int> counter: 0; // https://github.com/slint-ui/slint/discussions/3948
in-out property <float> red: 0.65;
}
export global Backend {
pure callback build_image(length, length) -> image;
in-out property <bool> checked;
in-out property <string> some-text; // this causes an image redraw for test case: shared-text
in-out property <float> cursor_pos: 0.0; // this does not cause an image redraw - updated by backend only
}
enum TestType { Text, TextInput, LineEdit, TextEdit, shared-text }
export component MainWindow inherits Window {
title: "redraw";
background: #222222;
preferred_width: 500px;
preferred_height: 400px;
// NOTE: TextEdit is the only component that behaves like I expect. I expect the others to behave like it.
property <TestType> test_case:
// Uncomment one of the following to test different components:
TestType.TextEdit; // does not cause an image redraw, even when global property is involved. hooray.
// TestType.Text; // causes an image redraw when a global property is involved
// TestType.TextInput; // causes an image redraw even when NO property is involved
// TestType.LineEdit; // causes an image redraw even when property is NOT involved - edit to see
// TestType.shared-text; // causes an image redraw any time the backend modifies the text
VerticalBox {
HorizontalBox {
max-height: 40px;
// TextEdit is the only "Text" component that doesn't cause a redraw when the
// counter property changes or when user modifies the text directly.
if (test_case == TestType.TextEdit) : TextEdit {
text: "C: " + Properties.counter;
min-width: 100px; min-height: 30px;
}
// TextInput causes a redraw when text is changed whether or not a property is involved.
if (test_case == TestType.TextInput) : TextInput {
text: "click me and edit";
min-width: 100px; min-height: 30px;
vertical-alignment: center;
}
// Text causes a redraw when the counter property changes.
if (test_case == TestType.Text) : Text {
text: "C: " + Properties.counter;
min-width: 100px; min-height: 30px;
vertical-alignment: center;
}
// LineEdit causes a redraw with or without the property change.
if (test_case == TestType.LineEdit) : LineEdit {
min-width: 100px; min-height: 30px;
text: "C: " + Properties.counter;
}
// Text causes a redraw every time the backend modifies the text.
if (test_case == TestType.shared-text) : Text {
text: Backend.some-text;
vertical-alignment: center;
width: 100px;
}
// SpinBox does not cause a redraw whether the user or the backend modifies the value.
SpinBox {
minimum: 0;
value <=> Properties.counter;
accessible-role: AccessibleRole.tab-list;
maximum: 10000;
}
Slider {
width: 100%;
minimum: 0.0;
maximum: 120.0;
value: Properties.counter; // one-way does not cause a redraw
}
}
HorizontalBox {
Text { text: "Red"; vertical-alignment: center; width: 30px; }
Slider {
min-width: 100px;
minimum: 0.0;
maximum: 1.0;
value <=> Properties.red; // two-way binding causes a redraw when the slider is moved.
// one-way binding does not.
}
Text { text: "Checked"; vertical-alignment: center; width: 50px; }
CheckBox {
checked <=> Backend.checked; // this causes two redraws when checked, but just one when unchecked.
// I don't expect it to cause any.
}
}
Rectangle {
pure function build_image(width: length, height: length) -> image {
debug("build image: " + width/1px + "x" + height/1px);
return Backend.build_image(width, height);
}
ri := Rectangle{
preferred-width: 400px;
preferred-height: 300px;
min-width: 0px; min-height: 0px;
// In my real app, the image is very expensive to render, so I need to only render it
// when it has changed. I've reduced my code to the simplest example that demonstrates
// my problem. My actual code is more elaborate in how it manages state and when
// to manually trigger an image redraw based on all the factors.
// In this simplest example I use to illustrate, the image source is a pure backend
// callback and only depends on the width and height of the image, so it should only be
// called when the size changes, right? Indeed, it does get called when the size changes,
// but it *also* gets called when the Text, TextInput, and LineEdit components are changed.
// Why it gets triggered by the Text, TextInput, and LineEdit components, but
// not the SpinBox or TextEdit is the mystery.
// It also gets triggered by the Properties.red slider change, but only if it has a
// two-way binding. Same with the Backend.checked and the checkbox. I don't understand
// that either.
Image {
x: 0; y: 0;
width: 100%; height: 100%;
source: build_image(self.width, self.height);
}
Rectangle { // the would-be playback cursor for my audio application, this works. Updating its
// position Backend.cursor_pos moves the curosor without triggering an image redraw.
// That's the way I expect all property changes affect image redraws, unless
// I manually wire them up to to force a redraw.
x: Backend.cursor_pos * ri.width;
width: 10px;
height: ri.height;
background: #ff0000aa;
}
}
Text { text: "RESIZING THE WINDOW\nREDRAWS THE IMAGE\nAS DESIRED"; vertical-alignment: center; horizontal-alignment: center; }
}
Slider {
width: 100%;
minimum: 0.0; maximum: 1.0;
value: Backend.cursor_pos;
changed => { Backend.cursor_pos = self.value; }
}
}
}
}
struct AppState {
image_calls: u32,
inverted: bool,
}
fn build_resizable_image(width: u32, height: u32, red: u8, mut green: u8, mut blue: u8, inverted: bool) -> Image {
let mut pixel_buffer = SharedPixelBuffer::<Rgba8Pixel>::new(width, height);
let bytes = pixel_buffer.make_mut_bytes();
if inverted { green = 255 - green; blue = 255 - blue; }
for x in 0..width {
for y in 0..height {
let sx = x as f32 / 50.0; let sy = y as f32 / 50.0;
let checkered = ((sx % 2.0).floor() + (sy % 2.0).floor()) % 2.0;
let index = (y * width * 4 + x * 4) as usize;
if checkered == 0.0 {
bytes[index] = red; bytes[index + 1] = blue; bytes[index + 2] = green; bytes[index + 3] = 255;
} else {
bytes[index] = 0; bytes[index + 1] = 0; bytes[index + 2] = 0; bytes[index + 3] = 255;
};
}
}
Image::from_rgba8(pixel_buffer)
}
fn main() {
let app = MainWindow::new().unwrap();
let backend = app.global::<Backend>();
let app_state = Rc::new(RefCell::new(AppState {
image_calls: 0,
inverted: false,
}));
backend.on_build_image({
let app_weak = app.as_weak();
let app_state = Rc::clone(&app_state);
move |width, height| {
let app = app_weak.unwrap();
let width = width as u32;
let height = height as u32;
let mut app_state = app_state.borrow_mut();
app_state.image_calls += 1;
let image_calls = app_state.image_calls;
println!("\nimage_calls: {}", image_calls);
println!("width: {}, height: {}", width, height);
let props = app.global::<Properties>();
let red = (props.get_red() * 255.0) as u8;
app_state.inverted = !app_state.inverted;
build_resizable_image(width, height, red, 180, 195, app_state.inverted)
}
});
let timer = Timer::default();
let app_weak = app.as_weak();
// create events 60 times a second to trigger property changes
let mut frame: i32 = 0;
let interval = Duration::from_millis(1000 / 60);
timer.start(TimerMode::Repeated, interval, {
let app = app_weak.upgrade().unwrap();
move || {
if frame == 60 {
let counter = app.global::<Properties>().get_counter() + 1;
frame = 0;
app.global::<Properties>().set_counter(counter);
app.global::<Backend>().set_some_text(SharedString::from(format!("{}", counter)));
}
app.global::<Backend>().set_cursor_pos(frame as f32 / 60.0);
frame += 1;
}
});
app.run().unwrap();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment