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;
17
18impl BitmapTextReadyEventArgs {
19 pub const EMPTY: Self = Self;
21}
22
23struct BitmapState {
24 width: u32,
25 height: u32,
26 texture_id: u32,
27 pixel_bytes: Vec<u8>,
28 offscreen_id: u32,
29 canvas_used: bool,
30 draw_context: Option<DrawContext>,
31 disposed: bool,
32 dirty_rects: Vec<(u32, u32, u32, u32)>,
33}
34
35#[derive(Clone)]
36pub struct Bitmap {
52 inner: Rc<RefCell<BitmapState>>,
53}
54
55impl Bitmap {
56 pub fn new(width: u32, height: u32) -> Self {
58 assert!(
59 width > 0 && height > 0,
60 "Bitmap width and height must be greater than zero."
61 );
62 let byte_len = (width as usize)
63 .checked_mul(height as usize)
64 .and_then(|value| value.checked_mul(4))
65 .expect("Bitmap byte length overflow.");
66 let texture_id = assets::allocate_dynamic_texture_id();
67 let offscreen_id = unsafe { ffi::fui_canvas_create_offscreen(width, height) };
68 Self {
69 inner: Rc::new(RefCell::new(BitmapState {
70 width,
71 height,
72 texture_id,
73 pixel_bytes: vec![0; byte_len],
74 offscreen_id,
75 canvas_used: false,
76 draw_context: None,
77 disposed: false,
78 dirty_rects: Vec::new(),
79 })),
80 }
81 }
82
83 pub fn width(&self) -> u32 {
85 self.inner.borrow().width
86 }
87
88 pub fn height(&self) -> u32 {
90 self.inner.borrow().height
91 }
92
93 pub fn texture_id(&self) -> u32 {
95 self.inner.borrow().texture_id
96 }
97
98 pub fn pixels(&self) -> std::cell::RefMut<'_, Vec<u8>> {
102 std::cell::RefMut::map(self.inner.borrow_mut(), |state| {
103 assert!(!state.disposed, "Bitmap.pixels() called after dispose.");
104 &mut state.pixel_bytes
105 })
106 }
107
108 pub fn pixel_ptr(&self) -> usize {
110 let state = self.inner.borrow();
111 if state.pixel_bytes.is_empty() {
112 0
113 } else {
114 state.pixel_bytes.as_ptr() as usize
115 }
116 }
117
118 pub fn canvas(&self) -> DrawContext {
124 let mut state = self.inner.borrow_mut();
125 assert!(!state.disposed, "Bitmap.canvas() called after dispose.");
126 state.canvas_used = true;
127 if let Some(context) = &state.draw_context {
128 return context.clone();
129 }
130 let ptr = unsafe { ffi::fui_canvas_get_offscreen_ptr(state.offscreen_id) };
131 let context = DrawContext::new(ptr);
132 state.draw_context = Some(context.clone());
133 context
134 }
135
136 pub fn render<T: Node>(&self, node: &T, x: f32, y: f32, scale: f32) -> bool {
141 let mut state = self.inner.borrow_mut();
142 assert!(!state.disposed, "Bitmap.render() called after dispose.");
143 let handle = node.handle().raw();
144 if handle == 0 {
145 return false;
146 }
147 unsafe {
148 ffi::fui_render_node_to_rgba(
149 handle,
150 state.width,
151 state.height,
152 state.pixel_bytes.as_mut_ptr() as usize,
153 state.pixel_bytes.len() as u32,
154 scale,
155 x,
156 y,
157 ) != 0
158 }
159 }
160
161 pub fn render_text_layout(&self, layout: &TextLayout, x: f32, y: f32, scale: f32) -> bool {
163 if !layout.is_ready() {
164 error(
165 "TextLayout",
166 "Bitmap.render_text_layout() called before the TextLayout was ready; register on_ready and render after the callback.",
167 );
168 return false;
169 }
170 let node = layout.draw_node();
171 self.render(&node, x, y, scale)
172 }
173
174 pub fn prepare_text<T: Node>(node: &T) {
176 node.build();
177 crate::bindings::ui::prepare_node(node.handle().raw());
178 }
179
180 pub fn on_text_ready<T: Node + Clone + 'static>(
182 &self,
183 node: &T,
184 callback: impl FnOnce(BitmapTextReadyEventArgs) + 'static,
185 ) -> &Self {
186 let node = node.clone();
187 let required_font_ids = node.required_font_ids_for_preparation();
188 let callback = Rc::new(RefCell::new(Some(callback)));
189 FontFace::when_fonts_loaded(&required_font_ids, move |_| {
190 let node = node.clone();
191 let callback = callback.clone();
192 on_loaded(move |_| {
193 Self::prepare_text(&node);
194 if let Some(callback) = callback.borrow_mut().take() {
195 callback(BitmapTextReadyEventArgs::EMPTY);
196 }
197 });
198 });
199 self
200 }
201
202 pub fn dirty_rect(&self, x: u32, y: u32, w: u32, h: u32) -> &Self {
206 let mut state = self.inner.borrow_mut();
207 if w == 0 || h == 0 || x >= state.width || y >= state.height {
208 return self;
209 }
210 let cw = (x + w).min(state.width) - x;
211 let ch = (y + h).min(state.height) - y;
212 if state.dirty_rects.len() < MAX_DIRTY_RECTS {
213 state.dirty_rects.push((x, y, cw, ch));
214 }
215 self
216 }
217
218 pub fn clear_dirty_rects(&self) -> &Self {
221 self.inner.borrow_mut().dirty_rects.clear();
222 self
223 }
224
225 pub fn has_dirty_rects(&self) -> bool {
227 !self.inner.borrow().dirty_rects.is_empty()
228 }
229
230 pub fn commit(&self) -> u32 {
233 let mut state = self.inner.borrow_mut();
234 assert!(!state.disposed, "Bitmap.commit() called after dispose.");
235 if state.canvas_used {
236 if let Some(context) = &state.draw_context {
237 context.flush();
238 }
239 unsafe {
240 ffi::fui_canvas_read_offscreen_pixels(
241 state.offscreen_id,
242 state.pixel_bytes.as_mut_ptr() as usize,
243 state.width,
244 state.height,
245 )
246 };
247 }
248 if state.dirty_rects.is_empty() {
249 unsafe {
250 ffi::fui_bitmap_commit(
251 state.texture_id,
252 state.pixel_bytes.as_ptr() as usize,
253 state.pixel_bytes.len() as u32,
254 state.width,
255 state.height,
256 )
257 };
258 } else {
259 let dirty_rects = std::mem::take(&mut state.dirty_rects);
260 for (x, y, w, h) in dirty_rects {
261 let mut rect_bytes = vec![0u8; (w as usize) * (h as usize) * 4];
262 for row in 0..h as usize {
263 let src = (((y as usize + row) * state.width as usize) + x as usize) * 4;
264 let dst = row * w as usize * 4;
265 let len = w as usize * 4;
266 rect_bytes[dst..dst + len].copy_from_slice(&state.pixel_bytes[src..src + len]);
267 }
268 unsafe {
269 ffi::fui_bitmap_commit_dirty(
270 state.texture_id,
271 rect_bytes.as_ptr() as usize,
272 rect_bytes.len() as u32,
273 state.width,
274 state.height,
275 x,
276 y,
277 w,
278 h,
279 )
280 };
281 }
282 }
283 assets::mark_texture_asset_ready(state.texture_id, state.width as f32, state.height as f32);
284 state.texture_id
285 }
286
287 pub fn dispose(&self) {
292 let mut state = self.inner.borrow_mut();
293 if state.disposed {
294 return;
295 }
296 state.disposed = true;
297 unsafe { ffi::fui_canvas_destroy_offscreen(state.offscreen_id) };
298 unsafe { ffi::fui_bitmap_release(state.texture_id) };
299 state.pixel_bytes.clear();
300 state.draw_context = None;
301 }
302}
303
304impl Drop for Bitmap {
305 fn drop(&mut self) {
306 if Rc::strong_count(&self.inner) == 1 {
307 self.dispose();
308 }
309 }
310}
311
312#[cfg(test)]
313mod tests {
314 use super::Bitmap;
315 use crate::drawing::Paint;
316 use crate::ffi::{self, Call};
317 use crate::frame_scheduler;
318 use crate::node::{Node, TextNode};
319 use std::cell::Cell;
320 use std::rc::Rc;
321
322 #[test]
323 fn bitmap_canvas_commit_flushes_offscreen_and_marks_asset_ready() {
324 ffi::test::reset();
325 let bitmap = Bitmap::new(32, 24);
326 let canvas = bitmap.canvas();
327 canvas.draw_rect(0.0, 0.0, 10.0, 12.0, Paint::fill(0xFF00FFFF));
328 bitmap.commit();
329
330 let calls = ffi::test::take_calls();
331 assert!(calls.iter().any(|call| matches!(
332 call,
333 Call::CanvasCreateOffscreen {
334 width: 32,
335 height: 24,
336 ..
337 }
338 )));
339 assert!(calls
340 .iter()
341 .any(|call| matches!(call, Call::CanvasDrawBatch { .. })));
342 assert!(calls.iter().any(|call| matches!(
343 call,
344 Call::CanvasReadOffscreenPixels {
345 width: 32,
346 height: 24,
347 ..
348 }
349 )));
350 assert!(calls.iter().any(|call| matches!(
351 call,
352 Call::BitmapCommit {
353 width: 32,
354 height: 24,
355 ..
356 }
357 )));
358 }
359
360 #[test]
361 fn bitmap_dirty_commit_uses_subrect_upload() {
362 ffi::test::reset();
363 let bitmap = Bitmap::new(8, 6);
364 bitmap.dirty_rect(2, 1, 3, 2).commit();
365
366 let calls = ffi::test::take_calls();
367 assert!(calls.iter().any(|call| matches!(
368 call,
369 Call::BitmapCommitDirty {
370 full_width: 8,
371 full_height: 6,
372 sub_x: 2,
373 sub_y: 1,
374 sub_w: 3,
375 sub_h: 2,
376 ..
377 }
378 )));
379 }
380
381 #[test]
382 fn bitmap_text_ready_builds_detached_text_before_preparing_it() {
383 ffi::test::reset();
384 frame_scheduler::reset_commit_state();
385 let bitmap = Bitmap::new(64, 32);
386 let text = TextNode::new("Detached bitmap text");
387 let fired = Rc::new(Cell::new(false));
388 bitmap.on_text_ready(&text, {
389 let fired = fired.clone();
390 move |_| fired.set(true)
391 });
392
393 frame_scheduler::fire_loaded_callbacks();
394
395 assert!(fired.get());
396 assert!(text.has_built_handle());
397 let calls = ffi::test::take_calls();
398 assert!(calls
399 .iter()
400 .any(|call| matches!(call, Call::PrepareNode { .. })));
401 }
402
403 #[test]
404 fn bitmap_render_retains_host_written_pixels_for_the_next_upload() {
405 ffi::test::reset();
406 let bitmap = Bitmap::new(2, 2);
407 let text = TextNode::new("Rich bitmap text");
408 text.build();
409
410 assert!(bitmap.render(&text, 0.0, 0.0, 1.0));
411 bitmap.commit();
412
413 let calls = ffi::test::take_calls();
414 assert!(calls.iter().any(|call| matches!(
415 call,
416 Call::BitmapCommit { bytes, .. }
417 if bytes.chunks_exact(4).any(|pixel| pixel == [0x3a, 0xc5, 0x6c, 0xff])
418 )));
419 }
420}