slint icon indicating copy to clipboard operation
slint copied to clipboard

Backend API

Open ogoffart opened this issue 2 years ago • 5 comments

We need a better way to select or customize or write backend.

Current Situation

Currently the backend is responsible for too many thing.

  1. The Native style: it is part of the Qt backend crate, but it is completely orthogonal to what the backend provide. We probably should move it outside of the backend, in its own crate.

  2. The renderer. We have a GL renderer (using femtoVG) in the GL backend, a QPainter based renderer in the Qt backend, and a software renderer (line by line) in the MCU backend.

  3. Font and images loading: That's probably related to the renderer

  4. The event-loop and window provider: The GL backend uses winit, the Qt backend uses QWindow and the Qt eventloop. The MCU backend uses a plain loop{} right now and provide a trait to draw on.

  5. other platform specific stuff: Such as the clipboard and the time abstraction

In order to implement a backend now one must implement the Backend, PlatformWindow, ItemRenderer trait. On the MCU, the board need to implement the Devices trait.

There is a global static Backend instance, which is initialized by the slint crate via the i-slint-backend-select crate, unless initialized by another backend call before (eg, mcu or testing backend)

The i-slint-backend-select will make a runtime choice to which backend initialize: The Qt backend if Qt was found, the GL backend otherwise. It is possible to change the default with an env variable. By default, we always compile the Qt backend, but if Qt is not found by the build.rs, the Qt backend will be dummy. It is possible to remove backend by removing default features.

What we want

  • cargo run for a simple crate using the slint crates with its default feature should "just work" and use the native style if Qt if found.

  • A simple hello world should not have too many lines of initialization. And the initialization should not be too complex to support many platform and board (desktop, wasm, mcu)

  • In order to integrate, the event loop might need to be driven by the application.

  • Ideally, there shouldn't be a 'static backend. The backend could be on the stack

ogoffart avatar Mar 24 '22 19:03 ogoffart

The backend might not provide itself an event loop. (eg, in the MCU case, the board helper would do instead) Or the user might want to use winit directly instead of using our event loop and just use the renderer

Example app using a custom MCU


struct MyPlatform;
impl PlatformAbstraction for MyPlatform {
  fn current_time(&self) -> Instant {
     // ...
  }


  //...

}

slint!{ import { MyWindow } from "myfile.slint"; }

fn run_event_loop() {

  let touch_driver = todo!();
  let display_driver = todo!();

  let mut platform_abstraction = MyPlatform::new(the_devices);
  //let my_window = MyWindow::new_with_window(&platform_abstraction);

  let plarform_window = Rc::new(MyPlatformWindow::new())

  let my_window = MyWindow::new_with_window(platform_window.clone);

  // or is that in another crate
  let mut renderer = slint::SoftwareRenderer(touch_driver);


  loop {
    // run timer event and stuff the application needs.

    if let Some(event) = touch_driver.next() {
       my_window.window().send_mouse_event(event, &platform_abstraction);
      //plarform_window.set_mouse_event(event, &platform_abstraction);
    }

    //let animated = my_window.window().render(&mut platform_abstraction);
    renderer.render(&my_window);
    if !animated {
      wfi();
    }
  }
}


impl slint::SoftwareSurface for display_driver {
  fn start_frame(&mut self) { .. };
  fn with_line_buffer(&mut self, line: usize, region: Region, render_line: FnOnce(&mut [TargetPixel])) { .. }
  fn end_frame(&mut self) { .. }
}

ogoffart avatar Mar 24 '22 20:03 ogoffart

Split the Rendering trait from the Item visiting trait

Currentlly, the ItemRendering knows about the Item. I think it would be better if we made the renderer a kind of canvas, with function such as

trait Renderer {
   fn fill_rect(&mut self, rect: Rect, brush: Brush, radius: f32, border: Option<(f32, Color)>);
   fn draw_image(&mut self, image: Image, source_rect: IntRect, dst_rect: Rect, ...);
   fn draw_path(&mut self, ...);
   fn draw_text(&mut self, text: &str, pos: Point, ...)
   // ...
   fn set_opacity(...)
   fn translate(...)
   fn start_layer(...)
   fn set_clip(...)
}

I wonder if that renderer should hold the state and have save()/restore(), or if the set_translation, set_clip, set_opacity should just set the absolute value and the caller take care of the state.

Then the runtime in our core lib can read the properties of items.

If we make this Renderer public, we can imagine user writing custom items in native code and have them expose a Fn(&mut dyn Renderer) function or something like that. And it is the responsability of the platform window to provide a Renderer, we would have a SoftwareLineRenderer that can be constructed with a line buffer, and a GLRenderer that can be constructed from an opengl context.

ogoffart avatar Apr 01 '22 07:04 ogoffart

I agree, an imperative painter API is rather tempting. The main downside I see is that it makes it harder to map to declarative "output" such as the DOM, and it makes it increases the amount of effort to maintain state between frames that may be non-trivial to compute (path tessellation for example).

tronical avatar Apr 01 '22 07:04 tronical

Can we eat keep the cake and eat it? Maybe keep the current "known item based" rendering and then make that lower to a painter based API?

hunger avatar Apr 01 '22 08:04 hunger

A MVP API for MCU was released with slint 0.3. We still need to make an API that can work with top level windows.

ogoffart avatar Sep 16 '22 11:09 ogoffart

Slint 1.2 had a almost full backend API

ogoffart avatar Sep 17 '23 05:09 ogoffart