1use std::{ops::Range, rc::Rc};
2
3use cranpose_foundation::PointerEvent;
4use cranpose_ui::{Brush, DrawCommand, DrawCommandFn, LayoutNodeData, ModifierNodeSlices};
5use cranpose_ui_graphics::{
6 BlendMode, Color, ColorFilter, CommandRecording, CompositingStrategy, CornerRadii,
7 DrawPrimitive, GraphicsLayer, Point, RoundedCornerShape, Size,
8};
9
10use crate::layer_transform::{layer_scale_x, layer_scale_y, layer_uniform_scale};
11
12pub struct NodeStyle {
13 pub padding: cranpose_ui_graphics::EdgeInsets,
14 pub background: Option<Color>,
15 pub click_actions: Vec<Rc<dyn Fn(Point)>>,
16 pub shape: Option<RoundedCornerShape>,
17 pub pointer_inputs: Vec<Rc<dyn Fn(PointerEvent)>>,
18 pub draw_commands: Vec<DrawCommand>,
19 pub graphics_layer: Option<GraphicsLayer>,
20 pub clip_to_bounds: bool,
21}
22
23impl NodeStyle {
24 pub fn from_layout_node(data: &LayoutNodeData) -> Self {
25 let resolved = data.resolved_modifiers;
26 let slices: &ModifierNodeSlices = data.modifier_slices();
27 let pointer_inputs = slices.pointer_inputs().to_vec();
28
29 Self {
30 padding: resolved.padding(),
31 background: None,
32 click_actions: slices.click_handlers().to_vec(),
33 shape: None,
34 pointer_inputs,
35 draw_commands: slices.draw_commands().to_vec(),
36 graphics_layer: slices.graphics_layer(),
37 clip_to_bounds: slices.clip_to_bounds(),
38 }
39 }
40}
41
42pub fn combine_layers(
43 current: GraphicsLayer,
44 modifier_layer: Option<GraphicsLayer>,
45) -> GraphicsLayer {
46 if let Some(layer) = modifier_layer {
47 GraphicsLayer {
48 alpha: (current.alpha * layer.alpha).clamp(0.0, 1.0),
49 scale: current.scale * layer.scale,
50 scale_x: current.scale_x * layer.scale_x,
51 scale_y: current.scale_y * layer.scale_y,
52 rotation_x: current.rotation_x + layer.rotation_x,
53 rotation_y: current.rotation_y + layer.rotation_y,
54 rotation_z: current.rotation_z + layer.rotation_z,
55 camera_distance: layer.camera_distance,
56 transform_origin: layer.transform_origin,
57 translation_x: current.translation_x + layer.translation_x,
58 translation_y: current.translation_y + layer.translation_y,
59 shadow_elevation: layer.shadow_elevation,
60 ambient_shadow_color: layer.ambient_shadow_color,
61 spot_shadow_color: layer.spot_shadow_color,
62 shape: layer.shape,
63 clip: current.clip || layer.clip,
64 color_filter: compose_color_filters(current.color_filter, layer.color_filter),
65 compositing_strategy: layer.compositing_strategy,
66 blend_mode: layer.blend_mode,
67 render_effect: layer.render_effect,
68 backdrop_effect: layer.backdrop_effect,
69 }
70 } else {
71 GraphicsLayer {
72 compositing_strategy: CompositingStrategy::Auto,
73 blend_mode: BlendMode::SrcOver,
74 render_effect: None,
75 backdrop_effect: None,
76 ..current
77 }
78 }
79}
80
81pub use crate::graph::quad_bounds;
82
83pub fn apply_layer_to_color(color: Color, layer: &GraphicsLayer) -> Color {
93 let color = color.srgb_8bit();
94 apply_color_filter_to_color(
95 Color(
96 color.0,
97 color.1,
98 color.2,
99 (color.3 * layer.alpha).clamp(0.0, 1.0),
100 ),
101 layer.color_filter,
102 )
103}
104
105fn apply_color_filter_to_color(color: Color, filter: Option<ColorFilter>) -> Color {
106 match filter {
107 Some(filter) => {
108 let [r, g, b, a] = filter.apply_rgba([color.0, color.1, color.2, color.3]);
109 Color(r, g, b, a)
110 }
111 None => color,
112 }
113}
114
115pub fn compose_color_filters(
116 base: Option<ColorFilter>,
117 overlay: Option<ColorFilter>,
118) -> Option<ColorFilter> {
119 match (base, overlay) {
120 (None, None) => None,
121 (Some(filter), None) | (None, Some(filter)) => Some(filter),
122 (Some(filter), Some(next)) => Some(filter.compose(next)),
123 }
124}
125
126pub fn apply_layer_to_brush(brush: Brush, layer: &GraphicsLayer) -> Brush {
127 if layer.alpha == 1.0
128 && layer.color_filter.is_none()
129 && layer_scale_x(layer) == 1.0
130 && layer_scale_y(layer) == 1.0
131 {
132 return map_brush_colors(brush, Color::srgb_8bit);
133 }
134 map_brush_colors(scale_brush_geometry(brush, layer), |color| {
135 apply_layer_to_color(color, layer)
136 })
137}
138
139fn map_brush_colors(brush: Brush, paint: impl Fn(Color) -> Color) -> Brush {
140 match brush {
141 Brush::Solid(color) => Brush::solid(paint(color)),
142 Brush::LinearGradient {
143 colors,
144 stops,
145 start,
146 end,
147 tile_mode,
148 } => Brush::LinearGradient {
149 colors: colors.into_iter().map(paint).collect(),
150 stops,
151 start,
152 end,
153 tile_mode,
154 },
155 Brush::RadialGradient {
156 colors,
157 stops,
158 center,
159 radius,
160 tile_mode,
161 } => Brush::RadialGradient {
162 colors: colors.into_iter().map(paint).collect(),
163 stops,
164 center,
165 radius,
166 tile_mode,
167 },
168 Brush::SweepGradient {
169 colors,
170 stops,
171 center,
172 } => Brush::SweepGradient {
173 colors: colors.into_iter().map(paint).collect(),
174 stops,
175 center,
176 },
177 }
178}
179
180fn scale_brush_geometry(brush: Brush, layer: &GraphicsLayer) -> Brush {
181 let scale_x = layer_scale_x(layer);
182 let scale_y = layer_scale_y(layer);
183 let uniform_scale = layer_uniform_scale(layer);
184
185 match brush {
186 Brush::Solid(color) => Brush::Solid(color),
187 Brush::LinearGradient {
188 colors,
189 stops,
190 mut start,
191 mut end,
192 tile_mode,
193 } => {
194 start.x *= scale_x;
195 start.y *= scale_y;
196 end.x *= scale_x;
197 end.y *= scale_y;
198 Brush::LinearGradient {
199 colors,
200 stops,
201 start,
202 end,
203 tile_mode,
204 }
205 }
206 Brush::RadialGradient {
207 colors,
208 stops,
209 mut center,
210 mut radius,
211 tile_mode,
212 } => {
213 center.x *= scale_x;
214 center.y *= scale_y;
215 radius *= uniform_scale;
216 Brush::RadialGradient {
217 colors,
218 stops,
219 center,
220 radius,
221 tile_mode,
222 }
223 }
224 Brush::SweepGradient {
225 colors,
226 stops,
227 mut center,
228 } => {
229 center.x *= scale_x;
230 center.y *= scale_y;
231 Brush::SweepGradient {
232 colors,
233 stops,
234 center,
235 }
236 }
237 }
238}
239
240#[derive(Clone, Debug, PartialEq)]
248pub enum ResolvedBrush {
249 Solid(Color),
250 Other(Brush),
252}
253
254impl ResolvedBrush {
255 pub fn from_brush(brush: Brush) -> Self {
256 match brush {
257 Brush::Solid(color) => Self::Solid(color),
258 other => Self::Other(other),
259 }
260 }
261
262 pub fn into_brush(self) -> Brush {
265 match self {
266 Self::Solid(color) => Brush::Solid(color),
267 Self::Other(brush) => brush,
268 }
269 }
270}
271
272pub fn resolve_layer_brush(brush: &Brush, layer: &GraphicsLayer) -> ResolvedBrush {
277 match brush {
278 Brush::Solid(color) => {
279 if layer.alpha == 1.0
280 && layer.color_filter.is_none()
281 && layer_scale_x(layer) == 1.0
282 && layer_scale_y(layer) == 1.0
283 {
284 ResolvedBrush::Solid(*color)
285 } else {
286 ResolvedBrush::Solid(apply_layer_to_color(*color, layer))
287 }
288 }
289 other => ResolvedBrush::Other(apply_layer_to_brush(other.clone(), layer)),
290 }
291}
292
293pub fn scale_corner_radii(radii: CornerRadii, scale: f32) -> CornerRadii {
294 CornerRadii {
295 top_left: radii.top_left * scale,
296 top_right: radii.top_right * scale,
297 bottom_right: radii.bottom_right * scale,
298 bottom_left: radii.bottom_left * scale,
299 }
300}
301
302#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
303pub enum DrawPlacement {
304 Behind,
305 Overlay,
306}
307
308pub fn primitives_for_placement(
309 command: &DrawCommand,
310 placement: DrawPlacement,
311 size: Size,
312) -> Vec<DrawPrimitive> {
313 recording_for_placement_reusing(command, placement, size, CommandRecording::default())
314 .map(|(recording, segments)| recording.primitives(segments).collect())
315 .unwrap_or_default()
316}
317
318pub fn recording_for_placement_reusing(
325 command: &DrawCommand,
326 placement: DrawPlacement,
327 size: Size,
328 storage: CommandRecording,
329) -> Option<(CommandRecording, Range<u32>)> {
330 let record = move |func: &DrawCommandFn| {
331 let mut scope = cranpose_ui::command_draw_scope_reusing(size, storage);
332 func(&mut scope);
333 scope.finish()
334 };
335 match (placement, command) {
336 (DrawPlacement::Behind, DrawCommand::Behind(func))
337 | (DrawPlacement::Overlay, DrawCommand::Overlay(func)) => {
338 let recording = record(func);
339 let segments = recording.all_segments();
340 Some((recording, segments))
341 }
342 (_, DrawCommand::WithContent(func)) => {
343 let recording = record(func);
344 let segments = recording.content_split(placement == DrawPlacement::Behind);
345 Some((recording, segments))
346 }
347 _ => None,
348 }
349}
350
351#[cfg(test)]
352mod tests {
353 use cranpose_ui_graphics::{DrawScope, DrawScopeDefault, Rect};
354
355 use super::*;
356
357 fn recorded_command(record: impl Fn(&mut dyn DrawScope) + 'static) -> DrawCommandFn {
358 Rc::new(move |scope: &mut DrawScopeDefault| record(scope))
359 }
360
361 fn rect_at(x: f32) -> Rect {
362 Rect {
363 x,
364 y: 0.0,
365 width: 1.0,
366 height: 1.0,
367 }
368 }
369
370 fn rect_xs(primitives: &[DrawPrimitive]) -> Vec<f32> {
371 primitives
372 .iter()
373 .map(|primitive| match primitive {
374 DrawPrimitive::Rect { rect, .. } => rect.x,
375 other => panic!("unexpected primitive {other:?}"),
376 })
377 .collect()
378 }
379
380 #[test]
381 fn marker_free_recording_passes_through() {
382 let command = DrawCommand::Behind(recorded_command(|scope| {
383 scope.draw_rect_at(rect_at(1.0), Brush::solid(Color::WHITE));
384 scope.draw_rect_at(rect_at(2.0), Brush::solid(Color::WHITE));
385 }));
386 let out = primitives_for_placement(&command, DrawPlacement::Behind, Size::new(10.0, 10.0));
387 assert_eq!(rect_xs(&out), [1.0, 2.0]);
388 }
389
390 #[test]
391 fn recorded_markers_still_split_content_placements() {
392 let with_content = recorded_command(|scope| {
393 scope.draw_rect_at(rect_at(1.0), Brush::solid(Color::WHITE));
394 scope.draw_content();
395 scope.draw_rect_at(rect_at(2.0), Brush::solid(Color::WHITE));
396 });
397 let command = DrawCommand::WithContent(with_content);
398 let size = Size::new(10.0, 10.0);
399 let behind = primitives_for_placement(&command, DrawPlacement::Behind, size);
400 assert_eq!(rect_xs(&behind), [1.0]);
401 let overlay = primitives_for_placement(&command, DrawPlacement::Overlay, size);
402 assert_eq!(rect_xs(&overlay), [2.0]);
403 }
404
405 #[test]
406 fn reused_storage_records_identically_to_fresh() {
407 let command = DrawCommand::WithContent(recorded_command(|scope| {
408 scope.draw_rect_at(rect_at(1.0), Brush::solid(Color::WHITE));
409 scope.draw_content();
410 scope.draw_rect_at(rect_at(2.0), Brush::solid(Color::WHITE));
411 }));
412 let size = Size::new(10.0, 10.0);
413 for placement in [DrawPlacement::Behind, DrawPlacement::Overlay] {
414 let fresh = primitives_for_placement(&command, placement, size);
415 let dirty = CommandRecording::from_primitives(vec![DrawPrimitive::Content; 8]);
416 let (recording, segments) =
417 recording_for_placement_reusing(&command, placement, size, dirty)
418 .expect("a with-content command records for both placements");
419 let reused: Vec<DrawPrimitive> = recording.primitives(segments).collect();
420 assert_eq!(fresh, reused);
421 }
422 }
423
424 #[test]
425 fn pushed_batches_keep_marker_count_authoritative() {
426 let command = DrawCommand::Behind(Rc::new(|scope: &mut DrawScopeDefault| {
427 scope.push_recorded(vec![
428 DrawPrimitive::Rect {
429 rect: rect_at(1.0),
430 brush: Brush::solid(Color::WHITE),
431 stroke: None,
432 },
433 DrawPrimitive::Content,
434 DrawPrimitive::Rect {
435 rect: rect_at(2.0),
436 brush: Brush::solid(Color::WHITE),
437 stroke: None,
438 },
439 ]);
440 }));
441 let out = primitives_for_placement(&command, DrawPlacement::Behind, Size::new(10.0, 10.0));
442 assert_eq!(rect_xs(&out), [1.0, 2.0]);
443 }
444}