Skip to main content

cotis_raylib/
drawables.rs

1//! Raylib draw implementations for `cotis-defaults` render commands and custom drawables.
2//!
3//! [`RaylibDrawable`] is the extension trait for types that can be drawn by [`RaylibRender`](crate::renderer::RaylibRender).
4//! Built-in impls cover [`Rectangle`], [`Text`], [`Border`], [`Image`], [`ClipStart`]/[`ClipEnd`], and
5//! [`RenderTList`](cotis_defaults::render_commands::render_t_list::RenderTList).
6//!
7//! # Custom drawables
8//!
9//! ```rust,ignore
10//! use cotis_raylib::drawables::{RaylibDrawable, RaylibDrawContext};
11//!
12//! struct CircleDrawable { cx: f32, cy: f32, radius: f32 }
13//!
14//! impl RaylibDrawable for CircleDrawable {
15//!     fn draw(self: Box<Self>, ctx: &mut RaylibDrawContext<'_, '_>) {
16//!         // ctx.d.draw_circle(...)
17//!     }
18//!     fn scale_by(&mut self, factor: f32) {
19//!         self.cx *= factor;
20//!         self.cy *= factor;
21//!         self.radius *= factor;
22//!     }
23//! }
24//! ```
25//!
26//! # Color conversion
27//!
28//! Solid colors are converted by truncating each `f32` channel to `u8`
29//! (expects cotis-defaults `0..=255` range; values outside that range are not clamped).
30
31use std::collections::HashMap;
32use std::sync::{Arc, Mutex};
33
34use raylib::drawing::{RaylibDraw, RaylibDrawHandle};
35use raylib::math::Vector2;
36
37use crate::fonts::ScalableFont;
38use crate::raylib_images::GenericCompatibleImage;
39use crate::renderer::RaylibRender;
40use cotis_defaults::colors::Color as CotisColor;
41use cotis_defaults::render_commands::render_t_list::{IsRenderList, RenderTList};
42use cotis_defaults::render_commands::*;
43use cotis_utils::math::BoundingBox;
44use raylib::texture::Texture2D;
45
46/// Per-frame drawing context passed to [`RaylibDrawable::draw`].
47pub struct RaylibDrawContext<'d, 'rl> {
48    /// Active raylib draw handle for the current frame.
49    pub d: &'d mut RaylibDrawHandle<'rl>,
50    /// Shared font table from [`RaylibRender`](crate::renderer::RaylibRender).
51    pub fonts: Arc<Mutex<HashMap<usize, ScalableFont>>>,
52    /// Stack of active clip rectangles `(x, y, width, height)` in layout coordinates.
53    pub clip_stack: &'d mut Vec<(f32, f32, f32, f32)>,
54    /// Path-keyed texture cache; `Some` for [`RenderTList`](cotis_defaults::render_commands::render_t_list::RenderTList) drawing, `None` for `Box<dyn RaylibDrawable>`.
55    pub image_cache: Option<&'d HashMap<String, Arc<Texture2D>>>,
56}
57
58/// Types that can be drawn by [`RaylibRender`](crate::renderer::RaylibRender).
59pub trait RaylibDrawable {
60    /// Renders this command using context `ctx`.
61    fn draw(self: Box<Self>, ctx: &mut RaylibDrawContext<'_, '_>);
62    /// Scales all positional and size data by `factor` in-place (not colors).
63    ///
64    /// Used by [`RayTestRender`](crate::test_render::RayTestRender) for logical-resolution scaling.
65    fn scale_by(&mut self, factor: f32);
66    /// Preloads path-based images into [`RaylibRender::image_cache`](crate::renderer::RaylibRender::image_cache).
67    ///
68    /// Default no-op; [`RenderTList`](cotis_defaults::render_commands::render_t_list::RenderTList) calls this before drawing.
69    fn preload_generic_images(&self, _render: &mut RaylibRender) {}
70}
71
72// ── Helpers ──────────────────────────────────────────────────────────────────
73
74/// Converts a cotis-defaults [`CotisColor`] to a raylib color by truncating each channel `f32` to `u8`.
75///
76/// Expects channel values in the `0..=255` range per cotis-defaults. Values outside that range are
77/// not clamped (`256.0` truncates to `0`).
78pub(crate) fn cotis_to_raylib_color(c: CotisColor) -> raylib::color::Color {
79    raylib::color::Color::new(c.r as u8, c.g as u8, c.b as u8, c.a as u8)
80}
81
82fn scale_corner_radii(r: &mut CornerRadii, factor: f32) {
83    r.top_left *= factor;
84    r.top_right *= factor;
85    r.bottom_left *= factor;
86    r.bottom_right *= factor;
87}
88
89fn scale_bounding_box(bb: &mut BoundingBox, factor: f32) {
90    bb.x *= factor;
91    bb.y *= factor;
92    bb.width *= factor;
93    bb.height *= factor;
94}
95
96fn intersect_clip(a: (f32, f32, f32, f32), b: (f32, f32, f32, f32)) -> (f32, f32, f32, f32) {
97    let ax2 = a.0 + a.2;
98    let ay2 = a.1 + a.3;
99    let bx2 = b.0 + b.2;
100    let by2 = b.1 + b.3;
101    let x = a.0.max(b.0);
102    let y = a.1.max(b.1);
103    let w = ax2.min(bx2) - x;
104    let h = ay2.min(by2) - y;
105    (x, y, w.max(0.0), h.max(0.0))
106}
107
108// ── Rectangle ────────────────────────────────────────────────────────────────
109//
110// Solid fill via raylib rectangles/rounded rects. With `complex-color`, delegates to gradient painting.
111// Rounded corners use only `corner_radii.top_left` for the radius formula.
112
113impl<'a, E> RaylibDrawable for Rectangle<'a, E> {
114    fn draw(self: Box<Self>, ctx: &mut RaylibDrawContext<'_, '_>) {
115        let bb = self.info.bounding_box;
116
117        #[cfg(feature = "complex-color")]
118        {
119            let clip = ctx.clip_stack.last().copied();
120            crate::color_paint::draw_color_layer_in_rect(
121                ctx.d,
122                bb.x,
123                bb.y,
124                bb.width,
125                bb.height,
126                &self.color,
127                &self.corner_radii,
128                clip,
129            );
130        }
131
132        #[cfg(not(feature = "complex-color"))]
133        {
134            if self.color.a > 0.0 {
135                if self.corner_radii.top_left > 0.0 {
136                    let radius = (self.corner_radii.top_left * 2.0)
137                        / if bb.width > bb.height {
138                            bb.height
139                        } else {
140                            bb.width
141                        };
142                    ctx.d.draw_rectangle_rounded(
143                        raylib::math::Rectangle::new(bb.x, bb.y, bb.width, bb.height),
144                        radius,
145                        8,
146                        cotis_to_raylib_color(self.color),
147                    );
148                } else {
149                    ctx.d.draw_rectangle(
150                        bb.x as i32,
151                        bb.y as i32,
152                        bb.width as i32,
153                        bb.height as i32,
154                        cotis_to_raylib_color(self.color),
155                    );
156                }
157            }
158        }
159    }
160
161    fn scale_by(&mut self, factor: f32) {
162        scale_bounding_box(&mut self.info.bounding_box, factor);
163        scale_corner_radii(&mut self.corner_radii, factor);
164    }
165}
166
167// ── Text ─────────────────────────────────────────────────────────────────────
168//
169// Uses the font ID from the command; missing fonts fall back to raylib's default font.
170// With `complex-color`, draws per-glyph gradient tints.
171
172impl<'a, E> RaylibDrawable for Text<'a, E> {
173    fn draw(self: Box<Self>, ctx: &mut RaylibDrawContext<'_, '_>) {
174        let bb = self.info.bounding_box;
175        let text_str: &str = self.text.as_ref();
176
177        let fonts_arc = ctx.fonts.clone();
178        let mut guard = fonts_arc.lock().unwrap();
179
180        #[cfg(feature = "complex-color")]
181        {
182            if let Some(font) = guard.get_mut(&(self.font_id as usize)) {
183                let f = font.get_font_for_size(self.font_size as usize);
184                crate::color_paint::draw_text_color_layer(
185                    ctx.d,
186                    f,
187                    text_str,
188                    Vector2::new(bb.x, bb.y),
189                    self.font_size,
190                    self.letter_spacing,
191                    &self.color,
192                );
193            } else {
194                let c = crate::color_paint::text_color_at(&self.color, 0.0);
195                ctx.d.draw_text(
196                    text_str,
197                    bb.x as i32,
198                    bb.y as i32,
199                    self.font_size as i32,
200                    crate::color_paint::cotis_color_to_raylib(c),
201                );
202            }
203        }
204
205        #[cfg(not(feature = "complex-color"))]
206        {
207            if let Some(font) = guard.get_mut(&(self.font_id as usize)) {
208                let raylib_font = font.get_font_for_size(self.font_size as usize);
209                ctx.d.draw_text_ex(
210                    raylib_font,
211                    text_str,
212                    Vector2::new(bb.x, bb.y),
213                    self.font_size,
214                    self.letter_spacing,
215                    cotis_to_raylib_color(self.color),
216                );
217            } else {
218                ctx.d.draw_text(
219                    text_str,
220                    bb.x as i32,
221                    bb.y as i32,
222                    self.font_size as i32,
223                    cotis_to_raylib_color(self.color),
224                );
225            }
226        }
227    }
228
229    fn scale_by(&mut self, factor: f32) {
230        scale_bounding_box(&mut self.info.bounding_box, factor);
231        self.font_size *= factor;
232        self.letter_spacing *= factor;
233        self.line_height *= factor;
234    }
235}
236
237// ── Border ───────────────────────────────────────────────────────────────────
238//
239// Four edge rectangles plus corner arc rings. `width.between_children` is scaled but not drawn.
240
241impl<'a, E> RaylibDrawable for Border<'a, E> {
242    fn draw(self: Box<Self>, ctx: &mut RaylibDrawContext<'_, '_>) {
243        let bb = self.info.bounding_box;
244        let color = cotis_to_raylib_color(self.color);
245
246        if self.width.left > 0.0 {
247            ctx.d.draw_rectangle(
248                bb.x as i32,
249                (bb.y + self.corner_radii.top_left) as i32,
250                self.width.left as i32,
251                (bb.height - self.corner_radii.top_left - self.corner_radii.bottom_left) as i32,
252                color,
253            );
254        }
255        if self.width.right > 0.0 {
256            ctx.d.draw_rectangle(
257                (bb.x + bb.width - self.width.right) as i32,
258                (bb.y + self.corner_radii.top_right) as i32,
259                self.width.right as i32,
260                (bb.height - self.corner_radii.top_right - self.corner_radii.bottom_right) as i32,
261                color,
262            );
263        }
264        if self.width.top > 0.0 {
265            ctx.d.draw_rectangle(
266                (bb.x + self.corner_radii.top_left) as i32,
267                bb.y as i32,
268                (bb.width - self.corner_radii.top_left - self.corner_radii.top_right) as i32,
269                self.width.top as i32,
270                color,
271            );
272        }
273        if self.width.bottom > 0.0 {
274            ctx.d.draw_rectangle(
275                (bb.x + self.corner_radii.bottom_left) as i32,
276                (bb.y + bb.height - self.width.bottom) as i32,
277                (bb.width - self.corner_radii.bottom_left - self.corner_radii.bottom_right) as i32,
278                self.width.bottom as i32,
279                color,
280            );
281        }
282
283        if self.corner_radii.top_left > 0.0 {
284            ctx.d.draw_ring(
285                Vector2::new(
286                    bb.x + self.corner_radii.top_left,
287                    bb.y + self.corner_radii.top_left,
288                ),
289                self.corner_radii.top_left - self.width.top,
290                self.corner_radii.top_left,
291                180.0,
292                270.0,
293                10,
294                color,
295            );
296        }
297        if self.corner_radii.top_right > 0.0 {
298            ctx.d.draw_ring(
299                Vector2::new(
300                    bb.x + bb.width - self.corner_radii.top_right,
301                    bb.y + self.corner_radii.top_right,
302                ),
303                self.corner_radii.top_right - self.width.top,
304                self.corner_radii.top_right,
305                270.0,
306                360.0,
307                10,
308                color,
309            );
310        }
311        if self.corner_radii.bottom_left > 0.0 {
312            ctx.d.draw_ring(
313                Vector2::new(
314                    bb.x + self.corner_radii.bottom_left,
315                    bb.y + bb.height - self.corner_radii.bottom_left,
316                ),
317                self.corner_radii.bottom_left - self.width.bottom,
318                self.corner_radii.bottom_left,
319                90.0,
320                180.0,
321                10,
322                color,
323            );
324        }
325        if self.corner_radii.bottom_right > 0.0 {
326            ctx.d.draw_ring(
327                Vector2::new(
328                    bb.x + bb.width - self.corner_radii.bottom_right,
329                    bb.y + bb.height - self.corner_radii.bottom_right,
330                ),
331                self.corner_radii.bottom_right - self.width.bottom,
332                self.corner_radii.bottom_right,
333                0.0,
334                90.0,
335                10,
336                color,
337            );
338        }
339    }
340
341    fn scale_by(&mut self, factor: f32) {
342        scale_bounding_box(&mut self.info.bounding_box, factor);
343        scale_corner_radii(&mut self.corner_radii, factor);
344        self.width.left *= factor;
345        self.width.right *= factor;
346        self.width.top *= factor;
347        self.width.bottom *= factor;
348        self.width.between_children *= factor;
349    }
350}
351
352// ── Image ────────────────────────────────────────────────────────────────────
353//
354// Textures are scaled horizontally to `bounding_box.width`. Requires preload via
355// [`RaylibDrawable::preload_generic_images`] or [`RaylibRender::translate_generic_image`].
356
357impl<'a, I: GenericCompatibleImage, E> RaylibDrawable for Image<'a, I, E> {
358    fn draw(self: Box<Self>, ctx: &mut RaylibDrawContext<'_, '_>) {
359        GenericCompatibleImage::load_texture(self.data.as_ref(), ctx);
360        let texture = GenericCompatibleImage::get_texture(self.data.as_ref(), ctx);
361
362        let bb = self.info.bounding_box;
363        #[cfg(feature = "complex-color")]
364        {
365            let clip = ctx.clip_stack.last().copied();
366            crate::color_paint::draw_color_layer_in_rect(
367                ctx.d,
368                bb.x,
369                bb.y,
370                bb.width,
371                bb.height,
372                &self.background_color,
373                &self.corner_radii,
374                clip,
375            );
376        }
377
378        #[cfg(not(feature = "complex-color"))]
379        {
380            if self.background_color.a > 0.0 {
381                ctx.d.draw_rectangle(
382                    bb.x as i32,
383                    bb.y as i32,
384                    bb.width as i32,
385                    bb.height as i32,
386                    cotis_to_raylib_color(self.background_color),
387                );
388            }
389        }
390
391        ctx.d.draw_texture_ex(
392            &*texture,
393            Vector2::new(bb.x, bb.y),
394            0.0,
395            bb.width / texture.width as f32,
396            raylib::color::Color::WHITE,
397        );
398    }
399
400    fn scale_by(&mut self, factor: f32) {
401        scale_bounding_box(&mut self.info.bounding_box, factor);
402        scale_corner_radii(&mut self.corner_radii, factor);
403    }
404
405    fn preload_generic_images(&self, render: &mut RaylibRender) {
406        render.translate_generic_image(self.data.as_ref());
407    }
408}
409
410// ── ClipStart ────────────────────────────────────────────────────────────────
411//
412// Pushes an intersected scissor rect onto `clip_stack` and enables raylib scissor mode.
413
414impl<'a, E> RaylibDrawable for ClipStart<'a, E> {
415    fn draw(self: Box<Self>, ctx: &mut RaylibDrawContext<'_, '_>) {
416        let bb = self.info.bounding_box;
417        let clip_rect = (bb.x, bb.y, bb.width.max(0.0), bb.height.max(0.0));
418        let effective = if let Some(parent) = ctx.clip_stack.last().copied() {
419            intersect_clip(parent, clip_rect)
420        } else {
421            clip_rect
422        };
423        ctx.clip_stack.push(effective);
424        unsafe {
425            raylib::ffi::BeginScissorMode(
426                effective.0 as i32,
427                effective.1 as i32,
428                effective.2 as i32,
429                effective.3 as i32,
430            );
431        }
432    }
433
434    fn scale_by(&mut self, factor: f32) {
435        scale_bounding_box(&mut self.info.bounding_box, factor);
436        scale_corner_radii(&mut self.corner_radii, factor);
437    }
438}
439
440// ── ClipEnd ──────────────────────────────────────────────────────────────────
441//
442// Pops the clip stack and restores the parent scissor rect if any.
443
444impl RaylibDrawable for ClipEnd {
445    fn draw(self: Box<Self>, ctx: &mut RaylibDrawContext<'_, '_>) {
446        if ctx.clip_stack.pop().is_some() {
447            unsafe {
448                raylib::ffi::EndScissorMode();
449                if let Some(prev) = ctx.clip_stack.last().copied() {
450                    raylib::ffi::BeginScissorMode(
451                        prev.0 as i32,
452                        prev.1 as i32,
453                        prev.2 as i32,
454                        prev.3 as i32,
455                    );
456                }
457            }
458        }
459    }
460
461    fn scale_by(&mut self, _factor: f32) {}
462}
463
464// ── RenderOutputToDrawables ───────────────────────────────────────────────────
465
466// ── RenderTList ───────────────────────────────────────────────────────────────
467//
468// These two impls allow `RenderTList` heterogeneous command lists to be drawn by raylib.
469// The recursive case handles `RenderTList<T, L>` where `L` itself implements `RaylibDrawable`
470// (i.e. a non-terminal tail), and the base case handles `RenderTList<T, ()>` (terminal tail).
471
472impl<T: RaylibDrawable, L: RaylibDrawable + IsRenderList> RaylibDrawable for RenderTList<T, L> {
473    fn draw(self: Box<Self>, ctx: &mut RaylibDrawContext<'_, '_>) {
474        match *self {
475            RenderTList::Type(t) => Box::new(t).draw(ctx),
476            RenderTList::List(l) => Box::new(l).draw(ctx),
477        }
478    }
479
480    fn scale_by(&mut self, factor: f32) {
481        match self {
482            RenderTList::Type(t) => t.scale_by(factor),
483            RenderTList::List(l) => l.scale_by(factor),
484        }
485    }
486
487    fn preload_generic_images(&self, render: &mut RaylibRender) {
488        match self {
489            RenderTList::Type(t) => t.preload_generic_images(render),
490            RenderTList::List(l) => l.preload_generic_images(render),
491        }
492    }
493}
494
495impl<T: RaylibDrawable> RaylibDrawable for RenderTList<T, ()> {
496    fn draw(self: Box<Self>, ctx: &mut RaylibDrawContext<'_, '_>) {
497        match *self {
498            RenderTList::Type(t) => Box::new(t).draw(ctx),
499            RenderTList::List(()) => {}
500        }
501    }
502
503    fn scale_by(&mut self, factor: f32) {
504        match self {
505            RenderTList::Type(t) => t.scale_by(factor),
506            RenderTList::List(()) => {}
507        }
508    }
509
510    fn preload_generic_images(&self, render: &mut RaylibRender) {
511        match self {
512            RenderTList::Type(t) => t.preload_generic_images(render),
513            RenderTList::List(()) => {}
514        }
515    }
516}
517
518// TryFromRenderOutput impls for cotis's concrete types live in cotis-layout,
519// since TryFromRenderOutput is defined there (local trait on foreign types = allowed).
520// Custom types like CircleDrawable implement TryFromRenderOutput directly where they
521// are defined, since they are local types (local type implementing foreign trait = allowed).