1use crate::assets;
2use crate::drawing::DrawContext;
3use crate::ffi;
4use crate::frame_scheduler::on_loaded;
5use crate::logger::error;
6use crate::node::Node;
7use crate::text::TextLayout;
8use crate::typography::FontFace;
9use std::cell::RefCell;
10use std::rc::Rc;
11
12const MAX_DIRTY_RECTS: usize = 16;
13
14#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
15pub struct BitmapTextReadyEventArgs;
16
17impl BitmapTextReadyEventArgs {
18 pub const EMPTY: Self = Self;
19}
20
21struct BitmapState {
22 width: u32,
23 height: u32,
24 texture_id: u32,
25 pixel_bytes: Vec<u8>,
26 offscreen_id: u32,
27 canvas_used: bool,
28 draw_context: Option<DrawContext>,
29 disposed: bool,
30 dirty_rects: Vec<(u32, u32, u32, u32)>,
31}
32
33#[derive(Clone)]
34pub struct Bitmap {
35 inner: Rc<RefCell<BitmapState>>,
36}
37
38impl Bitmap {
39 pub fn new(width: u32, height: u32) -> Self {
40 assert!(
41 width > 0 && height > 0,
42 "Bitmap width and height must be greater than zero."
43 );
44 let byte_len = (width as usize)
45 .checked_mul(height as usize)
46 .and_then(|value| value.checked_mul(4))
47 .expect("Bitmap byte length overflow.");
48 let texture_id = assets::allocate_dynamic_texture_id();
49 let offscreen_id = unsafe { ffi::fui_canvas_create_offscreen(width, height) };
50 Self {
51 inner: Rc::new(RefCell::new(BitmapState {
52 width,
53 height,
54 texture_id,
55 pixel_bytes: vec![0; byte_len],
56 offscreen_id,
57 canvas_used: false,
58 draw_context: None,
59 disposed: false,
60 dirty_rects: Vec::new(),
61 })),
62 }
63 }
64
65 pub fn width(&self) -> u32 {
66 self.inner.borrow().width
67 }
68
69 pub fn height(&self) -> u32 {
70 self.inner.borrow().height
71 }
72
73 pub fn texture_id(&self) -> u32 {
74 self.inner.borrow().texture_id
75 }
76
77 pub fn pixels(&self) -> std::cell::RefMut<'_, Vec<u8>> {
78 std::cell::RefMut::map(self.inner.borrow_mut(), |state| {
79 assert!(!state.disposed, "Bitmap.pixels() called after dispose.");
80 &mut state.pixel_bytes
81 })
82 }
83
84 pub fn pixel_ptr(&self) -> usize {
85 let state = self.inner.borrow();
86 if state.pixel_bytes.is_empty() {
87 0
88 } else {
89 state.pixel_bytes.as_ptr() as usize
90 }
91 }
92
93 pub fn canvas(&self) -> DrawContext {
94 let mut state = self.inner.borrow_mut();
95 assert!(!state.disposed, "Bitmap.canvas() called after dispose.");
96 state.canvas_used = true;
97 if let Some(context) = &state.draw_context {
98 return context.clone();
99 }
100 let ptr = unsafe { ffi::fui_canvas_get_offscreen_ptr(state.offscreen_id) };
101 let context = DrawContext::new(ptr);
102 state.draw_context = Some(context.clone());
103 context
104 }
105
106 pub fn render<T: Node>(&self, node: &T, x: f32, y: f32, scale: f32) {
107 let state = self.inner.borrow();
108 assert!(!state.disposed, "Bitmap.render() called after dispose.");
109 let handle = node.handle().raw();
110 if handle == 0 {
111 return;
112 }
113 unsafe {
114 ffi::fui_render_node_to_rgba(
115 handle,
116 state.width,
117 state.height,
118 state.pixel_bytes.as_ptr() as usize,
119 state.pixel_bytes.len() as u32,
120 scale,
121 x,
122 y,
123 )
124 };
125 }
126
127 pub fn render_text_layout(&self, layout: &TextLayout, x: f32, y: f32, scale: f32) {
128 if !layout.is_ready() {
129 error(
130 "TextLayout",
131 "Bitmap.render_text_layout() called before the TextLayout was ready; register on_ready and render after the callback.",
132 );
133 return;
134 }
135 let node = layout.draw_node();
136 self.render(&node, x, y, scale);
137 }
138
139 pub fn prepare_text<T: Node>(node: &T) {
140 node.build();
141 crate::bindings::ui::prepare_node(node.handle().raw());
142 }
143
144 pub fn on_text_ready<T: Node + Clone + 'static>(
145 &self,
146 node: &T,
147 callback: impl FnOnce(BitmapTextReadyEventArgs) + 'static,
148 ) -> &Self {
149 let node = node.clone();
150 let required_font_ids = node.required_font_ids_for_preparation();
151 let callback = Rc::new(RefCell::new(Some(callback)));
152 FontFace::when_fonts_loaded(&required_font_ids, move |_| {
153 let node = node.clone();
154 let callback = callback.clone();
155 on_loaded(move |_| {
156 Self::prepare_text(&node);
157 if let Some(callback) = callback.borrow_mut().take() {
158 callback(BitmapTextReadyEventArgs::EMPTY);
159 }
160 });
161 });
162 self
163 }
164
165 pub fn dirty_rect(&self, x: u32, y: u32, w: u32, h: u32) -> &Self {
166 let mut state = self.inner.borrow_mut();
167 if w == 0 || h == 0 || x >= state.width || y >= state.height {
168 return self;
169 }
170 let cw = (x + w).min(state.width) - x;
171 let ch = (y + h).min(state.height) - y;
172 if state.dirty_rects.len() < MAX_DIRTY_RECTS {
173 state.dirty_rects.push((x, y, cw, ch));
174 }
175 self
176 }
177
178 pub fn clear_dirty_rects(&self) -> &Self {
179 self.inner.borrow_mut().dirty_rects.clear();
180 self
181 }
182
183 pub fn has_dirty_rects(&self) -> bool {
184 !self.inner.borrow().dirty_rects.is_empty()
185 }
186
187 pub fn commit(&self) -> u32 {
188 let mut state = self.inner.borrow_mut();
189 assert!(!state.disposed, "Bitmap.commit() called after dispose.");
190 if state.canvas_used {
191 if let Some(context) = &state.draw_context {
192 context.flush();
193 }
194 unsafe {
195 ffi::fui_canvas_read_offscreen_pixels(
196 state.offscreen_id,
197 state.pixel_bytes.as_mut_ptr() as usize,
198 state.width,
199 state.height,
200 )
201 };
202 }
203 if state.dirty_rects.is_empty() {
204 unsafe {
205 ffi::fui_bitmap_commit(
206 state.texture_id,
207 state.pixel_bytes.as_ptr() as usize,
208 state.pixel_bytes.len() as u32,
209 state.width,
210 state.height,
211 )
212 };
213 } else {
214 let dirty_rects = std::mem::take(&mut state.dirty_rects);
215 for (x, y, w, h) in dirty_rects {
216 let mut rect_bytes = vec![0u8; (w as usize) * (h as usize) * 4];
217 for row in 0..h as usize {
218 let src = (((y as usize + row) * state.width as usize) + x as usize) * 4;
219 let dst = row * w as usize * 4;
220 let len = w as usize * 4;
221 rect_bytes[dst..dst + len].copy_from_slice(&state.pixel_bytes[src..src + len]);
222 }
223 unsafe {
224 ffi::fui_bitmap_commit_dirty(
225 state.texture_id,
226 rect_bytes.as_ptr() as usize,
227 rect_bytes.len() as u32,
228 state.width,
229 state.height,
230 x,
231 y,
232 w,
233 h,
234 )
235 };
236 }
237 }
238 assets::mark_texture_asset_ready(state.texture_id, state.width as f32, state.height as f32);
239 state.texture_id
240 }
241
242 pub fn dispose(&self) {
243 let mut state = self.inner.borrow_mut();
244 if state.disposed {
245 return;
246 }
247 state.disposed = true;
248 unsafe { ffi::fui_canvas_destroy_offscreen(state.offscreen_id) };
249 unsafe { ffi::fui_bitmap_release(state.texture_id) };
250 state.pixel_bytes.clear();
251 state.draw_context = None;
252 }
253}
254
255impl Drop for Bitmap {
256 fn drop(&mut self) {
257 if Rc::strong_count(&self.inner) == 1 {
258 self.dispose();
259 }
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use super::Bitmap;
266 use crate::drawing::Paint;
267 use crate::ffi::{self, Call};
268 use crate::frame_scheduler;
269 use crate::node::{Node, TextNode};
270 use std::cell::Cell;
271 use std::rc::Rc;
272
273 #[test]
274 fn bitmap_canvas_commit_flushes_offscreen_and_marks_asset_ready() {
275 ffi::test::reset();
276 let bitmap = Bitmap::new(32, 24);
277 let canvas = bitmap.canvas();
278 canvas.draw_rect(0.0, 0.0, 10.0, 12.0, Paint::fill(0xFF00FFFF));
279 bitmap.commit();
280
281 let calls = ffi::test::take_calls();
282 assert!(calls.iter().any(|call| matches!(
283 call,
284 Call::CanvasCreateOffscreen {
285 width: 32,
286 height: 24,
287 ..
288 }
289 )));
290 assert!(calls
291 .iter()
292 .any(|call| matches!(call, Call::CanvasDrawBatch { .. })));
293 assert!(calls.iter().any(|call| matches!(
294 call,
295 Call::CanvasReadOffscreenPixels {
296 width: 32,
297 height: 24,
298 ..
299 }
300 )));
301 assert!(calls.iter().any(|call| matches!(
302 call,
303 Call::BitmapCommit {
304 width: 32,
305 height: 24,
306 ..
307 }
308 )));
309 }
310
311 #[test]
312 fn bitmap_dirty_commit_uses_subrect_upload() {
313 ffi::test::reset();
314 let bitmap = Bitmap::new(8, 6);
315 bitmap.dirty_rect(2, 1, 3, 2).commit();
316
317 let calls = ffi::test::take_calls();
318 assert!(calls.iter().any(|call| matches!(
319 call,
320 Call::BitmapCommitDirty {
321 full_width: 8,
322 full_height: 6,
323 sub_x: 2,
324 sub_y: 1,
325 sub_w: 3,
326 sub_h: 2,
327 ..
328 }
329 )));
330 }
331
332 #[test]
333 fn bitmap_text_ready_builds_detached_text_before_preparing_it() {
334 ffi::test::reset();
335 frame_scheduler::reset_commit_state();
336 let bitmap = Bitmap::new(64, 32);
337 let text = TextNode::new("Detached bitmap text");
338 let fired = Rc::new(Cell::new(false));
339 bitmap.on_text_ready(&text, {
340 let fired = fired.clone();
341 move |_| fired.set(true)
342 });
343
344 frame_scheduler::fire_loaded_callbacks();
345
346 assert!(fired.get());
347 assert!(text.has_built_handle());
348 let calls = ffi::test::take_calls();
349 assert!(calls
350 .iter()
351 .any(|call| matches!(call, Call::PrepareNode { .. })));
352 }
353}