i_slint_backend_linuxkms/
calloop_backend.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4use std::cell::RefCell;
5#[cfg(not(feature = "libseat"))]
6use std::fs::OpenOptions;
7use std::os::fd::OwnedFd;
8#[cfg(feature = "libseat")]
9use std::os::fd::{AsFd, AsRawFd, FromRawFd};
10#[cfg(not(feature = "libseat"))]
11use std::os::unix::fs::OpenOptionsExt;
12use std::rc::Rc;
13use std::sync::atomic::AtomicBool;
14use std::sync::{Arc, Mutex};
15
16use calloop::EventLoop;
17use i_slint_core::platform::PlatformError;
18
19use crate::fullscreenwindowadapter::FullscreenWindowAdapter;
20use crate::BackendBuilder;
21
22#[cfg(not(any(target_family = "windows", target_vendor = "apple", target_arch = "wasm32")))]
23mod input;
24
25#[derive(Clone)]
26struct Proxy {
27    loop_signal: Arc<Mutex<Option<calloop::LoopSignal>>>,
28    quit_loop: Arc<AtomicBool>,
29    user_event_channel: Arc<Mutex<calloop::channel::Sender<Box<dyn FnOnce() + Send>>>>,
30}
31
32impl Proxy {
33    fn new(event_channel: calloop::channel::Sender<Box<dyn FnOnce() + Send>>) -> Self {
34        Self {
35            loop_signal: Arc::new(Mutex::new(None)),
36            quit_loop: Arc::new(AtomicBool::new(false)),
37            user_event_channel: Arc::new(Mutex::new(event_channel)),
38        }
39    }
40}
41
42impl i_slint_core::platform::EventLoopProxy for Proxy {
43    fn quit_event_loop(&self) -> Result<(), i_slint_core::api::EventLoopError> {
44        let signal = self.loop_signal.lock().unwrap();
45        signal.as_ref().map_or_else(
46            || Err(i_slint_core::api::EventLoopError::EventLoopTerminated),
47            |signal| {
48                self.quit_loop.store(true, std::sync::atomic::Ordering::Release);
49                signal.wakeup();
50                Ok(())
51            },
52        )
53    }
54
55    fn invoke_from_event_loop(
56        &self,
57        event: Box<dyn FnOnce() + Send>,
58    ) -> Result<(), i_slint_core::api::EventLoopError> {
59        let user_event_channel = self.user_event_channel.lock().unwrap();
60        user_event_channel
61            .send(event)
62            .map_err(|_| i_slint_core::api::EventLoopError::EventLoopTerminated)
63    }
64}
65
66pub struct Backend {
67    #[cfg(feature = "libseat")]
68    seat: Rc<RefCell<libseat::Seat>>,
69    window: RefCell<Option<Rc<FullscreenWindowAdapter>>>,
70    user_event_receiver: RefCell<Option<calloop::channel::Channel<Box<dyn FnOnce() + Send>>>>,
71    proxy: Proxy,
72    renderer_factory: for<'a> fn(
73        &'a crate::DeviceOpener,
74    ) -> Result<
75        Box<dyn crate::fullscreenwindowadapter::FullscreenRenderer>,
76        PlatformError,
77    >,
78    sel_clipboard: RefCell<Option<String>>,
79    clipboard: RefCell<Option<String>>,
80    libinput_event_hook: Option<Box<dyn Fn(&::input::Event) -> bool>>,
81}
82
83impl Backend {
84    pub fn build(builder: BackendBuilder) -> Result<Self, PlatformError> {
85        let (user_event_sender, user_event_receiver) = calloop::channel::channel();
86
87        let renderer_factory = match builder.renderer_name.as_deref() {
88            #[cfg(feature = "renderer-skia-vulkan")]
89            Some("skia-vulkan") => crate::renderer::skia::SkiaRendererAdapter::new_vulkan,
90            #[cfg(feature = "renderer-skia-opengl")]
91            Some("skia-opengl") => crate::renderer::skia::SkiaRendererAdapter::new_opengl,
92            #[cfg(any(feature = "renderer-skia-opengl", feature = "renderer-skia-vulkan"))]
93            Some("skia-software") => crate::renderer::skia::SkiaRendererAdapter::new_software,
94            #[cfg(feature = "renderer-femtovg")]
95            Some("femtovg") => crate::renderer::femtovg::FemtoVGRendererAdapter::new,
96            #[cfg(feature = "renderer-software")]
97            Some("software") => crate::renderer::sw::SoftwareRendererAdapter::new,
98            None => crate::renderer::try_skia_then_femtovg_then_software,
99            Some(renderer_name) => {
100                eprintln!(
101                    "slint linuxkms backend: unrecognized renderer {}, falling back default",
102                    renderer_name
103                );
104                crate::renderer::try_skia_then_femtovg_then_software
105            }
106        };
107
108        #[cfg(feature = "libseat")]
109        let seat_active = Rc::new(RefCell::new(false));
110
111        //libseat::set_log_level(libseat::LogLevel::Debug);
112
113        #[cfg(feature = "libseat")]
114        let mut seat = {
115            let seat_active = seat_active.clone();
116            libseat::Seat::open(move |_seat, event| match event {
117                libseat::SeatEvent::Enable => {
118                    *seat_active.borrow_mut() = true;
119                }
120                libseat::SeatEvent::Disable => {
121                    unimplemented!("Seat deactivation is not implemented");
122                }
123            })
124            .map_err(|e| format!("Error opening session with libseat: {e}"))?
125        };
126
127        #[cfg(feature = "libseat")]
128        while !(*seat_active.borrow()) {
129            if seat.dispatch(5000).map_err(|e| format!("Error waiting for seat activation: {e}"))?
130                == 0
131            {
132                return Err(format!("Timeout while waiting to activate session").into());
133            }
134        }
135
136        Ok(Backend {
137            #[cfg(feature = "libseat")]
138            seat: Rc::new(RefCell::new(seat)),
139            window: Default::default(),
140            user_event_receiver: RefCell::new(Some(user_event_receiver)),
141            proxy: Proxy::new(user_event_sender),
142            renderer_factory,
143            sel_clipboard: Default::default(),
144            clipboard: Default::default(),
145            libinput_event_hook: builder.libinput_event_hook,
146        })
147    }
148}
149
150impl i_slint_core::platform::Platform for Backend {
151    fn create_window_adapter(
152        &self,
153    ) -> Result<std::rc::Rc<dyn i_slint_core::window::WindowAdapter>, PlatformError> {
154        #[cfg(feature = "libseat")]
155        let device_accessor = |device: &std::path::Path| -> Result<Rc<OwnedFd>, PlatformError> {
156            let device = self
157                .seat
158                .borrow_mut()
159                .open_device(&device)
160                .map_err(|e| format!("Error opening device {}: {e}", device.display()))?;
161
162            // For polling for drm::control::Event::PageFlip we need a blocking FD. Would be better to do this non-blocking
163            let fd = device.as_fd();
164            let flags = nix::fcntl::fcntl(fd, nix::fcntl::FcntlArg::F_GETFL)
165                .map_err(|e| format!("Error getting file descriptor flags: {e}"))?;
166            let mut flags = nix::fcntl::OFlag::from_bits_retain(flags);
167            flags.remove(nix::fcntl::OFlag::O_NONBLOCK);
168            nix::fcntl::fcntl(fd, nix::fcntl::FcntlArg::F_SETFL(flags))
169                .map_err(|e| format!("Error making device fd non-blocking: {e}"))?;
170
171            // Safety: We take ownership of the now shared FD, ... although we should be using libseat's close_device....
172            Ok(Rc::new(unsafe { std::os::fd::OwnedFd::from_raw_fd(fd.as_raw_fd()) }))
173        };
174
175        #[cfg(not(feature = "libseat"))]
176        let device_accessor = |device: &std::path::Path| -> Result<Rc<OwnedFd>, PlatformError> {
177            let device = OpenOptions::new()
178                .custom_flags((nix::fcntl::OFlag::O_NOCTTY | nix::fcntl::OFlag::O_CLOEXEC).bits())
179                .read(true)
180                .write(true)
181                .open(device)
182                .map(|file| file.into())
183                .map_err(|e| format!("Error opening device {}: {e}", device.display()))?;
184
185            Ok(Rc::new(device))
186        };
187
188        // This could be per-screen, once we support multiple outputs
189        let rotation =
190            std::env::var("SLINT_KMS_ROTATION").map_or(Ok(Default::default()), |rot_str| {
191                rot_str
192                    .as_str()
193                    .try_into()
194                    .map_err(|e| format!("Failed to parse SLINT_KMS_ROTATION: {e}"))
195            })?;
196
197        let renderer = (self.renderer_factory)(&device_accessor)?;
198        let adapter = FullscreenWindowAdapter::new(renderer, rotation)?;
199
200        *self.window.borrow_mut() = Some(adapter.clone());
201
202        Ok(adapter)
203    }
204
205    fn run_event_loop(&self) -> Result<(), PlatformError> {
206        let mut event_loop: EventLoop<LoopData> =
207            EventLoop::try_new().map_err(|e| format!("Error creating event loop: {}", e))?;
208
209        let loop_signal = event_loop.get_signal();
210
211        *self.proxy.loop_signal.lock().unwrap() = Some(loop_signal.clone());
212        let quit_loop = self.proxy.quit_loop.clone();
213
214        let mouse_position_property = input::LibInputHandler::init(
215            &self.window,
216            &event_loop.handle(),
217            #[cfg(feature = "libseat")]
218            &self.seat,
219            &self.libinput_event_hook,
220        )?;
221
222        let Some(user_event_receiver) = self.user_event_receiver.borrow_mut().take() else {
223            return Err(
224                format!("Re-entering the linuxkms event loop is currently not supported").into()
225            );
226        };
227
228        let callbacks_to_invoke_per_iteration = Rc::new(RefCell::new(Vec::new()));
229
230        event_loop
231            .handle()
232            .insert_source(user_event_receiver, {
233                let callbacks_to_invoke_per_iteration = callbacks_to_invoke_per_iteration.clone();
234                move |event, _, _| {
235                    let calloop::channel::Event::Msg(callback) = event else { return };
236                    // Remember the callbacks and invoke them after updating the animation tick
237                    callbacks_to_invoke_per_iteration.borrow_mut().push(callback);
238                }
239            })
240            .map_err(
241                |e: calloop::InsertError<calloop::channel::Channel<Box<dyn FnOnce() + Send>>>| {
242                    format!("Error registering user event channel source: {e}")
243                },
244            )?;
245
246        let mut loop_data = LoopData::default();
247
248        quit_loop.store(false, std::sync::atomic::Ordering::Release);
249
250        while !quit_loop.load(std::sync::atomic::Ordering::Acquire) {
251            i_slint_core::platform::update_timers_and_animations();
252
253            // Only after updating the animation tick, invoke callbacks from invoke_from_event_loop(). They
254            // might set animated properties, which requires an up-to-date start time.
255            for callback in callbacks_to_invoke_per_iteration.take().into_iter() {
256                callback();
257            }
258
259            if let Some(adapter) = self.window.borrow().as_ref() {
260                adapter.clone().render_if_needed(mouse_position_property.as_ref())?;
261            };
262
263            let next_timeout = i_slint_core::platform::duration_until_next_timer_update();
264            event_loop
265                .dispatch(next_timeout, &mut loop_data)
266                .map_err(|e| format!("Error dispatch events: {e}"))?;
267        }
268
269        Ok(())
270    }
271
272    fn new_event_loop_proxy(&self) -> Option<Box<dyn i_slint_core::platform::EventLoopProxy>> {
273        Some(Box::new(self.proxy.clone()))
274    }
275
276    fn clipboard_text(&self, clipboard: i_slint_core::platform::Clipboard) -> Option<String> {
277        match clipboard {
278            i_slint_core::platform::Clipboard::DefaultClipboard => self.clipboard.borrow().clone(),
279            i_slint_core::platform::Clipboard::SelectionClipboard => {
280                self.sel_clipboard.borrow().clone()
281            }
282            _ => None,
283        }
284    }
285    fn set_clipboard_text(&self, text: &str, clipboard: i_slint_core::platform::Clipboard) {
286        match clipboard {
287            i_slint_core::platform::Clipboard::DefaultClipboard => {
288                *self.clipboard.borrow_mut() = Some(text.into())
289            }
290            i_slint_core::platform::Clipboard::SelectionClipboard => {
291                *self.sel_clipboard.borrow_mut() = Some(text.into())
292            }
293            _ => (),
294        }
295    }
296}
297
298#[derive(Default)]
299pub struct LoopData {}