1use std::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, FinishedRecording, 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,
69 backdrop_effect: layer.backdrop_effect,
71 }
72 } else {
73 GraphicsLayer {
74 compositing_strategy: CompositingStrategy::Auto,
75 blend_mode: BlendMode::SrcOver,
76 render_effect: None,
77 backdrop_effect: None,
78 ..current
79 }
80 }
81}
82
83pub use crate::graph::quad_bounds;
84
85pub fn apply_layer_to_color(color: Color, layer: &GraphicsLayer) -> Color {
86 apply_color_filter_to_color(
87 Color(
88 color.0,
89 color.1,
90 color.2,
91 (color.3 * layer.alpha).clamp(0.0, 1.0),
92 ),
93 layer.color_filter,
94 )
95}
96
97fn apply_color_filter_to_color(color: Color, filter: Option<ColorFilter>) -> Color {
98 match filter {
99 Some(filter) => {
100 let [r, g, b, a] = filter.apply_rgba([color.0, color.1, color.2, color.3]);
101 Color(r, g, b, a)
102 }
103 None => color,
104 }
105}
106
107pub fn compose_color_filters(
108 base: Option<ColorFilter>,
109 overlay: Option<ColorFilter>,
110) -> Option<ColorFilter> {
111 match (base, overlay) {
112 (None, None) => None,
113 (Some(filter), None) | (None, Some(filter)) => Some(filter),
114 (Some(filter), Some(next)) => Some(filter.compose(next)),
115 }
116}
117
118pub fn apply_layer_to_brush(brush: Brush, layer: &GraphicsLayer) -> Brush {
119 if layer.alpha == 1.0
123 && layer.color_filter.is_none()
124 && layer_scale_x(layer) == 1.0
125 && layer_scale_y(layer) == 1.0
126 {
127 return brush;
128 }
129 let scale_x = layer_scale_x(layer);
130 let scale_y = layer_scale_y(layer);
131 let uniform_scale = layer_uniform_scale(layer);
132
133 match brush {
134 Brush::Solid(color) => Brush::solid(apply_layer_to_color(color, layer)),
135 Brush::LinearGradient {
136 colors,
137 stops,
138 mut start,
139 mut end,
140 tile_mode,
141 } => {
142 start.x *= scale_x;
143 start.y *= scale_y;
144 end.x *= scale_x;
145 end.y *= scale_y;
146 Brush::LinearGradient {
147 colors: colors
148 .into_iter()
149 .map(|c| apply_layer_to_color(c, layer))
150 .collect(),
151 stops,
152 start,
153 end,
154 tile_mode,
155 }
156 }
157 Brush::RadialGradient {
158 colors,
159 stops,
160 mut center,
161 mut radius,
162 tile_mode,
163 } => {
164 center.x *= scale_x;
165 center.y *= scale_y;
166 radius *= uniform_scale;
167 Brush::RadialGradient {
168 colors: colors
169 .into_iter()
170 .map(|c| apply_layer_to_color(c, layer))
171 .collect(),
172 stops,
173 center,
174 radius,
175 tile_mode,
176 }
177 }
178 Brush::SweepGradient {
179 colors,
180 stops,
181 mut center,
182 } => {
183 center.x *= scale_x;
184 center.y *= scale_y;
185 Brush::SweepGradient {
186 colors: colors
187 .into_iter()
188 .map(|c| apply_layer_to_color(c, layer))
189 .collect(),
190 stops,
191 center,
192 }
193 }
194 }
195}
196
197pub fn scale_corner_radii(radii: CornerRadii, scale: f32) -> CornerRadii {
198 CornerRadii {
199 top_left: radii.top_left * scale,
200 top_right: radii.top_right * scale,
201 bottom_right: radii.bottom_right * scale,
202 bottom_left: radii.bottom_left * scale,
203 }
204}
205
206#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
207pub enum DrawPlacement {
208 Behind,
209 Overlay,
210}
211
212pub fn primitives_for_placement(
213 command: &DrawCommand,
214 placement: DrawPlacement,
215 size: Size,
216) -> Vec<DrawPrimitive> {
217 primitives_for_placement_reusing(command, placement, size, Vec::new())
218}
219
220pub fn primitives_for_placement_reusing(
225 command: &DrawCommand,
226 placement: DrawPlacement,
227 size: Size,
228 storage: Vec<DrawPrimitive>,
229) -> Vec<DrawPrimitive> {
230 primitives_for_placement_retained(
231 command,
232 placement,
233 size,
234 CommandRecording::default(),
235 storage,
236 )
237 .0
238}
239
240pub fn primitives_for_placement_retained(
244 command: &DrawCommand,
245 placement: DrawPlacement,
246 size: Size,
247 recording: CommandRecording,
248 storage: Vec<DrawPrimitive>,
249) -> (Vec<DrawPrimitive>, CommandRecording) {
250 let mut no_replay = None;
251 let (primitives, recording, _) = primitives_for_placement_verified(
252 command,
253 placement,
254 size,
255 recording,
256 storage,
257 &mut no_replay,
258 None,
259 );
260 (primitives, recording)
261}
262
263pub fn primitives_for_placement_verified(
273 command: &DrawCommand,
274 placement: DrawPlacement,
275 size: Size,
276 recording: CommandRecording,
277 storage: Vec<DrawPrimitive>,
278 replay: &mut Option<&mut cranpose_ui_graphics::CommandReplayState>,
279 command_id: Option<crate::graph::DrawCommandId>,
280) -> (
281 Vec<DrawPrimitive>,
282 CommandRecording,
283 Option<cranpose_ui_graphics::CommandReplayFrame>,
284) {
285 let filter_content = |primitives: Vec<DrawPrimitive>, markers: u32| {
292 if markers == 0 {
293 return primitives;
294 }
295 let mut out = Vec::with_capacity(primitives.len());
301 out.extend(
302 primitives
303 .into_iter()
304 .filter(|primitive| !matches!(primitive, DrawPrimitive::Content)),
305 );
306 out
307 };
308
309 let split_with_content = |primitives: Vec<DrawPrimitive>, placement, markers: u32| {
310 let last_content_idx = if markers == 0 {
311 None
312 } else {
313 primitives
314 .iter()
315 .rposition(|primitive| matches!(primitive, DrawPrimitive::Content))
316 };
317 let Some(last_content_idx) = last_content_idx else {
318 return if matches!(placement, DrawPlacement::Overlay) {
319 filter_content(primitives, markers)
320 } else {
321 Vec::new()
322 };
323 };
324
325 let mut out = Vec::with_capacity(primitives.len());
326 out.extend(
327 primitives
328 .into_iter()
329 .enumerate()
330 .filter_map(|(index, primitive)| {
331 if matches!(primitive, DrawPrimitive::Content) {
332 return None;
333 }
334 let is_before = index < last_content_idx;
335 match placement {
336 DrawPlacement::Behind if is_before => Some(primitive),
337 DrawPlacement::Overlay if !is_before => Some(primitive),
338 _ => None,
339 }
340 }),
341 );
342 out
343 };
344
345 fn record_into(
348 func: &DrawCommandFn,
349 size: Size,
350 recording: CommandRecording,
351 storage: Vec<DrawPrimitive>,
352 replay: &mut Option<&mut cranpose_ui_graphics::CommandReplayState>,
353 command: Option<crate::graph::DrawCommandId>,
354 ) -> (
355 FinishedRecording,
356 Option<cranpose_ui_graphics::CommandReplayFrame>,
357 ) {
358 let mut scope = cranpose_ui::command_draw_scope_retained(size, recording, storage);
359 func(&mut scope);
360 let Some(state) = replay.as_mut() else {
361 return (scope.finish(), None);
362 };
363 let outcome =
364 state.advance_pooled(scope.recorded(), crate::scene_builder::verify_executor());
365 let diag = if cranpose_core::env_flag!("CRANPOSE_COMMAND_REPLAY_DIAG") {
369 if let cranpose_ui_graphics::ReplayOutcome::Spans(spans) = &outcome {
370 let (mut retained, mut dynamic) = (0usize, 0usize);
371 for span in spans {
372 match span {
373 cranpose_ui_graphics::ReplaySpan::Retained { .. } => retained += 1,
374 cranpose_ui_graphics::ReplaySpan::Dynamic {
375 tape_start,
376 tape_end,
377 } => dynamic += tape_end - tape_start,
378 }
379 }
380 Some((
381 scope.recorded().len(),
382 retained,
383 dynamic,
384 state.segments().len(),
385 state.stats(),
386 ))
387 } else {
388 None
389 }
390 } else {
391 None
392 };
393 let center = state.center();
394 let markers = scope.content_marker_count();
399 let mut bypass = |slot: u32| {
400 markers == 0
401 && command.is_some_and(|id| crate::scene_builder::retained_slot_confirmed(id, slot))
402 };
403 let (finished, frame) = scope.finish_replay(center, outcome, &mut bypass);
404 if let Some((records, retained, dynamic, segments, (deaths, splits))) = diag {
405 log::warn!(
406 "[command-replay] {} records: {} retained spans, {} dynamic records, \
407 {} materialized; {} segments alive, lifetime deaths {} splits {}",
408 records,
409 retained,
410 dynamic,
411 finished.primitives.len(),
412 segments,
413 deaths,
414 splits,
415 );
416 }
417 (finished, frame)
418 }
419 match (placement, command) {
420 (DrawPlacement::Behind, DrawCommand::Behind(func)) => {
421 let (finished, frame) = record_into(func, size, recording, storage, replay, command_id);
422 let frame = (finished.content_markers == 0).then_some(frame).flatten();
423 (
424 filter_content(finished.primitives, finished.content_markers),
425 finished.recording,
426 frame,
427 )
428 }
429 (DrawPlacement::Overlay, DrawCommand::Overlay(func)) => {
430 let (finished, frame) = record_into(func, size, recording, storage, replay, command_id);
431 let frame = (finished.content_markers == 0).then_some(frame).flatten();
432 (
433 filter_content(finished.primitives, finished.content_markers),
434 finished.recording,
435 frame,
436 )
437 }
438 (_, DrawCommand::WithContent(func)) => {
439 let (finished, _) = record_into(func, size, recording, storage, replay, None);
442 (
443 split_with_content(finished.primitives, placement, finished.content_markers),
444 finished.recording,
445 None,
446 )
447 }
448 _ => (Vec::new(), recording, None),
449 }
450}
451
452#[cfg(test)]
453mod tests {
454 use super::*;
455 use cranpose_ui_graphics::{DrawScope, DrawScopeDefault, Rect};
456
457 fn recorded_command(record: impl Fn(&mut dyn DrawScope) + 'static) -> DrawCommandFn {
458 Rc::new(move |scope: &mut DrawScopeDefault| record(scope))
459 }
460
461 fn rect_at(x: f32) -> Rect {
462 Rect {
463 x,
464 y: 0.0,
465 width: 1.0,
466 height: 1.0,
467 }
468 }
469
470 fn rect_xs(primitives: &[DrawPrimitive]) -> Vec<f32> {
471 primitives
472 .iter()
473 .map(|primitive| match primitive {
474 DrawPrimitive::Rect { rect, .. } => rect.x,
475 other => panic!("unexpected primitive {other:?}"),
476 })
477 .collect()
478 }
479
480 #[test]
481 fn marker_free_recording_passes_through() {
482 let command = DrawCommand::Behind(recorded_command(|scope| {
483 scope.draw_rect_at(rect_at(1.0), Brush::solid(Color::WHITE));
484 scope.draw_rect_at(rect_at(2.0), Brush::solid(Color::WHITE));
485 }));
486 let out = primitives_for_placement(&command, DrawPlacement::Behind, Size::new(10.0, 10.0));
487 assert_eq!(rect_xs(&out), [1.0, 2.0]);
488 }
489
490 #[test]
491 fn recorded_markers_still_split_content_placements() {
492 let with_content = recorded_command(|scope| {
493 scope.draw_rect_at(rect_at(1.0), Brush::solid(Color::WHITE));
494 scope.draw_content();
495 scope.draw_rect_at(rect_at(2.0), Brush::solid(Color::WHITE));
496 });
497 let command = DrawCommand::WithContent(with_content);
498 let size = Size::new(10.0, 10.0);
499 let behind = primitives_for_placement(&command, DrawPlacement::Behind, size);
500 assert_eq!(rect_xs(&behind), [1.0]);
501 let overlay = primitives_for_placement(&command, DrawPlacement::Overlay, size);
502 assert_eq!(rect_xs(&overlay), [2.0]);
503 }
504
505 #[test]
509 fn reused_storage_records_identically_to_fresh() {
510 let command = DrawCommand::WithContent(recorded_command(|scope| {
511 scope.draw_rect_at(rect_at(1.0), Brush::solid(Color::WHITE));
512 scope.draw_content();
513 scope.draw_rect_at(rect_at(2.0), Brush::solid(Color::WHITE));
514 }));
515 let size = Size::new(10.0, 10.0);
516 for placement in [DrawPlacement::Behind, DrawPlacement::Overlay] {
517 let fresh = primitives_for_placement(&command, placement, size);
518 let dirty = vec![DrawPrimitive::Content; 8];
520 let reused = primitives_for_placement_reusing(&command, placement, size, dirty);
521 assert_eq!(fresh, reused);
522 }
523 }
524
525 #[test]
526 fn pushed_batches_keep_marker_count_authoritative() {
527 let command = DrawCommand::Behind(Rc::new(|scope: &mut DrawScopeDefault| {
530 scope.push_recorded(vec![
531 DrawPrimitive::Rect {
532 rect: rect_at(1.0),
533 brush: Brush::solid(Color::WHITE),
534 stroke: None,
535 },
536 DrawPrimitive::Content,
537 DrawPrimitive::Rect {
538 rect: rect_at(2.0),
539 brush: Brush::solid(Color::WHITE),
540 stroke: None,
541 },
542 ]);
543 }));
544 let out = primitives_for_placement(&command, DrawPlacement::Behind, Size::new(10.0, 10.0));
545 assert_eq!(rect_xs(&out), [1.0, 2.0]);
546 }
547}