Skip to main content

fui/
bitmap.rs

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}