Skip to main content

ferroui_linuxframebuffer/
linux_framebuffer_platform.rs

1//! The platform, its lifetime and the functions that start an application
2//! on it (the port of `LinuxFramebufferPlatform.cs`).
3
4use crate::epoll_dispatcher_impl::EpollDispatcherImpl;
5use crate::framebuffer_toplevel_impl::FramebufferToplevelImpl;
6use crate::input::ev_dev::EvDevBackend;
7use crate::input::IInputBackend;
8use crate::linux_framebuffer_platform_options::LinuxFramebufferPlatformOptions;
9use crate::output::{DrmOutput, FbDevOutputOptions, FbdevOutput, IOutputBackend};
10use crate::DrmOutputOptions;
11use crate::raw_event_grouping::{
12    IRawEventGrouperDispatchQueue, ManualRawEventGrouperDispatchQueue,
13    ManualRawEventGrouperDispatchQueueDispatcherInputProvider,
14};
15use crate::stubs::CursorFactoryStub;
16use ferroui_base::input::platform::{KeyGestureFormatInfo, PlatformHotkeyConfiguration};
17use ferroui_base::input::{IKeyboardDevice, InputElement, KeyModifiers, KeyboardDevice};
18use ferroui_base::platform::{
19    DefaultPlatformSettings, ICursorFactory, IPlatformGraphics, IPlatformSettings, PixelFormat, SharedBitmapImpl,
20};
21use ferroui_base::reactive::{Disposable, IDisposable};
22use ferroui_base::rendering::composition::Compositor;
23use ferroui_base::rendering::{IRenderLoop, IRenderTimer, RenderLoop, SleepLoopRenderTimer, UiThreadRenderTimer};
24use ferroui_base::threading::{CancellationToken, CancellationTokenSource, Dispatcher};
25use ferroui_base::utilities::HandlerList;
26use ferroui_base::{FerroLocator, LocatorExtensions, Ref};
27use ferroui_controls::application_lifetimes::{
28    ControlledApplicationLifetimeExitEventArgs, ControlledApplicationLifetimeStartupEventArgs, IApplicationLifetime,
29    IControlledApplicationLifetime, ISingleTopLevelApplicationLifetime, ISingleViewApplicationLifetime,
30};
31use ferroui_controls::embedding::EmbeddableControlRoot;
32use ferroui_controls::platform::{IPlatformIconLoader, ITopLevelImpl, IWindowIconImpl};
33use ferroui_controls::{AppBuilder, Control, TopLevel};
34use std::any::Any;
35use std::cell::{Cell, RefCell};
36use std::collections::HashMap;
37use std::io;
38use std::rc::{Rc, Weak};
39use std::sync::Arc;
40
41/// The icon loader of a platform without window icons.
42#[derive(Default)]
43pub(crate) struct LinuxFramebufferIconLoaderStub;
44
45struct IconStub;
46
47impl IWindowIconImpl for IconStub {
48    fn save(&self, _output_stream: &mut dyn io::Write) -> io::Result<()> {
49        Ok(())
50    }
51}
52
53impl IPlatformIconLoader for LinuxFramebufferIconLoaderStub {
54    fn load_icon_from_file(&self, _file_name: &str) -> io::Result<Rc<dyn IWindowIconImpl>> {
55        Ok(Rc::new(IconStub))
56    }
57
58    fn load_icon_from_stream(&self, _stream: &mut dyn io::Read) -> io::Result<Rc<dyn IWindowIconImpl>> {
59        Ok(Rc::new(IconStub))
60    }
61
62    fn load_icon_from_bitmap(&self, _bitmap: Arc<SharedBitmapImpl>) -> Rc<dyn IWindowIconImpl> {
63        Rc::new(IconStub)
64    }
65}
66
67ferroui_base::ferro_thread_local! {
68    /// `EventGrouperDispatchQueue`: the raw input of the top-level, which
69    /// the dispatcher implementation hands out one event at a time.
70    static EVENT_GROUPER_DISPATCH_QUEUE: Rc<ManualRawEventGrouperDispatchQueue> =
71        Rc::new(ManualRawEventGrouperDispatchQueue::new());
72    /// `Compositor`.
73    static COMPOSITOR: RefCell<Option<Rc<Compositor>>> = const { RefCell::new(None) };
74    /// The keyboard device that is registered with the platform.
75    static KEYBOARD_DEVICE: RefCell<Option<Rc<KeyboardDevice>>> = const { RefCell::new(None) };
76}
77
78pub struct LinuxFramebufferPlatform {
79    fb: Arc<dyn IOutputBackend>,
80}
81
82impl LinuxFramebufferPlatform {
83    fn new(backend: Arc<dyn IOutputBackend>) -> Self {
84        Self { fb: backend }
85    }
86
87    pub fn event_grouper_dispatch_queue() -> Rc<ManualRawEventGrouperDispatchQueue> {
88        EVENT_GROUPER_DISPATCH_QUEUE.with(Rc::clone)
89    }
90
91    /// The compositor of the platform, once the platform is initialised.
92    pub(crate) fn compositor() -> Option<Rc<Compositor>> {
93        COMPOSITOR.with(|compositor| compositor.borrow().clone())
94    }
95
96    /// The keyboard device of the platform, once the platform is
97    /// initialised.
98    pub(crate) fn keyboard_device() -> Option<Rc<KeyboardDevice>> {
99        KEYBOARD_DEVICE.with(|keyboard| keyboard.borrow().clone())
100    }
101
102    fn initialize_platform(&self) {
103        let locator = FerroLocator::current_mutable();
104        let graphics = self.fb.as_gl_output_backend().map(|gl| gl.platform_graphics());
105        if let Some(graphics) = &graphics {
106            locator.bind::<Arc<dyn IPlatformGraphics>>().to_constant(Rc::new(graphics.clone()));
107        }
108
109        let opts = FerroLocator::current()
110            .get_service::<LinuxFramebufferPlatformOptions>()
111            .map_or_else(LinuxFramebufferPlatformOptions::default, |opts| *opts);
112
113        let timer: Arc<dyn IRenderTimer> = if opts.should_render_on_ui_thread {
114            Arc::new(UiThreadRenderTimer::new(opts.fps))
115        } else {
116            // The reference uses its default render timer here, which
117            // ticks on threads of a pool. The compositor of the port is
118            // confined to one render thread, and the default timer starts
119            // a thread each time it is started; the timer that keeps one
120            // thread is used (DEVIATIONS.md).
121            Arc::new(SleepLoopRenderTimer::new(opts.fps))
122        };
123
124        Dispatcher::initialize_ui_thread_dispatcher(EpollDispatcherImpl::new(Box::new(
125            ManualRawEventGrouperDispatchQueueDispatcherInputProvider::new(Self::event_grouper_dispatch_queue()),
126        )));
127
128        let render_loop: Arc<dyn IRenderLoop> = RenderLoop::from_timer(timer);
129        let keyboard_device = KeyboardDevice::new();
130        KEYBOARD_DEVICE.with(|keyboard| *keyboard.borrow_mut() = Some(keyboard_device.clone()));
131        let keyboard_device: Rc<dyn IKeyboardDevice> = keyboard_device;
132        let platform_settings: Rc<dyn IPlatformSettings> = Rc::new(DefaultPlatformSettings::new());
133        locator
134            .bind::<Arc<dyn IRenderLoop>>()
135            .to_constant(Rc::new(render_loop))
136            .bind::<dyn ICursorFactory>()
137            .to_transient::<CursorFactoryStub>(|stub| stub)
138            .bind::<dyn IKeyboardDevice>()
139            .to_constant(keyboard_device)
140            .bind::<dyn IPlatformIconLoader>()
141            .to_singleton::<LinuxFramebufferIconLoaderStub>(|stub| stub)
142            .bind::<dyn IPlatformSettings>()
143            .to_constant(platform_settings)
144            .bind_to_self(Rc::new(PlatformHotkeyConfiguration::new(KeyModifiers::CONTROL)))
145            .bind_to_self(Rc::new(KeyGestureFormatInfo::new(Some(HashMap::new()), "Super", "Ctrl", "Alt", "Shift")));
146
147        let compositor = Compositor::new(graphics, false);
148        COMPOSITOR.with(|slot| *slot.borrow_mut() = Some(compositor));
149    }
150
151    /// `Initialize`: registers the platform with the builder and creates
152    /// the lifetime of its one top-level.
153    ///
154    /// The reference also selects its Skia backend here; the renderer is
155    /// the caller's choice on the builder in this port.
156    pub(crate) fn initialize(
157        builder: &AppBuilder,
158        output_backend: Arc<dyn IOutputBackend>,
159        input_backend: Option<Rc<dyn IInputBackend>>,
160    ) -> Rc<LinuxFramebufferLifetime> {
161        let platform = LinuxFramebufferPlatform::new(output_backend);
162        let fb = platform.fb.clone();
163        builder
164            .use_standard_runtime_platform_subsystem()
165            .use_windowing_subsystem(move || platform.initialize_platform(), "fbdev");
166        LinuxFramebufferLifetime::with_input(fb, input_backend)
167    }
168}
169
170/// The lifetime of an application on the platform: one top-level that
171/// shows the main view.
172pub struct LinuxFramebufferLifetime {
173    this: Weak<LinuxFramebufferLifetime>,
174    fb: Arc<dyn IOutputBackend>,
175    input_backend: Option<Rc<dyn IInputBackend>>,
176    top_level: RefCell<Option<Ref<EmbeddableControlRoot>>>,
177    main_view: RefCell<Option<Ref<Control>>>,
178    cts: CancellationTokenSource,
179    exit_code: Cell<i32>,
180    startup: HandlerList<dyn Fn(&ControlledApplicationLifetimeStartupEventArgs)>,
181    exit: HandlerList<dyn Fn(&ControlledApplicationLifetimeExitEventArgs)>,
182}
183
184impl LinuxFramebufferLifetime {
185    pub fn new(fb: Arc<dyn IOutputBackend>) -> Rc<Self> {
186        Self::with_input(fb, None)
187    }
188
189    pub fn with_input(fb: Arc<dyn IOutputBackend>, input: Option<Rc<dyn IInputBackend>>) -> Rc<Self> {
190        Rc::new_cyclic(|this| Self {
191            this: this.clone(),
192            fb,
193            input_backend: input,
194            top_level: RefCell::new(None),
195            main_view: RefCell::new(None),
196            cts: CancellationTokenSource::new(),
197            exit_code: Cell::new(0),
198            startup: HandlerList::new(),
199            exit: HandlerList::new(),
200        })
201    }
202
203    /// The token that is cancelled when the application shuts down.
204    pub fn token(&self) -> CancellationToken {
205        self.cts.token()
206    }
207
208    pub fn exit_code(&self) -> i32 {
209        self.exit_code.get()
210    }
211
212    pub fn start(&self, args: &[String]) {
213        let e = ControlledApplicationLifetimeStartupEventArgs::new(args.iter().cloned());
214        for (_, handler) in self.startup.snapshot().iter() {
215            handler(&e);
216        }
217    }
218
219    fn subscribe<F: ?Sized + 'static>(
220        &self,
221        select: fn(&Self) -> &HandlerList<F>,
222        handler: Rc<F>,
223    ) -> Rc<dyn IDisposable> {
224        let token = select(self).add(handler);
225        let weak = self.this.clone();
226        Disposable::create(move || {
227            if let Some(this) = weak.upgrade() {
228                select(&this).remove(token);
229            }
230        })
231    }
232
233    /// `EnsureTopLevel`.
234    ///
235    /// # Panics
236    /// Panics when no input backend was given and the one of the
237    /// environment cannot be created.
238    fn ensure_top_level(&self) -> Ref<EmbeddableControlRoot> {
239        if let Some(top_level) = self.top_level.borrow().clone() {
240            return top_level;
241        }
242
243        let input_backend: Rc<dyn IInputBackend> = match &self.input_backend {
244            Some(input_backend) => input_backend.clone(),
245            None => {
246                if std::env::var("FERROUI_USE_EVDEV").is_ok_and(|value| value == "1") {
247                    Rc::new(EvDevBackend::create_from_environment().unwrap_or_else(|error| panic!("{error}")))
248                } else {
249                    Rc::new(crate::input::lib_input::LibInputBackend::new())
250                }
251            }
252        };
253
254        let queue: Rc<dyn IRawEventGrouperDispatchQueue> = LinuxFramebufferPlatform::event_grouper_dispatch_queue();
255        let platform_impl: Rc<dyn ITopLevelImpl> = FramebufferToplevelImpl::new(
256            self.fb.clone(),
257            input_backend,
258            Some(queue),
259            LinuxFramebufferPlatform::compositor(),
260        );
261        let tl = EmbeddableControlRoot::with_impl(platform_impl);
262        tl.prepare();
263        tl.start_rendering();
264        *self.top_level.borrow_mut() = Some(tl.clone());
265
266        let scope: Ref<InputElement> = tl.clone().upcast();
267        tl.focus_manager().set_focus_scope(&scope);
268        tl
269    }
270}
271
272impl IApplicationLifetime for LinuxFramebufferLifetime {
273    fn as_any(&self) -> &dyn Any {
274        self
275    }
276
277    fn as_controlled_application_lifetime(&self) -> Option<&dyn IControlledApplicationLifetime> {
278        Some(self)
279    }
280
281    fn as_single_view_application_lifetime(&self) -> Option<&dyn ISingleViewApplicationLifetime> {
282        Some(self)
283    }
284
285    fn as_single_top_level_application_lifetime(&self) -> Option<&dyn ISingleTopLevelApplicationLifetime> {
286        Some(self)
287    }
288}
289
290impl IControlledApplicationLifetime for LinuxFramebufferLifetime {
291    fn startup(&self, handler: Rc<dyn Fn(&ControlledApplicationLifetimeStartupEventArgs)>) -> Rc<dyn IDisposable> {
292        self.subscribe(|this| &this.startup, handler)
293    }
294
295    fn exit(&self, handler: Rc<dyn Fn(&ControlledApplicationLifetimeExitEventArgs)>) -> Rc<dyn IDisposable> {
296        self.subscribe(|this| &this.exit, handler)
297    }
298
299    fn shutdown(&self, exit_code: i32) {
300        self.exit_code.set(exit_code);
301        let e = ControlledApplicationLifetimeExitEventArgs::new(exit_code);
302        for (_, handler) in self.exit.snapshot().iter() {
303            handler(&e);
304        }
305        self.exit_code.set(e.application_exit_code());
306        self.cts.cancel();
307    }
308}
309
310impl ISingleViewApplicationLifetime for LinuxFramebufferLifetime {
311    fn main_view(&self) -> Option<Ref<Control>> {
312        self.main_view.borrow().clone()
313    }
314
315    fn set_main_view(&self, value: Option<Ref<Control>>) {
316        let top_level = self.ensure_top_level();
317        *self.main_view.borrow_mut() = value.clone();
318        top_level.set_content(value.map(Control::boxed));
319    }
320}
321
322impl ISingleTopLevelApplicationLifetime for LinuxFramebufferLifetime {
323    fn top_level(&self) -> Option<Ref<TopLevel>> {
324        Some(self.ensure_top_level().upcast())
325    }
326}
327
328/// The functions that start an application on the platform.
329pub trait LinuxFramebufferPlatformExtensions {
330    /// Starts the application on a framebuffer device (`/dev/fb0`, or the
331    /// one the environment variable `FRAMEBUFFER` names, when `fbdev` is
332    /// not given) and runs it until it shuts down. Returns the exit code.
333    fn start_linux_fb_dev(
334        &self,
335        args: &[String],
336        fbdev: Option<&str>,
337        scaling: f64,
338        input_backend: Option<Rc<dyn IInputBackend>>,
339    ) -> io::Result<i32>;
340
341    /// As [`start_linux_fb_dev`](Self::start_linux_fb_dev), asking the
342    /// device for a pixel format.
343    fn start_linux_fb_dev_with_format(
344        &self,
345        args: &[String],
346        fbdev: &str,
347        format: Option<PixelFormat>,
348        scaling: f64,
349        input_backend: Option<Rc<dyn IInputBackend>>,
350    ) -> io::Result<i32>;
351
352    /// As [`start_linux_fb_dev`](Self::start_linux_fb_dev), with the
353    /// options of the output.
354    fn start_linux_fb_dev_with_options(
355        &self,
356        args: &[String],
357        options: FbDevOutputOptions,
358        input_backend: Option<Rc<dyn IInputBackend>>,
359    ) -> io::Result<i32>;
360
361    /// Starts the application on a KMS output: the first connected
362    /// connector of the card `card` (or of the first usable card of
363    /// `/dev/dri`) in its preferred mode, and runs it until it shuts down.
364    /// Returns the exit code.
365    fn start_linux_drm(
366        &self,
367        args: &[String],
368        card: Option<&str>,
369        scaling: f64,
370        input_backend: Option<Rc<dyn IInputBackend>>,
371    ) -> io::Result<i32>;
372
373    /// As [`start_linux_drm`](Self::start_linux_drm), probing the
374    /// connectors when asked to and with the options of the output.
375    fn start_linux_drm_with_options(
376        &self,
377        args: &[String],
378        card: Option<&str>,
379        connectors_force_probe: bool,
380        options: Option<DrmOutputOptions>,
381        input_backend: Option<Rc<dyn IInputBackend>>,
382    ) -> io::Result<i32>;
383
384    /// Starts the application on an output that is already created and
385    /// runs it until it shuts down. Returns the exit code.
386    fn start_linux_direct(
387        &self,
388        args: &[String],
389        output_backend: Arc<dyn IOutputBackend>,
390        input_backend: Option<Rc<dyn IInputBackend>>,
391    ) -> i32;
392
393    /// Registers the platform with the builder and returns the lifetime
394    /// of its top-level, without setting the application up or running
395    /// it. An addition: the reference keeps this step internal
396    /// (`LinuxFramebufferPlatform.Initialize`).
397    fn use_linux_framebuffer(
398        &self,
399        output_backend: Arc<dyn IOutputBackend>,
400        input_backend: Option<Rc<dyn IInputBackend>>,
401    ) -> Rc<LinuxFramebufferLifetime>;
402}
403
404impl LinuxFramebufferPlatformExtensions for AppBuilder {
405    fn start_linux_fb_dev(
406        &self,
407        args: &[String],
408        fbdev: Option<&str>,
409        scaling: f64,
410        input_backend: Option<Rc<dyn IInputBackend>>,
411    ) -> io::Result<i32> {
412        let output = FbdevOutput::with_format(fbdev, None)?;
413        output.set_scaling(scaling);
414        Ok(self.start_linux_direct(args, output, input_backend))
415    }
416
417    fn start_linux_fb_dev_with_format(
418        &self,
419        args: &[String],
420        fbdev: &str,
421        format: Option<PixelFormat>,
422        scaling: f64,
423        input_backend: Option<Rc<dyn IInputBackend>>,
424    ) -> io::Result<i32> {
425        let output = FbdevOutput::with_format(Some(fbdev), format)?;
426        output.set_scaling(scaling);
427        Ok(self.start_linux_direct(args, output, input_backend))
428    }
429
430    fn start_linux_fb_dev_with_options(
431        &self,
432        args: &[String],
433        options: FbDevOutputOptions,
434        input_backend: Option<Rc<dyn IInputBackend>>,
435    ) -> io::Result<i32> {
436        let output = FbdevOutput::with_options(options)?;
437        Ok(self.start_linux_direct(args, output, input_backend))
438    }
439
440    fn start_linux_drm(
441        &self,
442        args: &[String],
443        card: Option<&str>,
444        scaling: f64,
445        input_backend: Option<Rc<dyn IInputBackend>>,
446    ) -> io::Result<i32> {
447        let output = DrmOutput::new(card, false, None)?;
448        output.set_scaling(scaling);
449        Ok(self.start_linux_direct(args, output, input_backend))
450    }
451
452    fn start_linux_drm_with_options(
453        &self,
454        args: &[String],
455        card: Option<&str>,
456        connectors_force_probe: bool,
457        options: Option<DrmOutputOptions>,
458        input_backend: Option<Rc<dyn IInputBackend>>,
459    ) -> io::Result<i32> {
460        let output = DrmOutput::new(card, connectors_force_probe, options)?;
461        Ok(self.start_linux_direct(args, output, input_backend))
462    }
463
464    fn start_linux_direct(
465        &self,
466        args: &[String],
467        output_backend: Arc<dyn IOutputBackend>,
468        input_backend: Option<Rc<dyn IInputBackend>>,
469    ) -> i32 {
470        let lifetime = LinuxFramebufferPlatform::initialize(self, output_backend, input_backend);
471        self.setup_with_lifetime(lifetime.clone());
472        lifetime.start(args);
473        let instance = self.instance().unwrap_or_else(|| panic!("The application was not created"));
474        instance.run_with_token(&lifetime.token());
475        lifetime.exit_code()
476    }
477
478    fn use_linux_framebuffer(
479        &self,
480        output_backend: Arc<dyn IOutputBackend>,
481        input_backend: Option<Rc<dyn IInputBackend>>,
482    ) -> Rc<LinuxFramebufferLifetime> {
483        LinuxFramebufferPlatform::initialize(self, output_backend, input_backend)
484    }
485}