1use tiny_skia::{Paint, PathBuilder, Pixmap, PixmapPaint, Stroke, Transform};
11use truce_core::cast::len_u32;
12use truce_gui_types::render::{ImageId, RenderBackend};
13use truce_gui_types::theme::Color;
14use truce_gui_types::to_physical_px;
15
16use crate::ColorExt;
17
18pub struct CpuBackend {
23 pixmap: Pixmap,
24 scale: f32,
28 images: Vec<Option<Pixmap>>,
30}
31
32impl CpuBackend {
33 #[must_use]
39 pub fn new(logical_w: u32, logical_h: u32, scale: f32) -> Option<Self> {
40 let scale = scale.max(0.0);
41 let phys_w = to_physical_px(logical_w, f64::from(scale));
42 let phys_h = to_physical_px(logical_h, f64::from(scale));
43 Pixmap::new(phys_w, phys_h).map(|pixmap| Self {
44 pixmap,
45 scale,
46 images: Vec::new(),
47 })
48 }
49
50 pub fn resize(&mut self, logical_w: u32, logical_h: u32, scale: f32) -> bool {
55 let scale = scale.max(0.0);
56 let phys_w = to_physical_px(logical_w, f64::from(scale));
57 let phys_h = to_physical_px(logical_h, f64::from(scale));
58 if phys_w == self.pixmap.width() && phys_h == self.pixmap.height() {
59 self.scale = scale;
60 return false;
61 }
62 match Pixmap::new(phys_w, phys_h) {
63 Some(pm) => {
64 self.pixmap = pm;
65 self.scale = scale;
66 true
67 }
68 None => false,
69 }
70 }
71
72 #[must_use]
74 pub fn data(&self) -> &[u8] {
75 self.pixmap.data()
76 }
77
78 #[must_use]
80 pub fn width(&self) -> u32 {
81 self.pixmap.width()
82 }
83
84 #[must_use]
86 pub fn height(&self) -> u32 {
87 self.pixmap.height()
88 }
89
90 #[must_use]
92 pub fn scale(&self) -> f32 {
93 self.scale
94 }
95}
96
97#[allow(clippy::many_single_char_names)]
104impl RenderBackend for CpuBackend {
105 fn clear(&mut self, color: Color) {
106 self.pixmap.fill(color.to_skia());
107 }
108
109 fn fill_rect(&mut self, x: f32, y: f32, w: f32, h: f32, color: Color) {
110 let s = self.scale;
111 let Some(rect) = tiny_skia::Rect::from_xywh(x * s, y * s, w * s, h * s) else {
112 return;
113 };
114 let mut paint = Paint::default();
115 paint.set_color(color.to_skia());
116 paint.anti_alias = true;
117 self.pixmap
118 .fill_rect(rect, &paint, Transform::identity(), None);
119 }
120
121 fn fill_circle(&mut self, cx: f32, cy: f32, radius: f32, color: Color) {
122 let s = self.scale;
123 let mut pb = PathBuilder::new();
124 pb.push_circle(cx * s, cy * s, radius * s);
125 let Some(path) = pb.finish() else {
126 return;
127 };
128 let mut paint = Paint::default();
129 paint.set_color(color.to_skia());
130 paint.anti_alias = true;
131 self.pixmap.fill_path(
132 &path,
133 &paint,
134 tiny_skia::FillRule::Winding,
135 Transform::identity(),
136 None,
137 );
138 }
139
140 fn stroke_circle(&mut self, cx: f32, cy: f32, radius: f32, color: Color, width: f32) {
141 let s = self.scale;
142 let mut pb = PathBuilder::new();
143 pb.push_circle(cx * s, cy * s, radius * s);
144 let Some(path) = pb.finish() else {
145 return;
146 };
147 let mut paint = Paint::default();
148 paint.set_color(color.to_skia());
149 paint.anti_alias = true;
150 let stroke = Stroke {
151 width: width * s,
152 ..Stroke::default()
153 };
154 self.pixmap
155 .stroke_path(&path, &paint, &stroke, Transform::identity(), None);
156 }
157
158 #[allow(clippy::cast_precision_loss)]
159 fn stroke_arc(
160 &mut self,
161 cx: f32,
162 cy: f32,
163 radius: f32,
164 start_angle: f32,
165 end_angle: f32,
166 color: Color,
167 width: f32,
168 ) {
169 let s = self.scale;
170 let segments = 64;
171 let mut pb = PathBuilder::new();
172 let angle_range = end_angle - start_angle;
173 let step = angle_range / segments as f32;
174
175 for i in 0..=segments {
176 let angle = start_angle + step * i as f32;
177 let x = cx * s + radius * s * angle.cos();
178 let y = cy * s + radius * s * angle.sin();
179 if i == 0 {
180 pb.move_to(x, y);
181 } else {
182 pb.line_to(x, y);
183 }
184 }
185
186 let Some(path) = pb.finish() else {
187 return;
188 };
189 let mut paint = Paint::default();
190 paint.set_color(color.to_skia());
191 paint.anti_alias = true;
192 let stroke = Stroke {
193 width: width * s,
194 line_cap: tiny_skia::LineCap::Round,
195 ..Stroke::default()
196 };
197 self.pixmap
198 .stroke_path(&path, &paint, &stroke, Transform::identity(), None);
199 }
200
201 fn draw_line(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, color: Color, width: f32) {
202 let s = self.scale;
203 let mut pb = PathBuilder::new();
204 pb.move_to(x1 * s, y1 * s);
205 pb.line_to(x2 * s, y2 * s);
206 let Some(path) = pb.finish() else {
207 return;
208 };
209 let mut paint = Paint::default();
210 paint.set_color(color.to_skia());
211 paint.anti_alias = true;
212 let stroke = Stroke {
213 width: width * s,
214 line_cap: tiny_skia::LineCap::Round,
215 ..Stroke::default()
216 };
217 self.pixmap
218 .stroke_path(&path, &paint, &stroke, Transform::identity(), None);
219 }
220
221 fn draw_text(&mut self, text: &str, x: f32, y: f32, size: f32, color: Color) {
222 let s = self.scale;
223 let w = self.pixmap.width();
224 let h = self.pixmap.height();
225 crate::font::draw_text_fontdue(
226 self.pixmap.data_mut(),
227 w,
228 h,
229 text,
230 x * s,
231 y * s,
232 size * s,
233 color.r,
234 color.g,
235 color.b,
236 color.a,
237 );
238 }
239
240 fn text_width(&self, text: &str, size: f32) -> f32 {
241 let s = self.scale;
242 crate::font::text_width_fontdue(text, size * s) / s
243 }
244
245 fn register_image(&mut self, rgba: &[u8], width: u32, height: u32) -> ImageId {
246 let Some(mut pm) = Pixmap::new(width, height) else {
247 return ImageId::INVALID;
248 };
249 let expected = (width as usize) * (height as usize) * 4;
250 if rgba.len() < expected {
251 return ImageId::INVALID;
252 }
253 pm.data_mut()[..expected].copy_from_slice(&rgba[..expected]);
254
255 if let Some(slot) = self
256 .images
257 .iter_mut()
258 .enumerate()
259 .find(|(_, s)| s.is_none())
260 {
261 *slot.1 = Some(pm);
262 return ImageId(len_u32(slot.0));
263 }
264 let id = len_u32(self.images.len());
265 self.images.push(Some(pm));
266 ImageId(id)
267 }
268
269 fn unregister_image(&mut self, id: ImageId) {
270 if let Some(slot) = self.images.get_mut(id.0 as usize) {
271 *slot = None;
272 }
273 }
274
275 #[allow(clippy::cast_precision_loss)]
278 fn draw_image(&mut self, id: ImageId, x: f32, y: f32, w: f32, h: f32) {
279 let s = self.scale;
280 let Some(pm) = self.images.get(id.0 as usize).and_then(|s| s.as_ref()) else {
281 return;
282 };
283 let sx = (w * s) / pm.width() as f32;
284 let sy = (h * s) / pm.height() as f32;
285 let transform = Transform::from_scale(sx, sy).post_translate(x * s, y * s);
286 let paint = PixmapPaint::default();
287 self.pixmap
288 .draw_pixmap(0, 0, pm.as_ref(), &paint, transform, None);
289 }
290}