Skip to main content

oxide_sdk/
draw.rs

1//! Higher-level drawing API inspired by GPUI's rendering model.
2//!
3//! This module provides ergonomic types and an immediate-mode [`Canvas`] facade
4//! that wraps the low-level `canvas_*` functions. Guest apps can choose between
5//! the raw functions (maximum control) and these helpers (less boilerplate).
6//!
7//! # Example
8//!
9//! ```rust,ignore
10//! use oxide_sdk::draw::*;
11//!
12//! let c = Canvas::new();
13//! c.clear(Color::rgb(30, 30, 46));
14//! c.fill_rect(Rect::new(10.0, 10.0, 200.0, 100.0), Color::rgb(80, 120, 200));
15//! c.fill_circle(Point2D::new(300.0, 200.0), 50.0, Color::rgba(200, 100, 150, 200));
16//! c.text("Hello!", Point2D::new(20.0, 30.0), 24.0, Color::WHITE);
17//! c.line(Point2D::new(0.0, 0.0), Point2D::new(400.0, 300.0), 2.0, Color::YELLOW);
18//! ```
19
20/// sRGB color with alpha.
21#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
22pub struct Color {
23    pub r: u8,
24    pub g: u8,
25    pub b: u8,
26    pub a: u8,
27}
28
29impl Color {
30    /// Opaque color from RGB channels.
31    pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
32        Self { r, g, b, a: 255 }
33    }
34
35    /// Color with explicit alpha.
36    pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
37        Self { r, g, b, a }
38    }
39
40    /// Parse a 24-bit hex value (e.g. `0xFF8040`) into an opaque color.
41    pub const fn hex(hex: u32) -> Self {
42        Self {
43            r: ((hex >> 16) & 0xFF) as u8,
44            g: ((hex >> 8) & 0xFF) as u8,
45            b: (hex & 0xFF) as u8,
46            a: 255,
47        }
48    }
49
50    /// Return this color with a different alpha value.
51    #[must_use]
52    pub const fn with_alpha(self, a: u8) -> Self {
53        Self { a, ..self }
54    }
55
56    pub const WHITE: Self = Self::rgb(255, 255, 255);
57    pub const BLACK: Self = Self::rgb(0, 0, 0);
58    pub const RED: Self = Self::rgb(255, 0, 0);
59    pub const GREEN: Self = Self::rgb(0, 255, 0);
60    pub const BLUE: Self = Self::rgb(0, 0, 255);
61    pub const YELLOW: Self = Self::rgb(255, 255, 0);
62    pub const CYAN: Self = Self::rgb(0, 255, 255);
63    pub const MAGENTA: Self = Self::rgb(255, 0, 255);
64    pub const TRANSPARENT: Self = Self::rgba(0, 0, 0, 0);
65}
66
67impl Default for Color {
68    fn default() -> Self {
69        Self::WHITE
70    }
71}
72
73/// A 2D point in canvas coordinates.
74#[derive(Clone, Copy, Debug, Default, PartialEq)]
75pub struct Point2D {
76    pub x: f32,
77    pub y: f32,
78}
79
80impl Point2D {
81    pub const fn new(x: f32, y: f32) -> Self {
82        Self { x, y }
83    }
84
85    pub const ZERO: Self = Self::new(0.0, 0.0);
86}
87
88/// An axis-aligned rectangle in canvas coordinates.
89#[derive(Clone, Copy, Debug, Default, PartialEq)]
90pub struct Rect {
91    pub x: f32,
92    pub y: f32,
93    pub w: f32,
94    pub h: f32,
95}
96
97impl Rect {
98    pub const fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
99        Self { x, y, w, h }
100    }
101
102    /// Create from origin point and size.
103    pub const fn from_point_size(origin: Point2D, w: f32, h: f32) -> Self {
104        Self {
105            x: origin.x,
106            y: origin.y,
107            w,
108            h,
109        }
110    }
111
112    /// True if the point `(px, py)` is inside this rectangle (half-open: inclusive on
113    /// the near edge, exclusive on the far edge).
114    pub fn contains(&self, px: f32, py: f32) -> bool {
115        px >= self.x && py >= self.y && px < self.x + self.w && py < self.y + self.h
116    }
117
118    pub fn origin(&self) -> Point2D {
119        Point2D::new(self.x, self.y)
120    }
121
122    pub fn center(&self) -> Point2D {
123        Point2D::new(self.x + self.w / 2.0, self.y + self.h / 2.0)
124    }
125}
126
127/// A gradient color stop at a position along the gradient axis.
128#[derive(Clone, Copy, Debug, PartialEq)]
129pub struct GradientStop {
130    pub offset: f32,
131    pub color: Color,
132}
133
134impl GradientStop {
135    pub const fn new(offset: f32, color: Color) -> Self {
136        Self { offset, color }
137    }
138}
139
140/// Immediate-mode canvas facade that wraps the low-level drawing functions.
141///
142/// All methods paint immediately (no retained scene graph). Create one per
143/// frame or per `start_app` call and issue draw commands through it.
144pub struct Canvas;
145
146impl Canvas {
147    /// Create a new canvas handle. This is zero-cost — no allocation occurs.
148    pub fn new() -> Self {
149        Self
150    }
151
152    /// Clear the canvas with a solid color.
153    pub fn clear(&self, color: Color) {
154        crate::canvas_clear(color.r, color.g, color.b, color.a);
155    }
156
157    /// Draw a filled rectangle.
158    pub fn fill_rect(&self, rect: Rect, color: Color) {
159        crate::canvas_rect(
160            rect.x, rect.y, rect.w, rect.h, color.r, color.g, color.b, color.a,
161        );
162    }
163
164    /// Draw a filled rounded rectangle with uniform corner radius.
165    pub fn fill_rounded_rect(&self, rect: Rect, radius: f32, color: Color) {
166        crate::canvas_rounded_rect(
167            rect.x, rect.y, rect.w, rect.h, radius, color.r, color.g, color.b, color.a,
168        );
169    }
170
171    /// Draw a filled circle.
172    pub fn fill_circle(&self, center: Point2D, radius: f32, color: Color) {
173        crate::canvas_circle(
174            center.x, center.y, radius, color.r, color.g, color.b, color.a,
175        );
176    }
177
178    /// Draw a circular arc stroke from `start_angle` to `end_angle` (radians).
179    pub fn arc(
180        &self,
181        center: Point2D,
182        radius: f32,
183        start_angle: f32,
184        end_angle: f32,
185        thickness: f32,
186        color: Color,
187    ) {
188        crate::canvas_arc(
189            center.x,
190            center.y,
191            radius,
192            start_angle,
193            end_angle,
194            color.r,
195            color.g,
196            color.b,
197            color.a,
198            thickness,
199        );
200    }
201
202    /// Draw a cubic Bézier curve stroke.
203    pub fn bezier(
204        &self,
205        from: Point2D,
206        ctrl1: Point2D,
207        ctrl2: Point2D,
208        to: Point2D,
209        thickness: f32,
210        color: Color,
211    ) {
212        crate::canvas_bezier(
213            from.x, from.y, ctrl1.x, ctrl1.y, ctrl2.x, ctrl2.y, to.x, to.y, color.r, color.g,
214            color.b, color.a, thickness,
215        );
216    }
217
218    /// Draw text at a position.
219    pub fn text(&self, text: &str, pos: Point2D, size: f32, color: Color) {
220        crate::canvas_text(pos.x, pos.y, size, color.r, color.g, color.b, color.a, text);
221    }
222
223    /// Draw a line between two points.
224    pub fn line(&self, from: Point2D, to: Point2D, thickness: f32, color: Color) {
225        crate::canvas_line(
226            from.x, from.y, to.x, to.y, color.r, color.g, color.b, color.a, thickness,
227        );
228    }
229
230    /// Draw an image from encoded bytes (PNG, JPEG, GIF, WebP).
231    pub fn image(&self, rect: Rect, data: &[u8]) {
232        crate::canvas_image(rect.x, rect.y, rect.w, rect.h, data);
233    }
234
235    /// Fill a rectangle with a linear gradient.
236    pub fn linear_gradient(&self, rect: Rect, stops: &[GradientStop]) {
237        let raw: Vec<(f32, u8, u8, u8, u8)> = stops
238            .iter()
239            .map(|s| (s.offset, s.color.r, s.color.g, s.color.b, s.color.a))
240            .collect();
241        crate::canvas_gradient(
242            rect.x,
243            rect.y,
244            rect.w,
245            rect.h,
246            crate::GRADIENT_LINEAR,
247            rect.x,
248            rect.y,
249            rect.x + rect.w,
250            rect.y + rect.h,
251            &raw,
252        );
253    }
254
255    /// Fill a rectangle with a radial gradient.
256    pub fn radial_gradient(&self, rect: Rect, stops: &[GradientStop]) {
257        let raw: Vec<(f32, u8, u8, u8, u8)> = stops
258            .iter()
259            .map(|s| (s.offset, s.color.r, s.color.g, s.color.b, s.color.a))
260            .collect();
261        let cx = rect.x + rect.w / 2.0;
262        let cy = rect.y + rect.h / 2.0;
263        let r = rect.w.max(rect.h) / 2.0;
264        crate::canvas_gradient(
265            rect.x,
266            rect.y,
267            rect.w,
268            rect.h,
269            crate::GRADIENT_RADIAL,
270            cx,
271            cy,
272            0.0,
273            r,
274            &raw,
275        );
276    }
277
278    /// Push the current transform/clip/opacity state.
279    pub fn save(&self) {
280        crate::canvas_save();
281    }
282
283    /// Restore the most recently saved state.
284    pub fn restore(&self) {
285        crate::canvas_restore();
286    }
287
288    /// Apply a 2D translation to subsequent draw commands.
289    pub fn translate(&self, tx: f32, ty: f32) {
290        crate::canvas_transform(1.0, 0.0, 0.0, 1.0, tx, ty);
291    }
292
293    /// Apply a 2D rotation (radians) to subsequent draw commands.
294    pub fn rotate(&self, angle: f32) {
295        let (s, c) = (angle.sin(), angle.cos());
296        crate::canvas_transform(c, s, -s, c, 0.0, 0.0);
297    }
298
299    /// Apply a uniform scale to subsequent draw commands.
300    pub fn scale(&self, sx: f32, sy: f32) {
301        crate::canvas_transform(sx, 0.0, 0.0, sy, 0.0, 0.0);
302    }
303
304    /// Apply a full 2D affine transform.
305    pub fn transform(&self, a: f32, b: f32, c: f32, d: f32, tx: f32, ty: f32) {
306        crate::canvas_transform(a, b, c, d, tx, ty);
307    }
308
309    /// Intersect the current clip with a rectangle.
310    pub fn clip(&self, rect: Rect) {
311        crate::canvas_clip(rect.x, rect.y, rect.w, rect.h);
312    }
313
314    /// Set layer opacity (0.0–1.0) for subsequent draw commands.
315    pub fn set_opacity(&self, alpha: f32) {
316        crate::canvas_opacity(alpha);
317    }
318
319    /// Get the canvas dimensions in pixels.
320    pub fn dimensions(&self) -> (u32, u32) {
321        crate::canvas_dimensions()
322    }
323
324    /// Get the canvas width in pixels.
325    pub fn width(&self) -> u32 {
326        self.dimensions().0
327    }
328
329    /// Get the canvas height in pixels.
330    pub fn height(&self) -> u32 {
331        self.dimensions().1
332    }
333}
334
335impl Default for Canvas {
336    fn default() -> Self {
337        Self::new()
338    }
339}