1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
// SPDX-License-Identifier: MIT OR Apache-2.0
//! [`Screen`] — non-owning reference to the active LVGL screen.
use alloc::vec::Vec;
use core::cell::RefCell;
use oxivgl_sys::*;
use super::obj::AsLvHandle;
use crate::{
layout::{FlexAlign, FlexFlow},
style::{Selector, Style},
};
/// Non-owning reference to the active LVGL screen. Does **not** delete it on
/// drop.
///
/// Obtain via [`Screen::active()`]. Use as a parent for top-level widgets.
///
/// # Style lifetime
///
/// Styles added via [`add_style`](Screen::add_style) are intentionally
/// **leaked** when this `Screen` is dropped. The LVGL screen object outlives
/// any Rust handle to it, so styles must remain valid indefinitely. Each
/// `add_style` call costs one `Rc` bump that is never reclaimed — this is a
/// bounded leak proportional to the number of styles added.
///
/// # Examples
///
/// ```no_run
/// use oxivgl::widgets::Screen;
///
/// let screen = Screen::active().expect("LVGL not initialized");
/// screen.bg_color(0x06080f).bg_opa(255).pad_top(6).pad_bottom(6);
/// ```
pub struct Screen {
handle: *mut lv_obj_t,
/// Rc clones of styles added via `add_style`. Keeps the `lv_style_t`
/// alive as long as this Screen reference exists (spec §5.1).
_styles: RefCell<Vec<Style>>,
}
impl core::fmt::Debug for Screen {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Screen").finish_non_exhaustive()
}
}
impl AsLvHandle for Screen {
fn lv_handle(&self) -> *mut lv_obj_t {
self.handle
}
}
impl Screen {
/// Returns `None` if LVGL has no active screen yet.
pub fn active() -> Option<Self> {
// SAFETY: lv_screen_active() is safe after lv_init(); NULL result is handled
// below.
let handle = unsafe { lv_screen_active() };
if handle.is_null() { None } else { Some(Screen { handle, _styles: RefCell::new(Vec::new()) }) }
}
/// Get the top layer — a global overlay above all screens.
///
/// Returns a non-owning handle. The top layer is owned by LVGL and
/// must never be deleted.
///
/// **Warning:** Styles added to the returned `Child` handle will **not**
/// be freed, because `Child` suppresses `Drop`. If you add styles to
/// the top layer, they leak for the process lifetime.
pub fn layer_top() -> super::child::Child<super::obj::Obj<'static>> {
// SAFETY: lv_layer_top() returns a valid global object after lv_init().
let handle = unsafe { lv_layer_top() };
assert!(!handle.is_null(), "lv_layer_top returned NULL");
super::child::Child::new(super::obj::Obj::from_raw(handle))
}
/// Return the raw `lv_obj_t` pointer for this screen.
pub fn handle(&self) -> *mut lv_obj_t {
self.handle
}
/// Remove the scrollable flag from the screen.
pub fn remove_scrollable(&self) -> &Self {
// SAFETY: handle non-null (Screen::active() returns None for null).
unsafe { lv_obj_remove_flag(self.handle, crate::enums::ObjFlag::SCROLLABLE.0) };
self
}
/// Set background color from RGB hex.
pub fn bg_color(&self, color: u32) -> &Self {
// SAFETY: handle non-null (Screen::active() returns None for null).
unsafe { lv_obj_set_style_bg_color(self.handle, lv_color_hex(color), 0) };
self
}
/// Set background opacity (0–255).
pub fn bg_opa(&self, opa: u8) -> &Self {
// SAFETY: handle non-null (Screen::active() returns None for null).
unsafe { lv_obj_set_style_bg_opa(self.handle, opa as lv_opa_t, 0) };
self
}
/// Set top padding.
pub fn pad_top(&self, p: i32) -> &Self {
// SAFETY: handle non-null (Screen::active() returns None for null).
unsafe { lv_obj_set_style_pad_top(self.handle, p, 0) };
self
}
/// Set bottom padding.
pub fn pad_bottom(&self, p: i32) -> &Self {
// SAFETY: handle non-null (Screen::active() returns None for null).
unsafe { lv_obj_set_style_pad_bottom(self.handle, p, 0) };
self
}
/// Set left padding.
pub fn pad_left(&self, p: i32) -> &Self {
// SAFETY: handle non-null (Screen::active() returns None for null).
unsafe { lv_obj_set_style_pad_left(self.handle, p, 0) };
self
}
/// Set right padding.
pub fn pad_right(&self, p: i32) -> &Self {
// SAFETY: handle non-null (Screen::active() returns None for null).
unsafe { lv_obj_set_style_pad_right(self.handle, p, 0) };
self
}
/// Set default text color from RGB hex.
pub fn text_color(&self, color: u32) -> &Self {
// SAFETY: handle non-null (Screen::active() returns None for null).
unsafe { lv_obj_set_style_text_color(self.handle, lv_color_hex(color), 0) };
self
}
/// Set flex layout flow direction.
pub fn set_flex_flow(&self, flow: FlexFlow) -> &Self {
// SAFETY: handle non-null (Screen::active() returns None for null).
unsafe { lv_obj_set_flex_flow(self.handle, flow as lv_flex_flow_t) };
self
}
/// Apply a style for the given selector.
///
/// Clones the `Style` Rc to keep the `lv_style_t` alive for the
/// screen's lifetime (spec §5.1).
pub fn add_style(&self, style: &Style, selector: impl Into<Selector>) -> &Self {
self._styles.borrow_mut().push(style.clone());
let selector = selector.into().raw();
// SAFETY: handle non-null (Screen::active() returns None for null).
// Style Rc clone above keeps the lv_style_t valid.
unsafe { lv_obj_add_style(self.handle, style.lv_ptr(), selector) };
self
}
/// Conditionally bind a style via the observer API: add `style` with
/// `selector` when `subject == ref_value`, remove it otherwise.
///
/// Same lifetime contract as [`add_style`](Self::add_style) — the style Rc
/// clone is intentionally leaked on drop because the LVGL screen outlives
/// this non-owning handle.
///
/// The subject should outlive the screen. Both drop orders are safe
/// (see [`Subject`](super::subject::Subject) docs).
pub fn bind_style(
&self,
style: &Style,
selector: impl Into<Selector>,
subject: &super::subject::Subject,
ref_value: i32,
) -> &Self {
let selector = selector.into().raw();
// Intentionally leaked on drop (same as add_style for Screen).
self._styles.borrow_mut().push(style.clone());
// SAFETY: handle non-null; style pointer valid for Rc lifetime
// (repr(C) offset-0 guarantee); subject is pinned.
unsafe {
lv_obj_bind_style(self.handle, style.lv_ptr(), selector, subject.as_ptr(), ref_value);
}
self
}
/// Set flex alignment (main, cross, track).
pub fn set_flex_align(&self, main: FlexAlign, cross: FlexAlign, track: FlexAlign) -> &Self {
// SAFETY: handle non-null (Screen::active() returns None for null).
unsafe {
lv_obj_set_flex_align(
self.handle,
main as lv_flex_align_t,
cross as lv_flex_align_t,
track as lv_flex_align_t,
)
};
self
}
/// Create a new LVGL screen object (a root-level widget with no parent).
///
/// Returns an owned [`Obj`](super::obj::Obj) that the caller manages.
/// Use this for navigator-managed screens where normal `Obj::drop`
/// cleanup is desired (no intentional style leaking).
pub fn create() -> super::obj::Obj<'static> {
// SAFETY: lv_obj_create(NULL) creates a root-level screen object.
// Safe after lv_init().
let handle = unsafe { lv_obj_create(core::ptr::null_mut()) };
assert!(!handle.is_null(), "lv_obj_create(NULL) returned NULL");
super::obj::Obj::from_raw(handle)
}
/// Get the system layer — a global overlay above the top layer.
///
/// Returns a non-owning handle. The system layer is owned by LVGL and
/// must never be deleted.
///
/// **Warning:** Styles added to the returned `Child` handle will **not**
/// be freed, because `Child` suppresses `Drop`. If you add styles to
/// the system layer, they leak for the process lifetime.
pub fn layer_sys() -> super::child::Child<super::obj::Obj<'static>> {
// SAFETY: lv_layer_sys() returns a valid global object after lv_init().
let handle = unsafe { lv_layer_sys() };
assert!(!handle.is_null(), "lv_layer_sys returned NULL");
super::child::Child::new(super::obj::Obj::from_raw(handle))
}
/// Load a screen with an animated transition.
///
/// `scr` becomes the new active screen. The previous screen is
/// optionally deleted after the animation completes (`auto_del`).
pub fn load(
scr: &impl AsLvHandle,
anim: &ScreenAnim,
auto_del: bool,
) {
// SAFETY: scr handle is non-null (enforced by AsLvHandle contract).
unsafe {
lv_screen_load_anim(
scr.lv_handle(),
anim.anim_type as lv_screen_load_anim_t,
anim.duration_ms,
anim.delay_ms,
auto_del,
);
}
}
/// Load a screen instantly with no animation.
pub fn load_instant(scr: &impl AsLvHandle) {
// SAFETY: scr handle is non-null (enforced by AsLvHandle contract).
unsafe { lv_screen_load(scr.lv_handle()) };
}
}
/// Screen transition animation type.
///
/// Mirrors `lv_screen_load_anim_t`. Used with [`Screen::load`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum ScreenAnimType {
/// No animation — instant switch.
None = lv_screen_load_anim_t_LV_SCREEN_LOAD_ANIM_NONE,
/// New screen slides in from the left, covering the old one.
OverLeft = lv_screen_load_anim_t_LV_SCREEN_LOAD_ANIM_OVER_LEFT,
/// New screen slides in from the right, covering the old one.
OverRight = lv_screen_load_anim_t_LV_SCREEN_LOAD_ANIM_OVER_RIGHT,
/// New screen slides in from the top, covering the old one.
OverTop = lv_screen_load_anim_t_LV_SCREEN_LOAD_ANIM_OVER_TOP,
/// New screen slides in from the bottom, covering the old one.
OverBottom = lv_screen_load_anim_t_LV_SCREEN_LOAD_ANIM_OVER_BOTTOM,
/// Both screens move left together.
MoveLeft = lv_screen_load_anim_t_LV_SCREEN_LOAD_ANIM_MOVE_LEFT,
/// Both screens move right together.
MoveRight = lv_screen_load_anim_t_LV_SCREEN_LOAD_ANIM_MOVE_RIGHT,
/// Both screens move up together.
MoveTop = lv_screen_load_anim_t_LV_SCREEN_LOAD_ANIM_MOVE_TOP,
/// Both screens move down together.
MoveBottom = lv_screen_load_anim_t_LV_SCREEN_LOAD_ANIM_MOVE_BOTTOM,
/// New screen fades in over the old one.
FadeIn = lv_screen_load_anim_t_LV_SCREEN_LOAD_ANIM_FADE_IN,
/// Old screen fades out, revealing the new one.
FadeOut = lv_screen_load_anim_t_LV_SCREEN_LOAD_ANIM_FADE_OUT,
/// Old screen slides out to the left, revealing the new one.
OutLeft = lv_screen_load_anim_t_LV_SCREEN_LOAD_ANIM_OUT_LEFT,
/// Old screen slides out to the right, revealing the new one.
OutRight = lv_screen_load_anim_t_LV_SCREEN_LOAD_ANIM_OUT_RIGHT,
/// Old screen slides out upward, revealing the new one.
OutTop = lv_screen_load_anim_t_LV_SCREEN_LOAD_ANIM_OUT_TOP,
/// Old screen slides out downward, revealing the new one.
OutBottom = lv_screen_load_anim_t_LV_SCREEN_LOAD_ANIM_OUT_BOTTOM,
}
/// Screen transition animation parameters.
///
/// Used with [`Screen::load`] to control how screen transitions look.
#[derive(Debug, Clone, Copy)]
pub struct ScreenAnim {
/// Animation type (slide, fade, move, etc.).
pub anim_type: ScreenAnimType,
/// Duration in milliseconds.
pub duration_ms: u32,
/// Delay before animation starts, in milliseconds.
pub delay_ms: u32,
}
impl Drop for Screen {
fn drop(&mut self) {
// Intentionally leak style Rc clones. The LVGL screen object outlives
// this non-owning handle — freeing styles here would leave dangling
// pointers in LVGL. Bounded leak: one Rc bump per add_style call.
for style in self._styles.get_mut().drain(..) {
core::mem::forget(style);
}
}
}