1use crate::app::App;
18use crate::components::{
19 CARET_WIDTH_PX, CaretBlink, CaretWidth, Color, Display, EchoMode, Fill, FlexDirection,
20 FlexJustify, ImeState, Opacity, PASSWORD_MASK_CHAR, PasswordCharacter, Style, TextAlign,
21 TextBlockOrigin, TextContent, TextInput, TextInputPaint, TextInputScroll, TextStyle, Transform,
22 Visible, Visuals, resolve_line_height, text_baseline_in_line, text_block_top,
23};
24use crate::input::{Focused, ScrollOffset};
25use crate::node_ir::transform_extracted_to_nodes;
26use crate::tick::TickStage;
27use bevy_ecs::prelude::*;
28use bevy_ecs::query::Or;
29use glam::Vec2;
30use std::collections::HashMap;
31use std::sync::atomic::{AtomicBool, Ordering};
32use std::sync::{Arc, Mutex};
33
34pub type ExtractFn = fn(&mut World, &mut World);
38
39#[derive(Resource, Default, Debug)]
42pub struct ExtractPipeline;
43
44pub fn install_extract_pipeline(app: &mut App) {
59 if app.render_world.contains_resource::<ExtractPipeline>() {
60 return;
61 }
62 app.render_world.insert_resource(ExtractPipeline);
63 let chain: [ExtractFn; 8] = [
64 stash_hidden_entities,
67 extract_shadows,
68 extract_rects,
69 extract_borders,
70 extract_text,
71 extract_clips,
72 extract_opacity_groups,
73 extract_scrollbars,
74 ];
75 app.extract_fns.splice(0..0, chain);
76 app.add_systems(TickStage::A11ySync, roll_up_frame_dirty);
79 app.add_render_systems(RenderStage::Prepare, cull_offscreen);
83 app.add_render_systems(RenderStage::Prepare, cull_hidden);
84 app.add_render_systems(
85 RenderStage::Prepare,
86 transform_extracted_to_nodes
87 .after(cull_offscreen)
88 .after(cull_hidden),
89 );
90}
91
92#[derive(bevy_ecs::schedule::ScheduleLabel, Clone, Copy, Debug, Hash, PartialEq, Eq)]
94pub struct Render;
95
96#[derive(SystemSet, Clone, Copy, Debug, Hash, PartialEq, Eq)]
98pub enum RenderStage {
99 Prepare,
101 Render,
103}
104
105#[derive(Resource, Clone, Debug)]
108pub struct Viewport {
109 pub size: Vec2,
116 pub clear: Color,
118 pub scale_factor: f32,
123 pub monitor_scale: Option<f32>,
125 pub monitor_size: Option<Vec2>,
127 pub monitor_name: Option<String>,
129}
130
131impl Default for Viewport {
132 fn default() -> Self {
133 Self {
134 size: Vec2::new(800.0, 600.0),
135 clear: Color::rgba(0.0, 0.0, 0.0, 0.0),
136 scale_factor: 1.0,
137 monitor_scale: None,
138 monitor_size: None,
139 monitor_name: None,
140 }
141 }
142}
143
144#[derive(Resource, Clone, Copy, Debug, Default, PartialEq)]
153pub struct DockInsets {
154 pub right: f32,
156 pub bottom: f32,
158}
159
160#[derive(Resource, Debug)]
170pub struct FrameDirty {
171 pub dirty: bool,
173}
174
175impl Default for FrameDirty {
176 fn default() -> Self {
177 Self { dirty: true }
179 }
180}
181
182#[derive(Resource, Debug, Default)]
209pub struct AnimationsActive(std::sync::atomic::AtomicBool);
210
211impl AnimationsActive {
212 #[inline]
215 pub fn request(&self) {
216 self.0.store(true, std::sync::atomic::Ordering::Relaxed);
217 }
218
219 #[inline]
222 pub fn get(&self) -> bool {
223 self.0.load(std::sync::atomic::Ordering::Relaxed)
224 }
225
226 #[inline]
228 pub fn clear(&self) {
229 self.0.store(false, std::sync::atomic::Ordering::Relaxed);
230 }
231}
232
233pub fn reset_animations_active(flag: Res<AnimationsActive>) {
239 flag.clear();
240}
241
242#[derive(Clone, Copy, Debug, Default, PartialEq)]
244pub struct Rect {
245 pub origin: Vec2,
247 pub size: Vec2,
249}
250
251impl Rect {
252 pub const fn new(origin: Vec2, size: Vec2) -> Self {
254 Self { origin, size }
255 }
256}
257
258#[derive(Resource, Default)]
290pub struct ExtractContextCache {
291 active: bool,
295 parent_map: Option<(HashMap<Entity, Entity>, HashMap<Entity, u32>)>,
297 hidden: Option<std::collections::HashSet<Entity>>,
299 scroll: Option<HashMap<Entity, Vec2>>,
301 opacities: Option<OpacityModel>,
303 clip: Option<HashMap<Entity, (Vec2, Vec2)>>,
305}
306
307impl ExtractContextCache {
308 pub fn begin_phase(&mut self) {
312 self.active = true;
313 self.parent_map = None;
314 self.hidden = None;
315 self.scroll = None;
316 self.opacities = None;
317 self.clip = None;
318 }
319
320 pub fn end_phase(&mut self) {
324 self.active = false;
325 self.parent_map = None;
326 self.hidden = None;
327 self.scroll = None;
328 self.opacities = None;
329 self.clip = None;
330 }
331}
332
333#[derive(SystemSet, Clone, Copy, Debug, Hash, PartialEq, Eq)]
338pub enum ExtractSet {
339 Extract,
341}
342
343#[derive(bevy_ecs::schedule::ScheduleLabel, Clone, Copy, Debug, Hash, PartialEq, Eq)]
348pub struct ExtractSchedule;
349
350#[allow(clippy::type_complexity, clippy::too_many_arguments)]
364pub fn roll_up_frame_dirty(
365 mut fd: ResMut<FrameDirty>,
366 viewport: Res<Viewport>,
367 property_store: Option<Res<crate::property_store::PropertyStore>>,
368 component_changed: Query<
369 (),
370 Or<(
371 bevy_ecs::query::Changed<Transform>,
372 bevy_ecs::query::Changed<Visuals>,
373 bevy_ecs::query::Changed<TextStyle>,
374 bevy_ecs::query::Changed<TextContent>,
375 bevy_ecs::query::Changed<TextInput>,
376 bevy_ecs::query::Changed<TextInputScroll>,
377 bevy_ecs::query::Changed<Opacity>,
378 bevy_ecs::query::Changed<Visible>,
379 bevy_ecs::query::Changed<crate::components::LumenClasses>,
380 bevy_ecs::query::Changed<crate::input::ScrollbarState>,
383 bevy_ecs::query::Changed<ScrollOffset>,
387 bevy_ecs::query::Changed<EchoMode>,
390 bevy_ecs::query::Added<EchoMode>,
391 bevy_ecs::query::Added<Visible>,
392 )>,
393 >,
394 mut removed_childof: RemovedComponents<bevy_ecs::hierarchy::ChildOf>,
395 trace: (
396 Query<Entity, bevy_ecs::query::Changed<Transform>>,
397 Query<Entity, bevy_ecs::query::Changed<Visuals>>,
398 Query<Entity, bevy_ecs::query::Changed<TextStyle>>,
399 Query<Entity, bevy_ecs::query::Changed<TextContent>>,
400 Query<Entity, bevy_ecs::query::Changed<TextInput>>,
401 Query<Entity, bevy_ecs::query::Changed<TextInputScroll>>,
402 Query<Entity, bevy_ecs::query::Changed<Opacity>>,
403 Query<Entity, bevy_ecs::query::Changed<Visible>>,
404 Query<Entity, bevy_ecs::query::Changed<crate::components::LumenClasses>>,
405 Query<Entity, bevy_ecs::query::Changed<crate::input::ScrollbarState>>,
406 Query<Entity, bevy_ecs::query::Changed<ScrollOffset>>,
407 ),
408) {
409 let removed_any = removed_childof.read().count() > 0;
416 if fd.dirty {
417 return;
418 }
419 let property_dirty = property_store
420 .as_ref()
421 .is_some_and(|s| !s.dirty_peek().is_empty());
422 if property_dirty || removed_any || viewport.is_changed() || !component_changed.is_empty() {
423 fd.dirty = true;
424 static TRACE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
427 let trace_on = *TRACE.get_or_init(|| std::env::var_os("LUMEN_TRACE_FRAME_DIRTY").is_some());
428 if trace_on {
429 let mut sources: Vec<String> = Vec::new();
430 if property_dirty {
431 let keys: Vec<String> = property_store
432 .as_ref()
433 .map(|s| s.dirty_peek().iter().map(|k| format!("{k:?}")).collect())
434 .unwrap_or_default();
435 sources.push(format!("property_store{keys:?}"));
436 }
437 if removed_any {
438 sources.push("removed ChildOf".into());
439 }
440 if viewport.is_changed() {
441 sources.push("viewport".into());
442 }
443 let names = [
444 "Transform",
445 "Visuals",
446 "TextStyle",
447 "TextContent",
448 "TextInput",
449 "TextInputScroll",
450 "Opacity",
451 "Visible",
452 "LumenClasses",
453 "ScrollbarState",
454 "ScrollOffset",
455 ];
456 let counts = [
457 trace.0.iter().count(),
458 trace.1.iter().count(),
459 trace.2.iter().count(),
460 trace.3.iter().count(),
461 trace.4.iter().count(),
462 trace.5.iter().count(),
463 trace.6.iter().count(),
464 trace.7.iter().count(),
465 trace.8.iter().count(),
466 trace.9.iter().count(),
467 trace.10.iter().count(),
468 ];
469 for (name, count) in names.iter().zip(counts) {
470 if count > 0 {
471 sources.push(format!("Changed<{name}>x{count}"));
472 }
473 }
474 eprintln!("lumen-core: FrameDirty raised by {}", sources.join(", "));
475 }
476 }
477}
478
479pub type PaintOrder = u32;
500
501pub const OVERLAY_ORDER_BASE: PaintOrder = 0x4000_0000;
505
506#[derive(Component, Clone, Copy, Debug, Default)]
514pub struct OverlayLayer;
515
516#[derive(Resource, Default, Debug)]
523pub struct OverlayOpenOrder {
524 pub stamps: HashMap<Entity, u64>,
526 pub next: u64,
528}
529
530#[derive(Clone, Debug, PartialEq)]
537pub enum Brush {
538 Solid(Color),
540 Linear {
542 angle_deg: f32,
544 stops: std::sync::Arc<[(f32, Color)]>,
546 },
547 Radial {
549 radius: f32,
551 stops: std::sync::Arc<[(f32, Color)]>,
553 },
554 Conic {
556 from_deg: f32,
558 stops: std::sync::Arc<[(f32, Color)]>,
560 },
561}
562
563impl Brush {
564 pub fn with_opacity(self, alpha: Opacity) -> Self {
567 match self {
568 Brush::Solid(c) => Brush::Solid(alpha.apply(c)),
569 Brush::Linear { angle_deg, stops } => {
570 let mapped: std::sync::Arc<[(f32, Color)]> =
571 stops.iter().map(|(o, c)| (*o, alpha.apply(*c))).collect();
572 Brush::Linear {
573 angle_deg,
574 stops: mapped,
575 }
576 }
577 Brush::Radial { radius, stops } => {
578 let mapped: std::sync::Arc<[(f32, Color)]> =
579 stops.iter().map(|(o, c)| (*o, alpha.apply(*c))).collect();
580 Brush::Radial {
581 radius,
582 stops: mapped,
583 }
584 }
585 Brush::Conic { from_deg, stops } => {
586 let mapped: std::sync::Arc<[(f32, Color)]> =
587 stops.iter().map(|(o, c)| (*o, alpha.apply(*c))).collect();
588 Brush::Conic {
589 from_deg,
590 stops: mapped,
591 }
592 }
593 }
594 }
595}
596
597impl From<&Fill> for Brush {
598 fn from(f: &Fill) -> Self {
599 match f {
600 Fill::Solid(c) => Brush::Solid(*c),
601 Fill::Linear { angle_deg, stops } => Brush::Linear {
602 angle_deg: *angle_deg,
603 stops: stops.iter().copied().collect(),
604 },
605 Fill::Radial { radius, stops } => Brush::Radial {
606 radius: *radius,
607 stops: stops.iter().copied().collect(),
608 },
609 Fill::Conic { from_deg, stops } => Brush::Conic {
610 from_deg: *from_deg,
611 stops: stops.iter().copied().collect(),
612 },
613 }
614 }
615}
616
617#[derive(Component, Clone, Debug)]
619pub struct ExtractedRect {
620 pub origin: Vec2,
622 pub size: Vec2,
624 pub brush: Brush,
626 pub radius: f32,
628 pub corner_radii: Option<[f32; 4]>,
631 pub order: PaintOrder,
633}
634
635#[derive(Component, Clone, Copy, Debug)]
637pub struct ExtractedShadow {
638 pub origin: Vec2,
640 pub size: Vec2,
642 pub radius: f32,
644 pub spread: f32,
647 pub blur: f32,
649 pub color: Color,
651 pub order: PaintOrder,
654 pub inner: bool,
656 pub rect_origin: Vec2,
658}
659
660#[derive(Component, Clone, Copy, Debug)]
668pub struct ExtractedBorder {
669 pub origin: Vec2,
671 pub size: Vec2,
673 pub widths: [f32; 4],
675 pub color: Color,
677 pub side_colors: Option<[Color; 4]>,
680 pub radius: f32,
682 pub corner_radii: Option<[f32; 4]>,
684 pub order: PaintOrder,
686}
687
688#[derive(Component, Clone, Copy, Debug)]
691pub struct ExtractedOutline {
692 pub origin: Vec2,
694 pub size: Vec2,
696 pub stroke: Color,
698 pub width: f32,
700 pub radius: f32,
702 pub order: PaintOrder,
704}
705
706#[derive(Component, Clone, Debug)]
709pub struct ExtractedImage {
710 pub origin: Vec2,
712 pub size: Vec2,
714 pub width: u32,
716 pub height: u32,
718 pub rgba: std::sync::Arc<[u8]>,
720 pub fit: crate::components::ImageFit,
722 pub order: PaintOrder,
724 pub alpha: f32,
726 pub background: Option<BackgroundClip>,
730}
731
732#[derive(Clone, Copy, Debug, PartialEq)]
735pub struct BackgroundClip {
736 pub origin: Vec2,
738 pub size: Vec2,
740 pub radii: [f32; 4],
742}
743
744pub const DEFAULT_SELECTION_BG: Color = Color::rgba(0.20, 0.51, 0.98, 0.40);
752
753#[derive(Component, Clone, Debug, PartialEq)]
759pub struct ExtractedText {
760 pub origin: Vec2,
762 pub text: String,
764 pub size_px: f32,
766 pub fill: Color,
768 pub caret: Option<usize>,
770 pub selection: Option<(usize, usize)>,
772 pub selection_color: Option<Color>,
776 pub selection_foreground: Option<Color>,
780 pub caret_color: Option<Color>,
783 pub order: PaintOrder,
785 pub container_width: f32,
787 pub align: TextAlign,
789 pub wrap: crate::components::TextWrap,
791 pub max_lines: Option<u32>,
793 pub family: Option<std::sync::Arc<str>>,
795 pub weight: u16,
797 pub line_height_px: f32,
801 pub caret_width_px: f32,
804}
805
806#[derive(Component, Clone, Copy, Debug)]
811pub struct ExtractedClipBox {
812 pub origin: Vec2,
815 pub size: Vec2,
817 pub radius: f32,
819 pub start_order: PaintOrder,
823 pub end_order: PaintOrder,
826}
827
828#[derive(Component, Clone, Copy, Debug, PartialEq)]
837pub struct ExtractedOpacityGroup {
838 pub alpha: f32,
840 pub start_order: PaintOrder,
843 pub end_order: PaintOrder,
846 pub key: u64,
849}
850
851#[derive(Clone, Copy, Debug, PartialEq)]
853pub struct ScrollbarDrawRect {
854 pub origin: Vec2,
856 pub size: Vec2,
858 pub color: Color,
860 pub radius: f32,
862}
863
864#[derive(Component, Clone, Debug)]
873pub struct ExtractedScrollbar {
874 pub draws: Vec<ScrollbarDrawRect>,
876 pub order: PaintOrder,
878}
879
880pub fn extract_scrollbars(main: &mut World, render: &mut World) {
890 use crate::input::{
891 Scroll, ScrollbarAxisPick, ScrollbarInteraction, ScrollbarState, ScrollbarStyle,
892 horizontal_scrollbar, vertical_scrollbar,
893 };
894 let (parents, mut depth_cache) = build_parent_map(main);
895 let hidden = hidden_entities(main, &parents);
896 let scroll_offsets = parent_scroll_offsets(main, &parents);
897 let interaction = main
898 .get_resource::<ScrollbarInteraction>()
899 .copied()
900 .unwrap_or_default();
901
902 let mut children: std::collections::HashMap<Entity, Vec<Entity>> =
904 std::collections::HashMap::new();
905 for (&e, &p) in parents.iter() {
906 children.entry(p).or_default().push(e);
907 }
908 let transforms: std::collections::HashMap<Entity, Transform> = {
909 let mut q = main.query::<(Entity, &Transform)>();
910 q.iter(main).map(|(e, t)| (e, *t)).collect()
911 };
912
913 type Row<'a> = (
914 Entity,
915 &'a Transform,
916 &'a Scroll,
917 &'a ScrollOffset,
918 &'a ScrollbarState,
919 Option<&'a ScrollbarStyle>,
920 );
921 let mut q = main.query::<Row>();
922 let mut pairs: Vec<(Entity, ExtractedScrollbar)> = Vec::new();
923 for (e, tf, scroll, offset, state, style) in q.iter(main) {
924 if hidden.contains(&e) || state.alpha <= 0.001 {
925 continue;
926 }
927 let style = style.copied().unwrap_or_default();
928 let Some(metrics) = style.metrics() else {
930 continue;
931 };
932 let (mut content_w, mut content_h) = (0.0_f32, 0.0_f32);
935 if let Some(kids) = children.get(&e) {
936 for kid in kids {
937 if let Some(kt) = transforms.get(kid) {
938 content_w = content_w.max((kt.absolute.x - tf.absolute.x) + kt.size.x);
939 content_h = content_h.max((kt.absolute.y - tf.absolute.y) + kt.size.y);
940 }
941 }
942 }
943 let allow_y = scroll.axis.allows_y();
944 let allow_x = scroll.axis.allows_x();
945 let anc_off = scroll_offsets.get(&e).copied().unwrap_or(Vec2::ZERO);
948 let origin = tf.absolute - anc_off;
949 let v_overflow = allow_y && content_h - tf.size.y > 0.5;
950 let h_overflow = allow_x && content_w - tf.size.x > 0.5;
951 let v_geo = if v_overflow {
952 vertical_scrollbar(origin, tf.size, content_h, offset.0.y, h_overflow, metrics)
953 } else {
954 None
955 };
956 let h_geo = if h_overflow {
957 horizontal_scrollbar(origin, tf.size, content_w, offset.0.x, v_overflow, metrics)
958 } else {
959 None
960 };
961 if v_geo.is_none() && h_geo.is_none() {
962 continue;
963 }
964
965 let fade = state.alpha.clamp(0.0, 1.0);
966 let radius = metrics.thickness / 2.0;
967
968 let mut draws: Vec<ScrollbarDrawRect> = Vec::with_capacity(4);
969 let mut push_bar = |geo: crate::input::ScrollbarGeometry, axis: ScrollbarAxisPick| {
970 let hovered = interaction
971 .drag
972 .map(|d| d.entity == e && d.axis == axis)
973 .unwrap_or(false)
974 || interaction
975 .hover
976 .map(|(he, ha, _)| he == e && ha == axis)
977 .unwrap_or(false);
978 let track = match style.track {
982 Some(c) => Some(c),
983 None if hovered => Some(style.hover_track),
984 None => None,
985 };
986 if let Some(mut track) = track {
987 track.a *= fade;
988 draws.push(ScrollbarDrawRect {
989 origin: geo.track_origin,
990 size: geo.track_size,
991 color: track,
992 radius,
993 });
994 }
995 let mut thumb = style.thumb;
996 thumb.a =
997 (thumb.a * if hovered { style.hover_boost } else { 1.0 }).clamp(0.0, 1.0) * fade;
998 draws.push(ScrollbarDrawRect {
999 origin: geo.thumb_origin,
1000 size: geo.thumb_size,
1001 color: thumb,
1002 radius,
1003 });
1004 };
1005 if let Some(geo) = v_geo {
1006 push_bar(geo, ScrollbarAxisPick::Vertical);
1007 }
1008 if let Some(geo) = h_geo {
1009 push_bar(geo, ScrollbarAxisPick::Horizontal);
1010 }
1011
1012 let mut end = paint_order_of(e, &parents, &mut depth_cache);
1016 let mut stack = vec![e];
1017 while let Some(n) = stack.pop() {
1018 let o = paint_order_of(n, &parents, &mut depth_cache);
1019 if o > end {
1020 end = o;
1021 }
1022 if let Some(kids) = children.get(&n) {
1023 stack.extend(kids.iter().copied());
1024 }
1025 }
1026 pairs.push((
1027 e,
1028 ExtractedScrollbar {
1029 draws,
1030 order: end.saturating_add(1),
1031 },
1032 ));
1033 }
1034
1035 let prior = std::mem::take(&mut render.resource_mut::<RenderEntityMap>().scrollbar);
1038 let mut next: std::collections::HashMap<Entity, Entity> =
1039 std::collections::HashMap::with_capacity(pairs.len());
1040 for (main_e, bar) in pairs {
1041 let reuse = prior
1042 .get(&main_e)
1043 .copied()
1044 .filter(|&re| render.get_entity(re).is_ok());
1045 let render_e = match reuse {
1046 Some(re) => {
1047 render.entity_mut(re).insert(bar);
1048 re
1049 }
1050 None => render.spawn(bar).id(),
1051 };
1052 next.insert(main_e, render_e);
1053 }
1054 for (main_e, render_e) in &prior {
1055 if !next.contains_key(main_e)
1056 && let Ok(em) = render.get_entity_mut(*render_e)
1057 {
1058 em.despawn();
1059 }
1060 }
1061 render.resource_mut::<RenderEntityMap>().scrollbar = next;
1062}
1063
1064#[derive(Resource, Default, Debug)]
1077pub struct HiddenExtracts(pub std::collections::HashSet<Entity>);
1078
1079pub fn stash_hidden_entities(main: &mut World, render: &mut World) {
1086 let (parents, _) = build_parent_map(main);
1087 let hidden = hidden_entities(main, &parents);
1088 render.resource_mut::<HiddenExtracts>().0 = hidden;
1089}
1090
1091pub fn cull_hidden(
1102 mut commands: Commands,
1103 hidden: Res<HiddenExtracts>,
1104 mut map: ResMut<RenderEntityMap>,
1105) {
1106 if hidden.0.is_empty() {
1107 return;
1108 }
1109 let h = &hidden.0;
1110 let mut victims: Vec<Entity> = Vec::new();
1111 let map = &mut *map;
1114 for m in [
1115 &mut map.rect,
1116 &mut map.text,
1117 &mut map.outline,
1118 &mut map.border,
1119 &mut map.image,
1120 &mut map.svg,
1121 &mut map.clip,
1122 &mut map.scrollbar,
1123 ] {
1124 m.retain(|main_e, render_e| {
1125 if h.contains(main_e) {
1126 victims.push(*render_e);
1127 false
1128 } else {
1129 true
1130 }
1131 });
1132 }
1133 map.native.retain(|(_, main_e), render_e| {
1135 if h.contains(main_e) {
1136 victims.push(*render_e);
1137 false
1138 } else {
1139 true
1140 }
1141 });
1142 map.shadow.retain(|main_e, render_es| {
1144 if h.contains(main_e) {
1145 victims.extend(render_es.iter().copied());
1146 false
1147 } else {
1148 true
1149 }
1150 });
1151 for e in victims {
1152 commands.entity(e).despawn();
1153 }
1154}
1155
1156pub fn cull_offscreen(
1161 mut commands: Commands,
1162 viewport: Res<Viewport>,
1163 rects: Query<(Entity, &ExtractedRect)>,
1164 texts: Query<(Entity, &ExtractedText)>,
1165 natives: Query<(Entity, &crate::native::ExtractedNative)>,
1166) {
1167 let vw = viewport.size.x;
1168 let vh = viewport.size.y;
1169 for (e, n) in &natives {
1172 if n.bounds.origin.x + n.bounds.size.x <= 0.0
1173 || n.bounds.origin.y + n.bounds.size.y <= 0.0
1174 || n.bounds.origin.x >= vw
1175 || n.bounds.origin.y >= vh
1176 {
1177 commands.entity(e).despawn();
1178 }
1179 }
1180 for (e, r) in &rects {
1181 if r.origin.x + r.size.x <= 0.0
1182 || r.origin.y + r.size.y <= 0.0
1183 || r.origin.x >= vw
1184 || r.origin.y >= vh
1185 {
1186 commands.entity(e).despawn();
1187 }
1188 }
1189 for (e, t) in &texts {
1190 let h = t.size_px;
1192 let w = t.size_px * (t.text.chars().count() as f32 + 1.0);
1193 if t.origin.x + w <= 0.0
1194 || t.origin.y + h <= 0.0
1195 || t.origin.x >= vw
1196 || t.origin.y >= vh
1202 {
1203 commands.entity(e).despawn();
1204 }
1205 }
1206}
1207
1208#[derive(Resource, Default, Debug)]
1213pub struct RenderEntityMap {
1214 pub rect: std::collections::HashMap<Entity, Entity>,
1216 pub text: std::collections::HashMap<Entity, Entity>,
1218 pub shadow: std::collections::HashMap<Entity, Vec<Entity>>,
1220 pub outline: std::collections::HashMap<Entity, Entity>,
1222 pub border: std::collections::HashMap<Entity, Entity>,
1224 pub image: std::collections::HashMap<Entity, Entity>,
1226 pub svg: std::collections::HashMap<Entity, Entity>,
1228 pub clip: std::collections::HashMap<Entity, Entity>,
1230 pub opacity_group: std::collections::HashMap<Entity, Entity>,
1233 pub scrollbar: std::collections::HashMap<Entity, Entity>,
1236 pub native: std::collections::HashMap<(std::sync::Arc<str>, Entity), Entity>,
1241}
1242
1243pub fn clear_extracted(render: &mut World) {
1248 let upserted: std::collections::HashSet<Entity> = {
1249 let map = render.resource::<RenderEntityMap>();
1250 let mut set: std::collections::HashSet<Entity> =
1251 std::collections::HashSet::with_capacity(map.rect.len() + map.shadow.len());
1252 set.extend(map.rect.values().copied());
1253 set.extend(map.shadow.values().flatten().copied());
1254 set.extend(map.text.values().copied());
1255 set.extend(map.outline.values().copied());
1256 set.extend(map.border.values().copied());
1257 set.extend(map.clip.values().copied());
1258 set.extend(map.opacity_group.values().copied());
1259 set.extend(map.image.values().copied());
1260 set.extend(map.svg.values().copied());
1261 set.extend(map.scrollbar.values().copied());
1262 set.extend(map.native.values().copied());
1263 set
1264 };
1265 let to_despawn: Vec<Entity> = render
1266 .query_filtered::<Entity, Or<(
1267 With<ExtractedRect>,
1268 With<ExtractedText>,
1269 With<ExtractedImage>,
1270 With<ExtractedOutline>,
1271 With<ExtractedBorder>,
1272 With<ExtractedShadow>,
1273 With<ExtractedScrollbar>,
1274 With<crate::native::ExtractedNative>,
1275 )>>()
1276 .iter(render)
1277 .filter(|e| !upserted.contains(e))
1278 .collect();
1279 for e in to_despawn {
1280 render.despawn(e);
1281 }
1282}
1283
1284pub fn extract_shadows(main: &mut World, render: &mut World) {
1288 let (parents, mut depth_cache) = build_parent_map(main);
1289 let hidden = hidden_entities(main, &parents);
1290 let scroll = parent_scroll_offsets(main, &parents);
1291 let alphas = opacity_model(main, &parents).draw;
1292 let mut q = main.query::<(Entity, &Transform, &Visuals)>();
1293 let mut groups: std::collections::HashMap<Entity, Vec<ExtractedShadow>> =
1295 std::collections::HashMap::new();
1296 for (e, t, v) in q.iter(main) {
1297 if hidden.contains(&e) || v.shadows.is_empty() {
1298 continue;
1299 }
1300 let alpha = draw_opacity(&alphas, e);
1301 let off = scroll.get(&e).copied().unwrap_or(Vec2::ZERO);
1302 let own_order = paint_order_of(e, &parents, &mut depth_cache);
1303 let base_order = own_order.saturating_sub(1);
1304 let mut entries = Vec::with_capacity(v.shadows.len());
1305 let mut outer_idx = 0u32;
1306 for s in &v.shadows {
1307 let order = if s.inner {
1308 own_order
1309 } else {
1310 outer_idx += 1;
1311 base_order.saturating_add(outer_idx - 1)
1312 };
1313 entries.push(ExtractedShadow {
1314 origin: Vec2::new(t.absolute.x + s.offset_x, t.absolute.y + s.offset_y) - off,
1315 size: t.size,
1316 radius: v.radius,
1317 spread: s.spread,
1318 blur: s.blur,
1319 color: alpha.apply(s.color),
1320 order,
1321 inner: s.inner,
1322 rect_origin: t.absolute - off,
1323 });
1324 }
1325 groups.insert(e, entries);
1326 }
1327
1328 let prior = std::mem::take(&mut render.resource_mut::<RenderEntityMap>().shadow);
1330 let mut next: std::collections::HashMap<Entity, Vec<Entity>> =
1331 std::collections::HashMap::with_capacity(groups.len());
1332 for (main_e, shadows) in groups {
1333 let mut slots: Vec<Entity> = prior
1335 .get(&main_e)
1336 .cloned()
1337 .unwrap_or_default()
1338 .into_iter()
1339 .filter(|&re| render.get_entity(re).is_ok())
1340 .collect();
1341 while slots.len() < shadows.len() {
1342 slots.push(
1343 render
1344 .spawn(ExtractedShadow {
1345 origin: Vec2::ZERO,
1346 size: Vec2::ZERO,
1347 radius: 0.0,
1348 spread: 0.0,
1349 blur: 0.0,
1350 color: Color::rgba(0.0, 0.0, 0.0, 0.0),
1351 order: 0,
1352 inner: false,
1353 rect_origin: Vec2::ZERO,
1354 })
1355 .id(),
1356 );
1357 }
1358 while slots.len() > shadows.len() {
1359 let drop = slots.pop().unwrap();
1360 if let Ok(em) = render.get_entity_mut(drop) {
1361 em.despawn();
1362 }
1363 }
1364 for (re, s) in slots.iter().copied().zip(shadows) {
1365 render.entity_mut(re).insert(s);
1366 }
1367 next.insert(main_e, slots);
1368 }
1369 for (main_e, slots) in &prior {
1371 if !next.contains_key(main_e) {
1372 for re in slots {
1373 if let Ok(em) = render.get_entity_mut(*re) {
1374 em.despawn();
1375 }
1376 }
1377 }
1378 }
1379 render.resource_mut::<RenderEntityMap>().shadow = next;
1380}
1381
1382pub fn snap_to_device_pixels(origin: Vec2, size: Vec2, scale: f32) -> (Vec2, Vec2) {
1389 let axis = |start: f32, extent: f32| {
1390 let s0 = (start * scale).round() / scale;
1391 let mut s1 = ((start + extent) * scale).round() / scale;
1392 if extent > 0.0 && s1 <= s0 {
1393 s1 = s0 + 1.0 / scale;
1394 }
1395 (s0, (s1 - s0).max(0.0))
1396 };
1397 let (x, w) = axis(origin.x, size.x);
1398 let (y, h) = axis(origin.y, size.y);
1399 (Vec2::new(x, y), Vec2::new(w, h))
1400}
1401
1402pub fn extract_rects(main: &mut World, render: &mut World) {
1408 let scale = main
1409 .get_resource::<Viewport>()
1410 .map(|v| v.scale_factor)
1411 .filter(|s| s.is_finite() && *s > 0.0)
1412 .unwrap_or(1.0);
1413 let (parents, mut depth_cache) = build_parent_map(main);
1414 let hidden = hidden_entities(main, &parents);
1415 let scroll = parent_scroll_offsets(main, &parents);
1416 let alphas = opacity_model(main, &parents).draw;
1417 let clip = parent_scroll_clip_rects(main, &parents);
1418 let mut q = main.query::<(Entity, &Transform, &Visuals)>();
1419 let pairs: Vec<(Entity, ExtractedRect)> = q
1420 .iter(main)
1421 .filter(|(e, _, _)| !hidden.contains(e))
1422 .filter_map(|(e, t, v)| {
1423 let alpha = draw_opacity(&alphas, e);
1424 let brush = Brush::from(v.fill.as_ref()?).with_opacity(alpha);
1425 let off = scroll.get(&e).copied().unwrap_or(Vec2::ZERO);
1426 let origin = t.absolute - off;
1427 if let Some(clip_rect) = clip.get(&e)
1429 && aabb_outside(origin, t.size, *clip_rect)
1430 {
1431 return None;
1432 }
1433 let (origin, size) = snap_to_device_pixels(origin, t.size, scale);
1434 Some((
1435 e,
1436 ExtractedRect {
1437 origin,
1438 size,
1439 brush,
1440 radius: v.radius,
1441 corner_radii: v.corner_radii,
1442 order: paint_order_of(e, &parents, &mut depth_cache),
1443 },
1444 ))
1445 })
1446 .collect();
1447
1448 let prior = std::mem::take(&mut render.resource_mut::<RenderEntityMap>().rect);
1452 let mut next: std::collections::HashMap<Entity, Entity> =
1453 std::collections::HashMap::with_capacity(pairs.len());
1454 for (main_e, rect) in pairs {
1455 let reuse = prior
1456 .get(&main_e)
1457 .copied()
1458 .filter(|&re| render.get_entity(re).is_ok());
1459 let render_e = match reuse {
1460 Some(re) => {
1461 render.entity_mut(re).insert(rect);
1462 re
1463 }
1464 None => render.spawn(rect).id(),
1465 };
1466 next.insert(main_e, render_e);
1467 }
1468 for (main_e, render_e) in &prior {
1470 if !next.contains_key(main_e)
1471 && let Ok(em) = render.get_entity_mut(*render_e)
1472 {
1473 em.despawn();
1474 }
1475 }
1476 render.resource_mut::<RenderEntityMap>().rect = next;
1477}
1478
1479pub fn extract_borders(main: &mut World, render: &mut World) {
1486 use crate::components::ResolvedDirection;
1487 let (parents, mut depth_cache) = build_parent_map(main);
1488 let hidden = hidden_entities(main, &parents);
1489 let scroll = parent_scroll_offsets(main, &parents);
1490 let alphas = opacity_model(main, &parents).draw;
1491 let clip = parent_scroll_clip_rects(main, &parents);
1492 type Row<'a> = (
1493 Entity,
1494 &'a Transform,
1495 &'a Visuals,
1496 Option<&'a ResolvedDirection>,
1497 );
1498 let mut q = main.query::<Row>();
1499 let pairs: Vec<(Entity, ExtractedBorder)> = q
1500 .iter(main)
1501 .filter(|(e, _, _, _)| !hidden.contains(e))
1502 .filter_map(|(e, t, v, dir)| {
1503 let border = v.border.as_ref()?;
1504 let widths = border
1505 .widths
1506 .resolved(dir.map(|d| d.direction()).unwrap_or_default());
1507 if widths.top <= 0.0
1508 && widths.right <= 0.0
1509 && widths.bottom <= 0.0
1510 && widths.left <= 0.0
1511 {
1512 return None;
1513 }
1514 let alpha = draw_opacity(&alphas, e);
1515 let off = scroll.get(&e).copied().unwrap_or(Vec2::ZERO);
1516 let origin = t.absolute - off;
1517 if let Some(clip_rect) = clip.get(&e)
1518 && aabb_outside(origin, t.size, *clip_rect)
1519 {
1520 return None;
1521 }
1522 Some((
1523 e,
1524 ExtractedBorder {
1525 origin,
1526 size: t.size,
1527 widths: [widths.top, widths.right, widths.bottom, widths.left],
1528 color: alpha.apply(border.color),
1529 side_colors: border.side_colors.map(|cs| cs.map(|c| alpha.apply(c))),
1530 radius: v.radius,
1531 corner_radii: v.corner_radii,
1532 order: paint_order_of(e, &parents, &mut depth_cache),
1533 },
1534 ))
1535 })
1536 .collect();
1537
1538 let prior = std::mem::take(&mut render.resource_mut::<RenderEntityMap>().border);
1541 let mut next: std::collections::HashMap<Entity, Entity> =
1542 std::collections::HashMap::with_capacity(pairs.len());
1543 for (main_e, border) in pairs {
1544 let reuse = prior
1545 .get(&main_e)
1546 .copied()
1547 .filter(|&re| render.get_entity(re).is_ok());
1548 let render_e = match reuse {
1549 Some(re) => {
1550 render.entity_mut(re).insert(border);
1551 re
1552 }
1553 None => render.spawn(border).id(),
1554 };
1555 next.insert(main_e, render_e);
1556 }
1557 for (main_e, render_e) in &prior {
1558 if !next.contains_key(main_e)
1559 && let Ok(em) = render.get_entity_mut(*render_e)
1560 {
1561 em.despawn();
1562 }
1563 }
1564 render.resource_mut::<RenderEntityMap>().border = next;
1565}
1566
1567pub fn parent_scroll_clip_rects(
1575 main: &mut World,
1576 parents: &std::collections::HashMap<Entity, Entity>,
1577) -> std::collections::HashMap<Entity, (Vec2, Vec2)> {
1578 use crate::components::{Overflow, Style, Transform};
1579 use crate::input::Scroll;
1580 if let Some(c) = main.get_resource::<ExtractContextCache>()
1581 && c.active
1582 && let Some(v) = &c.clip
1583 {
1584 return v.clone();
1585 }
1586 let offsets = parent_scroll_offsets(main, parents);
1587 let clippers: std::collections::HashMap<Entity, (Vec2, Vec2)> = {
1589 let mut q = main.query::<(Entity, &Transform, &Style, Option<&Scroll>)>();
1590 q.iter(main)
1591 .filter_map(|(e, t, style, scroll)| {
1592 let qualifies = scroll.is_some()
1593 || matches!(style.overflow_y, Overflow::Hidden)
1594 || matches!(style.overflow_x, Overflow::Hidden);
1595 let off = offsets.get(&e).copied().unwrap_or(Vec2::ZERO);
1596 qualifies.then_some((e, (t.absolute - off, t.size)))
1597 })
1598 .collect()
1599 };
1600 if clippers.is_empty() {
1601 if let Some(mut c) = main.get_resource_mut::<ExtractContextCache>()
1602 && c.active
1603 {
1604 c.clip = Some(std::collections::HashMap::new());
1605 }
1606 return std::collections::HashMap::new();
1607 }
1608 let overlay: std::collections::HashSet<Entity> = {
1612 let mut oq = main.query_filtered::<Entity, With<OverlayLayer>>();
1613 oq.iter(main).collect()
1614 };
1615 let mut out: std::collections::HashMap<Entity, (Vec2, Vec2)> = std::collections::HashMap::new();
1616 let mut cache: std::collections::HashMap<Entity, Option<(Vec2, Vec2)>> =
1617 std::collections::HashMap::new();
1618 fn resolve(
1619 e: Entity,
1620 parents: &std::collections::HashMap<Entity, Entity>,
1621 clippers: &std::collections::HashMap<Entity, (Vec2, Vec2)>,
1622 overlay: &std::collections::HashSet<Entity>,
1623 cache: &mut std::collections::HashMap<Entity, Option<(Vec2, Vec2)>>,
1624 ) -> Option<(Vec2, Vec2)> {
1625 if let Some(v) = cache.get(&e) {
1626 return *v;
1627 }
1628 let result = if overlay.contains(&e) {
1629 None
1631 } else if let Some(p) = parents.get(&e).copied() {
1632 if let Some(rect) = clippers.get(&p) {
1633 Some(*rect)
1634 } else {
1635 resolve(p, parents, clippers, overlay, cache)
1636 }
1637 } else {
1638 None
1639 };
1640 cache.insert(e, result);
1641 result
1642 }
1643 for &e in parents.keys() {
1644 if let Some(rect) = resolve(e, parents, &clippers, &overlay, &mut cache) {
1645 out.insert(e, rect);
1646 }
1647 }
1648 if let Some(mut c) = main.get_resource_mut::<ExtractContextCache>()
1649 && c.active
1650 {
1651 c.clip = Some(out.clone());
1652 }
1653 out
1654}
1655
1656pub(crate) fn aabb_outside(origin: Vec2, size: Vec2, clip: (Vec2, Vec2)) -> bool {
1665 let (co, cs) = clip;
1666 origin.x + size.x <= co.x
1667 || origin.y + size.y <= co.y
1668 || origin.x >= co.x + cs.x
1669 || origin.y >= co.y + cs.y
1670}
1671
1672pub fn extract_clips(main: &mut World, render: &mut World) {
1682 use crate::components::{Overflow, Style, Transform};
1683 use crate::input::Scroll;
1684 let (parents, mut depth_cache) = build_parent_map(main);
1685 let hidden = hidden_entities(main, &parents);
1686 let offsets = parent_scroll_offsets(main, &parents);
1687 let mut children: std::collections::HashMap<Entity, Vec<Entity>> =
1689 std::collections::HashMap::new();
1690 for (&e, &p) in parents.iter() {
1691 children.entry(p).or_default().push(e);
1692 }
1693 let mut candidates: Vec<Entity> = Vec::new();
1695 {
1696 let mut q = main.query::<(Entity, &Style, Option<&Scroll>)>();
1697 for (e, style, scroll) in q.iter(main) {
1698 if hidden.contains(&e) {
1699 continue;
1700 }
1701 let clip_y = matches!(style.overflow_y, Overflow::Hidden) || scroll.is_some();
1702 let clip_x = matches!(style.overflow_x, Overflow::Hidden) || scroll.is_some();
1703 if clip_x || clip_y {
1704 candidates.push(e);
1705 }
1706 }
1707 }
1708 if candidates.is_empty() {
1709 let prior = std::mem::take(&mut render.resource_mut::<RenderEntityMap>().clip);
1711 for re in prior.values() {
1712 if let Ok(em) = render.get_entity_mut(*re) {
1713 em.despawn();
1714 }
1715 }
1716 return;
1717 }
1718 let transforms: std::collections::HashMap<Entity, Transform> = {
1720 let mut q = main.query::<(Entity, &Transform)>();
1721 q.iter(main).map(|(e, t)| (e, *t)).collect()
1722 };
1723 let overlay: std::collections::HashSet<Entity> = {
1726 let mut oq = main.query_filtered::<Entity, With<OverlayLayer>>();
1727 oq.iter(main).collect()
1728 };
1729 let pairs: Vec<(Entity, ExtractedClipBox)> = candidates
1730 .into_iter()
1731 .filter_map(|e| {
1732 let t = transforms.get(&e).copied()?;
1733 let own = paint_order_of(e, &parents, &mut depth_cache);
1734 let end = max_desc_order(e, &children, &parents, &overlay, &mut depth_cache);
1735 let radius = main
1736 .get::<crate::components::Visuals>(e)
1737 .map(|v| v.radius)
1738 .unwrap_or(0.0);
1739 let off = offsets.get(&e).copied().unwrap_or(Vec2::ZERO);
1740 Some((
1741 e,
1742 ExtractedClipBox {
1743 origin: t.absolute - off,
1744 size: t.size,
1745 radius,
1746 start_order: own,
1747 end_order: end,
1748 },
1749 ))
1750 })
1751 .collect();
1752 let prior = std::mem::take(&mut render.resource_mut::<RenderEntityMap>().clip);
1754 let mut next: std::collections::HashMap<Entity, Entity> =
1755 std::collections::HashMap::with_capacity(pairs.len());
1756 for (main_e, clip) in pairs {
1757 let reuse = prior
1758 .get(&main_e)
1759 .copied()
1760 .filter(|&re| render.get_entity(re).is_ok());
1761 let render_e = match reuse {
1762 Some(re) => {
1763 render.entity_mut(re).insert(clip);
1764 re
1765 }
1766 None => render.spawn(clip).id(),
1767 };
1768 next.insert(main_e, render_e);
1769 }
1770 for (main_e, render_e) in &prior {
1771 if !next.contains_key(main_e)
1772 && let Ok(em) = render.get_entity_mut(*render_e)
1773 {
1774 em.despawn();
1775 }
1776 }
1777 render.resource_mut::<RenderEntityMap>().clip = next;
1778}
1779
1780pub fn build_parent_map(
1799 main: &mut World,
1800) -> (
1801 std::collections::HashMap<Entity, Entity>,
1802 std::collections::HashMap<Entity, u32>,
1803) {
1804 if let Some(c) = main.get_resource::<ExtractContextCache>()
1807 && c.active
1808 && let Some(pm) = &c.parent_map
1809 {
1810 return pm.clone();
1811 }
1812 use bevy_ecs::hierarchy::{ChildOf, Children};
1813 let mut pq = main.query::<(Entity, &ChildOf)>();
1814 let parents: std::collections::HashMap<Entity, Entity> =
1815 pq.iter(main).map(|(e, p)| (e, p.parent())).collect();
1816 let z_of: std::collections::HashMap<Entity, i32> = {
1819 let mut zq = main.query::<(Entity, &crate::components::ZIndex)>();
1820 zq.iter(main).map(|(e, z)| (e, z.0)).collect()
1821 };
1822 let children: std::collections::HashMap<Entity, Vec<Entity>> = {
1827 let mut cq = main.query::<(Entity, &Children)>();
1828 cq.iter(main)
1829 .map(|(e, kids)| {
1830 let mut list: Vec<Entity> = kids.iter().collect();
1831 if !z_of.is_empty() {
1832 list.sort_by_key(|c| z_of.get(c).copied().unwrap_or(0));
1833 }
1834 (e, list)
1835 })
1836 .collect()
1837 };
1838 let overlay_roots: Vec<Entity> = {
1840 let mut oq = main.query_filtered::<Entity, With<OverlayLayer>>();
1841 oq.iter(main).collect()
1842 };
1843 let overlay_set: std::collections::HashSet<Entity> = overlay_roots.iter().copied().collect();
1844 let mut roots: Vec<Entity> = children
1847 .keys()
1848 .copied()
1849 .filter(|e| !parents.contains_key(e))
1850 .collect();
1851 roots.sort_by_key(|e| e.to_bits());
1852 let mut order: std::collections::HashMap<Entity, u32> =
1855 std::collections::HashMap::with_capacity(parents.len() + roots.len());
1856 let mut stack: Vec<Entity> = roots
1857 .into_iter()
1858 .rev()
1859 .filter(|e| !overlay_set.contains(e))
1860 .collect();
1861 let mut rank: u32 = 0;
1862 while let Some(e) = stack.pop() {
1863 order.insert(e, rank.saturating_mul(2));
1864 rank = rank.saturating_add(1);
1865 if let Some(kids) = children.get(&e) {
1866 stack.extend(
1867 kids.iter()
1868 .rev()
1869 .copied()
1870 .filter(|k| !overlay_set.contains(k)),
1871 );
1872 }
1873 }
1874 if !overlay_roots.is_empty() {
1875 rank_overlay_band(
1876 main,
1877 &parents,
1878 &children,
1879 &overlay_roots,
1880 &overlay_set,
1881 &mut order,
1882 );
1883 }
1884 let result = (parents, order);
1885 if let Some(mut c) = main.get_resource_mut::<ExtractContextCache>()
1886 && c.active
1887 {
1888 c.parent_map = Some(result.clone());
1889 }
1890 result
1891}
1892
1893fn max_desc_order(
1897 root: Entity,
1898 children: &HashMap<Entity, Vec<Entity>>,
1899 parents: &HashMap<Entity, Entity>,
1900 overlay: &std::collections::HashSet<Entity>,
1901 depth_cache: &mut HashMap<Entity, u32>,
1902) -> PaintOrder {
1903 let mut stack = vec![root];
1904 let mut best = paint_order_of(root, parents, depth_cache);
1905 while let Some(n) = stack.pop() {
1906 let order = paint_order_of(n, parents, depth_cache);
1907 if order > best {
1908 best = order;
1909 }
1910 if let Some(kids) = children.get(&n) {
1911 for &k in kids {
1912 if !overlay.contains(&k) {
1913 stack.push(k);
1914 }
1915 }
1916 }
1917 }
1918 best
1919}
1920
1921#[derive(Clone, Debug, Default)]
1924pub struct OpacityModel {
1925 pub draw: HashMap<Entity, f32>,
1927 pub groups: Vec<(Entity, f32)>,
1929}
1930
1931pub fn opacity_model(main: &mut World, parents: &HashMap<Entity, Entity>) -> OpacityModel {
1941 if let Some(c) = main.get_resource::<ExtractContextCache>()
1942 && c.active
1943 && let Some(v) = &c.opacities
1944 {
1945 return v.clone();
1946 }
1947 let direct: HashMap<Entity, f32> = {
1948 let mut q = main.query::<(Entity, &Opacity)>();
1949 q.iter(main)
1950 .filter(|(_, o)| o.0 < 1.0)
1951 .map(|(e, o)| (e, o.0.max(0.0)))
1952 .collect()
1953 };
1954 let model = if direct.is_empty() {
1955 OpacityModel::default()
1956 } else {
1957 let hidden = hidden_entities(main, parents);
1958 let overlay: std::collections::HashSet<Entity> = {
1959 let mut q = main.query_filtered::<Entity, With<OverlayLayer>>();
1960 q.iter(main).collect()
1961 };
1962 let mut children: HashMap<Entity, Vec<Entity>> = HashMap::new();
1963 for (&e, &p) in parents {
1964 children.entry(p).or_default().push(e);
1965 }
1966 let mut groups: Vec<(Entity, f32)> = direct
1967 .iter()
1968 .filter(|(e, _)| !hidden.contains(e))
1969 .filter(|(e, _)| {
1970 children.get(e).is_some_and(|kids| {
1971 kids.iter()
1972 .any(|k| !hidden.contains(k) && !overlay.contains(k))
1973 })
1974 })
1975 .map(|(&e, &a)| (e, a))
1976 .collect();
1977 groups.sort_by_key(|(e, _)| *e);
1978 let grouped: std::collections::HashSet<Entity> = groups.iter().map(|(e, _)| *e).collect();
1979
1980 fn chain(
1982 e: Entity,
1983 parents: &HashMap<Entity, Entity>,
1984 direct: &HashMap<Entity, f32>,
1985 memo: &mut HashMap<Entity, f32>,
1986 ) -> f32 {
1987 if let Some(v) = memo.get(&e) {
1988 return *v;
1989 }
1990 let above = parents
1991 .get(&e)
1992 .map(|p| chain(*p, parents, direct, memo))
1993 .unwrap_or(1.0);
1994 let total = above * direct.get(&e).copied().unwrap_or(1.0);
1995 memo.insert(e, total);
1996 total
1997 }
1998 fn outer(
2001 e: Entity,
2002 parents: &HashMap<Entity, Entity>,
2003 direct: &HashMap<Entity, f32>,
2004 overlay: &std::collections::HashSet<Entity>,
2005 chains: &mut HashMap<Entity, f32>,
2006 memo: &mut HashMap<Entity, f32>,
2007 ) -> f32 {
2008 if let Some(v) = memo.get(&e) {
2009 return *v;
2010 }
2011 let value = if overlay.contains(&e) {
2012 parents
2013 .get(&e)
2014 .map(|p| chain(*p, parents, direct, chains))
2015 .unwrap_or(1.0)
2016 } else {
2017 parents
2018 .get(&e)
2019 .map(|p| outer(*p, parents, direct, overlay, chains, memo))
2020 .unwrap_or(1.0)
2021 };
2022 memo.insert(e, value);
2023 value
2024 }
2025 let mut chains = HashMap::new();
2026 let mut outers = HashMap::new();
2027 let mut draw = HashMap::new();
2028 let entities: Vec<Entity> = parents
2029 .keys()
2030 .chain(parents.values())
2031 .chain(direct.keys())
2032 .copied()
2033 .collect();
2034 for e in entities {
2035 if draw.contains_key(&e) {
2036 continue;
2037 }
2038 let own = if grouped.contains(&e) {
2039 1.0
2040 } else {
2041 direct.get(&e).copied().unwrap_or(1.0)
2042 };
2043 let alpha = own * outer(e, parents, &direct, &overlay, &mut chains, &mut outers);
2044 if alpha < 1.0 {
2045 draw.insert(e, alpha);
2046 }
2047 }
2048 OpacityModel { draw, groups }
2049 };
2050 if let Some(mut c) = main.get_resource_mut::<ExtractContextCache>()
2051 && c.active
2052 {
2053 c.opacities = Some(model.clone());
2054 }
2055 model
2056}
2057
2058pub fn draw_opacity(draw: &HashMap<Entity, f32>, e: Entity) -> Opacity {
2060 Opacity(draw.get(&e).copied().unwrap_or(1.0))
2061}
2062
2063pub fn extract_opacity_groups(main: &mut World, render: &mut World) {
2067 let (parents, mut depth_cache) = build_parent_map(main);
2068 let groups = opacity_model(main, &parents).groups;
2069 let mut pairs: Vec<(Entity, ExtractedOpacityGroup)> = Vec::with_capacity(groups.len());
2070 if !groups.is_empty() {
2071 let overlay: std::collections::HashSet<Entity> = {
2072 let mut q = main.query_filtered::<Entity, With<OverlayLayer>>();
2073 q.iter(main).collect()
2074 };
2075 let mut children: HashMap<Entity, Vec<Entity>> = HashMap::new();
2076 for (&e, &p) in &parents {
2077 children.entry(p).or_default().push(e);
2078 }
2079 for (e, alpha) in groups {
2080 let own = paint_order_of(e, &parents, &mut depth_cache);
2081 let end = max_desc_order(e, &children, &parents, &overlay, &mut depth_cache);
2082 pairs.push((
2083 e,
2084 ExtractedOpacityGroup {
2085 alpha,
2086 start_order: own.saturating_sub(1),
2087 end_order: end,
2088 key: e.to_bits(),
2089 },
2090 ));
2091 }
2092 }
2093 let prior = std::mem::take(&mut render.resource_mut::<RenderEntityMap>().opacity_group);
2095 let mut next: HashMap<Entity, Entity> = HashMap::with_capacity(pairs.len());
2096 for (main_e, group) in pairs {
2097 let reuse = prior
2098 .get(&main_e)
2099 .copied()
2100 .filter(|&re| render.get_entity(re).is_ok());
2101 let render_e = match reuse {
2102 Some(re) => {
2103 render.entity_mut(re).insert(group);
2104 re
2105 }
2106 None => render.spawn(group).id(),
2107 };
2108 next.insert(main_e, render_e);
2109 }
2110 for (main_e, render_e) in &prior {
2111 if !next.contains_key(main_e)
2112 && let Ok(em) = render.get_entity_mut(*render_e)
2113 {
2114 em.despawn();
2115 }
2116 }
2117 render.resource_mut::<RenderEntityMap>().opacity_group = next;
2118}
2119
2120fn rank_overlay_band(
2129 main: &mut World,
2130 parents: &std::collections::HashMap<Entity, Entity>,
2131 children: &std::collections::HashMap<Entity, Vec<Entity>>,
2132 overlay_roots: &[Entity],
2133 overlay_set: &std::collections::HashSet<Entity>,
2134 order: &mut std::collections::HashMap<Entity, u32>,
2135) {
2136 let hidden = hidden_entities(main, parents);
2137 if main.get_resource::<OverlayOpenOrder>().is_none() {
2138 main.insert_resource(OverlayOpenOrder::default());
2139 }
2140 let ordered_roots: Vec<Entity> = {
2141 let mut oo = main.resource_mut::<OverlayOpenOrder>();
2142 let oo = &mut *oo;
2143 oo.stamps
2145 .retain(|e, _| overlay_set.contains(e) && !hidden.contains(e));
2146 let mut newly_open: Vec<Entity> = overlay_roots
2149 .iter()
2150 .copied()
2151 .filter(|e| !hidden.contains(e) && !oo.stamps.contains_key(e))
2152 .collect();
2153 newly_open.sort_by_key(|e| e.to_bits());
2154 for e in newly_open {
2155 let s = oo.next;
2156 oo.next += 1;
2157 oo.stamps.insert(e, s);
2158 }
2159 let mut stamped: Vec<(u64, Entity)> = oo.stamps.iter().map(|(&e, &s)| (s, e)).collect();
2160 stamped.sort_unstable_by_key(|&(s, e)| (s, e.to_bits()));
2161 let mut list: Vec<Entity> = stamped.into_iter().map(|(_, e)| e).collect();
2162 let mut closed: Vec<Entity> = overlay_roots
2163 .iter()
2164 .copied()
2165 .filter(|e| hidden.contains(e))
2166 .collect();
2167 closed.sort_by_key(|e| e.to_bits());
2168 list.extend(closed);
2169 list
2170 };
2171 let mut orank: u32 = OVERLAY_ORDER_BASE / 2;
2173 for root in ordered_roots {
2174 let mut stack = vec![root];
2175 while let Some(e) = stack.pop() {
2176 order.insert(e, orank.saturating_mul(2));
2177 orank = orank.saturating_add(1);
2178 if let Some(kids) = children.get(&e) {
2179 stack.extend(
2180 kids.iter()
2181 .rev()
2182 .copied()
2183 .filter(|k| !overlay_set.contains(k)),
2184 );
2185 }
2186 }
2187 }
2188}
2189
2190pub fn parent_scroll_offsets(
2196 main: &mut World,
2197 parents: &HashMap<Entity, Entity>,
2198) -> HashMap<Entity, Vec2> {
2199 if let Some(c) = main.get_resource::<ExtractContextCache>()
2200 && c.active
2201 && let Some(v) = &c.scroll
2202 {
2203 return v.clone();
2204 }
2205 let direct: HashMap<Entity, Vec2> = {
2206 let mut q = main.query::<(Entity, &ScrollOffset)>();
2207 q.iter(main).map(|(e, o)| (e, o.0)).collect()
2208 };
2209 let by_entity: HashMap<Entity, Vec2> = if direct.is_empty() {
2210 HashMap::new()
2211 } else {
2212 fn cumulative(
2213 e: Entity,
2214 parents: &HashMap<Entity, Entity>,
2215 direct: &HashMap<Entity, Vec2>,
2216 cache: &mut HashMap<Entity, Vec2>,
2217 ) -> Vec2 {
2218 if let Some(v) = cache.get(&e) {
2219 return *v;
2220 }
2221 let parent_off = parents
2222 .get(&e)
2223 .map(|p| cumulative(*p, parents, direct, cache))
2224 .unwrap_or(Vec2::ZERO);
2225 let own = direct.get(&e).copied().unwrap_or(Vec2::ZERO);
2226 let total = parent_off + own;
2227 cache.insert(e, total);
2228 total
2229 }
2230 let mut cache: HashMap<Entity, Vec2> = HashMap::new();
2231 let mut by_entity: HashMap<Entity, Vec2> = HashMap::new();
2232 for &e in parents.keys() {
2233 if let Some(p) = parents.get(&e) {
2234 let off = cumulative(*p, parents, &direct, &mut cache);
2235 if off != Vec2::ZERO {
2236 by_entity.insert(e, off);
2237 }
2238 }
2239 }
2240 by_entity
2241 };
2242 if let Some(mut c) = main.get_resource_mut::<ExtractContextCache>()
2243 && c.active
2244 {
2245 c.scroll = Some(by_entity.clone());
2246 }
2247 by_entity
2248}
2249
2250pub fn hidden_entities(
2257 main: &mut World,
2258 parents: &std::collections::HashMap<Entity, Entity>,
2259) -> std::collections::HashSet<Entity> {
2260 if let Some(c) = main.get_resource::<ExtractContextCache>()
2261 && c.active
2262 && let Some(v) = &c.hidden
2263 {
2264 return v.clone();
2265 }
2266 let mut hide_roots = std::collections::HashSet::new();
2267 let mut q = main.query::<(Entity, &Visible)>();
2268 for (e, v) in q.iter(main) {
2269 if !v.0 {
2270 hide_roots.insert(e);
2271 }
2272 }
2273 let mut styles = main.query::<(Entity, &Style)>();
2277 for (e, style) in styles.iter(main) {
2278 if matches!(style.display, Display::None) {
2279 hide_roots.insert(e);
2280 }
2281 }
2282 let hidden: std::collections::HashSet<Entity> = if hide_roots.is_empty() {
2283 std::collections::HashSet::new()
2284 } else {
2285 let mut cache: std::collections::HashMap<Entity, bool> = std::collections::HashMap::new();
2286 let mut hidden = std::collections::HashSet::new();
2287 for &entity in parents.keys().chain(hide_roots.iter()) {
2288 if is_hidden_walk(entity, &hide_roots, parents, &mut cache) {
2289 hidden.insert(entity);
2290 }
2291 }
2292 hidden
2293 };
2294 if let Some(mut c) = main.get_resource_mut::<ExtractContextCache>()
2295 && c.active
2296 {
2297 c.hidden = Some(hidden.clone());
2298 }
2299 hidden
2300}
2301
2302fn is_hidden_walk(
2303 e: Entity,
2304 roots: &std::collections::HashSet<Entity>,
2305 parents: &std::collections::HashMap<Entity, Entity>,
2306 cache: &mut std::collections::HashMap<Entity, bool>,
2307) -> bool {
2308 if let Some(v) = cache.get(&e) {
2309 return *v;
2310 }
2311 let v = if roots.contains(&e) {
2312 true
2313 } else {
2314 match parents.get(&e) {
2315 Some(p) => is_hidden_walk(*p, roots, parents, cache),
2316 None => false,
2317 }
2318 };
2319 cache.insert(e, v);
2320 v
2321}
2322
2323pub fn paint_order_of(
2331 e: Entity,
2332 _parents: &std::collections::HashMap<Entity, Entity>,
2333 cache: &mut std::collections::HashMap<Entity, u32>,
2334) -> PaintOrder {
2335 if let Some(o) = cache.get(&e) {
2336 return *o;
2337 }
2338 let idx = (e.to_bits() as u32) & 0x3FFF_FFFF;
2339 let o = 0x8000_0000 | (idx << 1);
2340 cache.insert(e, o);
2341 o
2342}
2343
2344#[derive(Clone, Debug)]
2346pub struct SurfaceFrame {
2347 pub width: u32,
2349 pub height: u32,
2351 pub rgba8: Vec<u8>,
2353}
2354
2355#[derive(Resource, Clone, Default)]
2360pub struct SurfaceCapture {
2361 pub request: Arc<AtomicBool>,
2363 pub store: Arc<Mutex<Option<SurfaceFrame>>>,
2365 pub waker: Arc<std::sync::OnceLock<crate::app::EventLoopWaker>>,
2375}
2376
2377impl SurfaceCapture {
2378 pub fn is_requested(&self) -> bool {
2380 self.request.load(Ordering::Acquire)
2381 }
2382
2383 pub fn set_waker(&self, waker: crate::app::EventLoopWaker) {
2386 let _ = self.waker.set(waker);
2387 }
2388
2389 pub fn request(&self) {
2394 self.request.store(true, Ordering::Release);
2395 if let Some(waker) = self.waker.get() {
2396 waker.wake();
2397 }
2398 }
2399
2400 pub fn clear_request(&self) {
2402 self.request.store(false, Ordering::Release);
2403 }
2404
2405 pub fn write(&self, frame: SurfaceFrame) {
2407 if let Ok(mut slot) = self.store.lock() {
2408 *slot = Some(frame);
2409 }
2410 }
2411
2412 pub fn read(&self) -> Option<SurfaceFrame> {
2414 self.store.lock().ok().and_then(|g| g.clone())
2415 }
2416}
2417
2418fn masked_offset(text: &str, plain_byte: usize, mask: char) -> usize {
2423 let mut b = plain_byte.min(text.len());
2424 while b > 0 && !text.is_char_boundary(b) {
2425 b -= 1;
2426 }
2427 text[..b].chars().count() * mask.len_utf8()
2428}
2429
2430fn mask_echo(
2440 mode: EchoMode,
2441 text: &str,
2442 caret: Option<usize>,
2443 selection: Option<(usize, usize)>,
2444 mask: char,
2445) -> (String, Option<usize>, Option<(usize, usize)>) {
2446 match mode {
2447 EchoMode::NoEcho => (String::new(), caret.map(|_| 0), None),
2448 EchoMode::Password => {
2449 let scalars = text.chars().count();
2450 let masked: String = mask.to_string().repeat(scalars);
2451 let caret = caret.map(|c| masked_offset(text, c, mask));
2452 let selection = selection
2453 .map(|(s, e)| (masked_offset(text, s, mask), masked_offset(text, e, mask)));
2454 (masked, caret, selection)
2455 }
2456 EchoMode::Normal => (text.to_string(), caret, selection),
2457 }
2458}
2459
2460fn text_run_align(align: TextAlign, style: Option<&Style>) -> TextAlign {
2479 if align != TextAlign::Start {
2480 return align;
2481 }
2482 let Some(style) = style else {
2483 return align;
2484 };
2485 let placed = match style.justify {
2486 FlexJustify::Center | FlexJustify::SpaceAround | FlexJustify::SpaceEvenly => {
2490 TextAlign::Center
2491 }
2492 FlexJustify::End => TextAlign::End,
2493 FlexJustify::Start | FlexJustify::SpaceBetween => TextAlign::Start,
2494 };
2495 match style.flex_direction {
2496 FlexDirection::Row => placed,
2497 FlexDirection::RowReverse => match placed {
2499 TextAlign::Start => TextAlign::End,
2500 TextAlign::End => TextAlign::Start,
2501 TextAlign::Center => TextAlign::Center,
2502 },
2503 FlexDirection::Column | FlexDirection::ColumnReverse => align,
2504 }
2505}
2506
2507pub fn extract_text(main: &mut World, render: &mut World) {
2514 let (parents, mut depth_cache) = build_parent_map(main);
2515 let hidden = hidden_entities(main, &parents);
2516 let scroll = parent_scroll_offsets(main, &parents);
2517 let alphas = opacity_model(main, &parents).draw;
2518 let clip = parent_scroll_clip_rects(main, &parents);
2519 let caret_visible = main
2524 .get_resource::<CaretBlink>()
2525 .map(|b| b.visible)
2526 .unwrap_or(true);
2527 type RowFor<'a> = (
2528 Entity,
2529 &'a Transform,
2530 &'a TextContent,
2531 Option<&'a TextStyle>,
2532 Option<&'a ImeState>,
2533 Option<&'a TextInput>,
2534 Option<&'a Focused>,
2535 Option<&'a Style>,
2536 Option<&'a TextInputScroll>,
2537 Option<&'a EchoMode>,
2538 Option<&'a TextInputPaint>,
2539 Option<&'a TextBlockOrigin>,
2540 Option<&'a CaretWidth>,
2541 Option<&'a PasswordCharacter>,
2542 );
2543 let mut q = main.query::<RowFor>();
2544 let pairs: Vec<(Entity, ExtractedText)> = q
2545 .iter(main)
2546 .filter(|(e, ..)| !hidden.contains(e))
2547 .filter_map(
2548 |(
2549 e,
2550 t,
2551 text,
2552 ts,
2553 ime,
2554 input,
2555 focused,
2556 style,
2557 edit_scroll,
2558 echo,
2559 paint,
2560 block_origin,
2561 caret_width,
2562 password_char,
2563 )| {
2564 let ts = ts.cloned().unwrap_or_default();
2565 let size_px = ts.size_px;
2566 let line_height_px = resolve_line_height(ts.line_height, size_px);
2567 let caret_width_px = caret_width.map(|w| w.0).unwrap_or(CARET_WIDTH_PX);
2568 let mask_char = password_char.map(|c| c.0).unwrap_or(PASSWORD_MASK_CHAR);
2569 let preedit = ime.map(|i| i.preedit.as_str()).unwrap_or("");
2570 let placeholder = match input {
2574 Some(i) if text.0.is_empty() && preedit.is_empty() => i.placeholder.as_str(),
2575 _ => "",
2576 };
2577 if input.is_none() && text.0.is_empty() && preedit.is_empty() {
2578 return None;
2579 }
2580 let caret = match (input, focused) {
2581 (Some(i), Some(_)) if caret_visible => {
2582 let base = i.cursor.min(text.0.len());
2584 Some(base + preedit.len())
2585 }
2586 _ => None,
2587 };
2588 let caret = caret.map(|c| if placeholder.is_empty() { c } else { 0 });
2591 let combined = if !placeholder.is_empty() {
2592 placeholder.to_string()
2593 } else {
2594 format!("{}{}", text.0, preedit)
2595 };
2596 let selection = match (input, focused) {
2598 (Some(i), Some(_)) => i.selection_anchor.and_then(|a| {
2599 let cur = i.cursor.min(text.0.len());
2600 let a = a.min(text.0.len());
2601 if a == cur {
2602 None
2603 } else {
2604 Some((a.min(cur), a.max(cur)))
2605 }
2606 }),
2607 _ => None,
2608 };
2609 let (combined, caret, selection) = match echo {
2617 Some(mode) if mode.is_concealed() && placeholder.is_empty() => {
2618 mask_echo(*mode, &combined, caret, selection, mask_char)
2619 }
2620 _ => (combined, caret, selection),
2621 };
2622 let pad_left = style.map(|s| s.padding.left).unwrap_or(0.0);
2627 let pad_right = style.map(|s| s.padding.right).unwrap_or(0.0);
2628 let pad_top = style.map(|s| s.padding.top).unwrap_or(0.0);
2629 let pad_bottom = style.map(|s| s.padding.bottom).unwrap_or(0.0);
2630 let inner_h = (t.size.y - pad_top - pad_bottom).max(size_px);
2631 let block_top = block_origin.map(|b| b.top).unwrap_or_else(|| {
2632 let stacked = input.is_some_and(|i| i.multiline) || combined.contains('\n');
2633 text_block_top(inner_h, line_height_px, stacked)
2634 });
2635 let baseline_y = t.absolute.y
2636 + pad_top
2637 + block_top
2638 + text_baseline_in_line(size_px, line_height_px);
2639 let container_width = (t.size.x - pad_left - pad_right).max(0.0);
2640 let alpha = draw_opacity(&alphas, e);
2641 let off = scroll.get(&e).copied().unwrap_or(Vec2::ZERO);
2642 if let Some(clip_rect) = clip.get(&e) {
2644 let probe_origin = Vec2::new(t.absolute.x, t.absolute.y) - off;
2645 if aabb_outside(probe_origin, t.size, *clip_rect) {
2646 return None;
2647 }
2648 }
2649 let edit_off = edit_scroll.map(|s| s.offset).unwrap_or(Vec2::ZERO);
2653 Some((
2654 e,
2655 ExtractedText {
2656 origin: Vec2::new(t.absolute.x + pad_left, baseline_y) - off - edit_off,
2657 text: combined,
2658 size_px,
2659 fill: alpha.apply(ts.color),
2660 caret,
2661 selection,
2662 selection_color: ts.selection_color.map(|c| alpha.apply(c)),
2663 selection_foreground: paint
2664 .and_then(|p| p.selection_foreground)
2665 .map(|c| alpha.apply(c)),
2666 caret_color: paint.and_then(|p| p.caret_color).map(|c| alpha.apply(c)),
2667 container_width,
2668 align: text_run_align(ts.align, style),
2669 wrap: ts.wrap,
2670 max_lines: ts.max_lines,
2671 family: ts.family.clone(),
2672 weight: ts.weight,
2673 line_height_px,
2674 caret_width_px,
2675 order: paint_order_of(e, &parents, &mut depth_cache),
2676 },
2677 ))
2678 },
2679 )
2680 .collect();
2681 let prior = std::mem::take(&mut render.resource_mut::<RenderEntityMap>().text);
2683 let mut next: std::collections::HashMap<Entity, Entity> =
2684 std::collections::HashMap::with_capacity(pairs.len());
2685 for (main_e, et) in pairs {
2686 let reuse = prior
2687 .get(&main_e)
2688 .copied()
2689 .filter(|&re| render.get_entity(re).is_ok());
2690 let render_e = match reuse {
2691 Some(re) => {
2692 render.entity_mut(re).insert(et);
2693 re
2694 }
2695 None => render.spawn(et).id(),
2696 };
2697 next.insert(main_e, render_e);
2698 }
2699 for (main_e, render_e) in &prior {
2700 if !next.contains_key(main_e)
2701 && let Ok(em) = render.get_entity_mut(*render_e)
2702 {
2703 em.despawn();
2704 }
2705 }
2706 render.resource_mut::<RenderEntityMap>().text = next;
2707}
2708
2709#[cfg(test)]
2710mod echo_mask_tests {
2711 use super::*;
2715
2716 #[test]
2717 fn password_masks_each_scalar_and_remaps_caret() {
2718 let (disp, caret, sel) =
2721 mask_echo(EchoMode::Password, "abc", Some(2), None, PASSWORD_MASK_CHAR);
2722 assert_eq!(disp.chars().count(), 3);
2723 assert!(disp.chars().all(|c| c == PASSWORD_MASK_CHAR));
2724 assert_eq!(caret, Some(6));
2725 assert_eq!(sel, None);
2726 }
2727
2728 #[test]
2729 fn password_remaps_selection_range() {
2730 let (_disp, _caret, sel) = mask_echo(
2732 EchoMode::Password,
2733 "abc",
2734 None,
2735 Some((1, 3)),
2736 PASSWORD_MASK_CHAR,
2737 );
2738 assert_eq!(sel, Some((3, 9)));
2739 }
2740
2741 #[test]
2742 fn password_scalar_count_not_byte_count() {
2743 let (disp, caret, _) = mask_echo(
2746 EchoMode::Password,
2747 "\u{e9}",
2748 Some(2),
2749 None,
2750 PASSWORD_MASK_CHAR,
2751 );
2752 assert_eq!(disp.chars().count(), 1);
2753 assert_eq!(caret, Some(3));
2754 }
2755
2756 #[test]
2761 fn password_character_override_changes_mask_glyph_and_width() {
2762 let (disp, caret, _) = mask_echo(EchoMode::Password, "abc", Some(2), None, '*');
2763 assert_eq!(disp, "***");
2764 assert_eq!(caret, Some(2), "1-byte mask -> caret byte == scalar count");
2765 }
2766
2767 #[test]
2768 fn masked_offset_snaps_mid_codepoint_down() {
2769 assert_eq!(EchoMode::Password.display_offset("\u{e9}", 1), 0);
2771 }
2772
2773 #[test]
2774 fn masked_offset_round_trips_to_the_plaintext_byte() {
2775 let plain = "a\u{e9}bc";
2778 for (i, _) in plain
2779 .char_indices()
2780 .chain(std::iter::once((plain.len(), 'x')))
2781 {
2782 let d = EchoMode::Password.display_offset(plain, i);
2783 assert_eq!(EchoMode::Password.plain_offset(plain, d), i);
2784 }
2785 }
2786
2787 #[test]
2788 fn no_echo_hides_everything_and_collapses_caret() {
2789 let (disp, caret, sel) = mask_echo(
2790 EchoMode::NoEcho,
2791 "secret",
2792 Some(4),
2793 Some((0, 6)),
2794 PASSWORD_MASK_CHAR,
2795 );
2796 assert!(disp.is_empty());
2797 assert_eq!(caret, Some(0), "caret collapses to the origin");
2798 assert_eq!(sel, None, "nothing to highlight under no-echo");
2799 }
2800
2801 #[test]
2805 fn extract_masks_password_run_but_not_placeholder() {
2806 use crate::components::{TextContent, TextInput, Transform};
2807 use crate::input::Focused;
2808
2809 fn masked_text_for(content: &str, placeholder: &str) -> String {
2810 let mut main = World::new();
2811 let mut render = World::new();
2812 render.init_resource::<RenderEntityMap>();
2813 let e = main
2814 .spawn((
2815 Transform::new(Vec2::ZERO, Vec2::new(120.0, 24.0)),
2816 TextContent(content.to_string()),
2817 TextInput {
2818 placeholder: placeholder.to_string(),
2819 cursor: content.len(),
2820 ..Default::default()
2821 },
2822 EchoMode::Password,
2823 Focused,
2824 ))
2825 .id();
2826 let _ = e;
2827 extract_text(&mut main, &mut render);
2828 let mut q = render.query::<&ExtractedText>();
2829 q.iter(&render).next().unwrap().text.clone()
2830 }
2831
2832 let masked = masked_text_for("hunter2", "");
2834 assert_eq!(masked.chars().count(), 7);
2835 assert!(masked.chars().all(|c| c == PASSWORD_MASK_CHAR));
2836
2837 assert_eq!(masked_text_for("", "Password"), "Password");
2840 }
2841
2842 #[test]
2847 fn extract_password_character_override_replaces_default_mask() {
2848 use crate::components::{TextContent, TextInput, Transform};
2849 use crate::input::Focused;
2850
2851 let mut main = World::new();
2852 let mut render = World::new();
2853 render.init_resource::<RenderEntityMap>();
2854 main.spawn((
2855 Transform::new(Vec2::ZERO, Vec2::new(120.0, 24.0)),
2856 TextContent("hunter2".to_string()),
2857 TextInput {
2858 cursor: 7,
2859 ..Default::default()
2860 },
2861 EchoMode::Password,
2862 PasswordCharacter('*'),
2863 Focused,
2864 ));
2865 extract_text(&mut main, &mut render);
2866 let mut q = render.query::<&ExtractedText>();
2867 let text = q.iter(&render).next().unwrap().text.clone();
2868 assert_eq!(text, "*******");
2869 }
2870}
2871
2872#[cfg(test)]
2873mod tests {
2874 use super::*;
2875 use bevy_ecs::hierarchy::ChildOf;
2876
2877 #[test]
2883 fn hidden_root_subtree_extracts_zero_paint_nodes() {
2884 use crate::components::{Fill, TextContent, Transform, Visible, Visuals};
2885
2886 let mut main = World::new();
2887 let mut render = World::new();
2888 render.insert_resource(RenderEntityMap::default());
2889
2890 let base = main
2892 .spawn((
2893 Transform {
2894 absolute: Vec2::ZERO,
2895 size: Vec2::new(100.0, 100.0),
2896 baseline_y: None,
2897 },
2898 Visuals {
2899 fill: Some(Fill::Solid(Color::rgba(1.0, 0.0, 0.0, 1.0))),
2900 ..Default::default()
2901 },
2902 ))
2903 .id();
2904
2905 let overlay = main
2907 .spawn((
2908 Transform {
2909 absolute: Vec2::ZERO,
2910 size: Vec2::new(100.0, 100.0),
2911 baseline_y: None,
2912 },
2913 Visuals {
2914 fill: Some(Fill::Solid(Color::rgba(0.0, 1.0, 0.0, 1.0))),
2915 ..Default::default()
2916 },
2917 Visible(false),
2918 ))
2919 .id();
2920 let panel = main
2921 .spawn((
2922 Transform {
2923 absolute: Vec2::new(10.0, 10.0),
2924 size: Vec2::new(50.0, 50.0),
2925 baseline_y: None,
2926 },
2927 Visuals {
2928 fill: Some(Fill::Solid(Color::rgba(0.0, 0.0, 1.0, 1.0))),
2929 ..Default::default()
2930 },
2931 ChildOf(overlay),
2932 ))
2933 .id();
2934 let _label = main
2935 .spawn((
2936 Transform {
2937 absolute: Vec2::new(12.0, 12.0),
2938 size: Vec2::new(40.0, 16.0),
2939 baseline_y: None,
2940 },
2941 TextContent("devtools".into()),
2942 ChildOf(panel),
2943 ))
2944 .id();
2945
2946 extract_rects(&mut main, &mut render);
2947 extract_text(&mut main, &mut render);
2948
2949 let rects: Vec<(Entity, Vec2)> = {
2951 let mut q = render.query::<(Entity, &ExtractedRect)>();
2952 q.iter(&render).map(|(e, r)| (e, r.origin)).collect()
2953 };
2954 assert_eq!(
2955 rects.len(),
2956 1,
2957 "only the visible base rect extracts; the hidden subtree contributes none (got {rects:?})"
2958 );
2959 let map = render.resource::<RenderEntityMap>();
2961 assert_eq!(map.rect.get(&base).copied(), Some(rects[0].0));
2962 assert!(!map.rect.contains_key(&overlay));
2963 assert!(!map.rect.contains_key(&panel));
2964
2965 let text_count = render.query::<&ExtractedText>().iter(&render).count();
2967 assert_eq!(text_count, 0, "hidden subtree text must not extract");
2968 }
2969
2970 #[test]
2975 fn adjacent_rects_at_a_fractional_offset_share_a_pixel_edge() {
2976 use crate::components::{Fill, Transform, Visuals};
2977 let mut main = World::new();
2978 let mut render = World::new();
2979 render.insert_resource(RenderEntityMap::default());
2980 main.insert_resource(Viewport {
2981 scale_factor: 2.0,
2982 ..Viewport::default()
2983 });
2984 let row = |y: f32| {
2985 (
2986 Transform {
2987 absolute: Vec2::new(0.0, y),
2988 size: Vec2::new(100.0, 30.0),
2989 baseline_y: None,
2990 },
2991 Visuals {
2992 fill: Some(Fill::Solid(Color::rgba(0.2, 0.3, 0.4, 1.0))),
2993 ..Default::default()
2994 },
2995 )
2996 };
2997 main.spawn(row(-1693.68 + 1680.0));
2998 main.spawn(row(-1693.68 + 1710.0));
2999 extract_rects(&mut main, &mut render);
3000 let mut edges: Vec<(f32, f32)> = render
3001 .query::<&ExtractedRect>()
3002 .iter(&render)
3003 .map(|r| (r.origin.y, r.origin.y + r.size.y))
3004 .collect();
3005 edges.sort_by(|a, b| a.0.total_cmp(&b.0));
3006 assert_eq!(edges[0].1, edges[1].0, "one shared edge: {edges:?}");
3007 for (top, bottom) in edges {
3008 assert_eq!((top * 2.0).fract(), 0.0, "top on a device pixel");
3009 assert_eq!((bottom * 2.0).fract(), 0.0, "bottom on a device pixel");
3010 }
3011 }
3012
3013 #[test]
3015 fn snapping_keeps_a_hairline() {
3016 let (origin, size) = snap_to_device_pixels(Vec2::new(0.2, 0.2), Vec2::new(10.0, 0.3), 1.0);
3017 assert_eq!(origin, Vec2::ZERO);
3018 assert_eq!(size, Vec2::new(10.0, 1.0));
3019 }
3020
3021 #[test]
3024 fn inset_shadow_takes_the_element_order() {
3025 use crate::components::{Fill, ShadowSpec, Transform, Visuals};
3026 let mut main = World::new();
3027 let mut render = World::new();
3028 render.insert_resource(RenderEntityMap::default());
3029 let shadow = |inner: bool| ShadowSpec {
3030 offset_x: 0.0,
3031 offset_y: 0.0,
3032 blur: 4.0,
3033 spread: 0.0,
3034 color: Color::rgba(1.0, 0.0, 0.0, 1.0),
3035 inner,
3036 };
3037 main.spawn((
3038 Transform {
3039 absolute: Vec2::ZERO,
3040 size: Vec2::new(50.0, 50.0),
3041 baseline_y: None,
3042 },
3043 Visuals {
3044 fill: Some(Fill::Solid(Color::rgba(1.0, 1.0, 1.0, 1.0))),
3045 shadows: vec![shadow(false), shadow(true)],
3046 ..Default::default()
3047 },
3048 ));
3049 extract_rects(&mut main, &mut render);
3050 extract_shadows(&mut main, &mut render);
3051 let rect = render
3052 .query::<&ExtractedRect>()
3053 .iter(&render)
3054 .next()
3055 .expect("rect")
3056 .order;
3057 let mut shadows: Vec<(bool, PaintOrder)> = render
3058 .query::<&ExtractedShadow>()
3059 .iter(&render)
3060 .map(|s| (s.inner, s.order))
3061 .collect();
3062 shadows.sort();
3063 assert_eq!(shadows, vec![(false, rect - 1), (true, rect)]);
3064 }
3065
3066 #[test]
3070 fn display_none_subtree_extracts_no_text() {
3071 use crate::components::{Display, Style, TextContent, Transform};
3072
3073 let mut main = World::new();
3074 let mut render = World::new();
3075 render.insert_resource(RenderEntityMap::default());
3076 let at = |x: f32, y: f32, w: f32, h: f32| Transform {
3077 absolute: Vec2::new(x, y),
3078 size: Vec2::new(w, h),
3079 baseline_y: None,
3080 };
3081 let root = main.spawn(at(0.0, 0.0, 200.0, 200.0)).id();
3082 let hidden = main
3083 .spawn((
3084 at(0.0, 0.0, 0.0, 0.0),
3085 Style {
3086 display: Display::None,
3087 ..Default::default()
3088 },
3089 ChildOf(root),
3090 ))
3091 .id();
3092 main.spawn((
3093 at(0.0, 0.0, 80.0, 16.0),
3094 TextContent("should be hidden".into()),
3095 ChildOf(hidden),
3096 ));
3097 main.spawn((
3098 at(0.0, 40.0, 80.0, 16.0),
3099 TextContent("shown".into()),
3100 ChildOf(root),
3101 ));
3102
3103 extract_text(&mut main, &mut render);
3104
3105 let texts: Vec<String> = render
3106 .query::<&ExtractedText>()
3107 .iter(&render)
3108 .map(|t| t.text.to_string())
3109 .collect();
3110 assert_eq!(texts, vec!["shown".to_string()]);
3111 }
3112
3113 #[test]
3118 fn cull_hidden_despawns_leaked_render_entities() {
3119 use crate::components::{Transform, Visible};
3120
3121 let mut main = World::new();
3122 let mut render = World::new();
3123 render.insert_resource(RenderEntityMap::default());
3124 render.insert_resource(HiddenExtracts::default());
3125
3126 let hidden_root = main.spawn((Transform::default(), Visible(false))).id();
3127 let child = main
3128 .spawn((Transform::default(), ChildOf(hidden_root)))
3129 .id();
3130
3131 let leaked_rect = render
3134 .spawn(ExtractedRect {
3135 origin: Vec2::ZERO,
3136 size: Vec2::new(10.0, 10.0),
3137 brush: Brush::Solid(Color::rgba(0.0, 1.0, 0.0, 1.0)),
3138 radius: 0.0,
3139 corner_radii: None,
3140 order: 0,
3141 })
3142 .id();
3143 let leaked_img = render
3144 .spawn(ExtractedRect {
3145 origin: Vec2::ZERO,
3146 size: Vec2::new(10.0, 10.0),
3147 brush: Brush::Solid(Color::rgba(0.0, 0.0, 1.0, 1.0)),
3148 radius: 0.0,
3149 corner_radii: None,
3150 order: 0,
3151 })
3152 .id();
3153 {
3154 let mut map = render.resource_mut::<RenderEntityMap>();
3155 map.rect.insert(hidden_root, leaked_rect);
3156 map.image.insert(child, leaked_img);
3157 }
3158
3159 stash_hidden_entities(&mut main, &mut render);
3161 let mut schedule = Schedule::default();
3162 schedule.add_systems(cull_hidden);
3163 schedule.run(&mut render);
3164
3165 assert!(
3166 render.get_entity(leaked_rect).is_err(),
3167 "leaked rect for the hidden root must be despawned"
3168 );
3169 assert!(
3170 render.get_entity(leaked_img).is_err(),
3171 "leaked image for the hidden child must be despawned"
3172 );
3173 let map = render.resource::<RenderEntityMap>();
3174 assert!(map.rect.is_empty(), "hidden slot pruned from rect map");
3175 assert!(map.image.is_empty(), "hidden slot pruned from image map");
3176 }
3177
3178 #[test]
3181 fn paint_order_follows_document_order_not_entity_ids() {
3182 let mut world = World::new();
3183 let child_a = world.spawn_empty().id();
3184 let child_b = world.spawn_empty().id();
3185 let grandchild = world.spawn_empty().id();
3186 let parent = world.spawn_empty().id();
3187 world.entity_mut(child_a).insert(ChildOf(parent));
3189 world.entity_mut(child_b).insert(ChildOf(parent));
3190 world.entity_mut(grandchild).insert(ChildOf(child_a));
3191
3192 let (parents, mut cache) = build_parent_map(&mut world);
3193 let po_parent = paint_order_of(parent, &parents, &mut cache);
3194 let po_a = paint_order_of(child_a, &parents, &mut cache);
3195 let po_gc = paint_order_of(grandchild, &parents, &mut cache);
3196 let po_b = paint_order_of(child_b, &parents, &mut cache);
3197
3198 assert!(
3199 po_parent < po_a && po_a < po_gc && po_gc < po_b,
3200 "expected pre-order parent < child_a < grandchild < child_b, got {po_parent} {po_a} {po_gc} {po_b}"
3201 );
3202 }
3203
3204 #[test]
3207 fn z_index_reorders_sibling_paint_order() {
3208 use crate::components::ZIndex;
3209 let mut world = World::new();
3210 let parent = world.spawn_empty().id();
3211 let a = world.spawn(ChildOf(parent)).id();
3212 let b = world.spawn((ChildOf(parent), ZIndex(-1))).id();
3213 let c = world.spawn(ChildOf(parent)).id();
3214
3215 let (parents, mut cache) = build_parent_map(&mut world);
3216 let po_a = paint_order_of(a, &parents, &mut cache);
3217 let po_b = paint_order_of(b, &parents, &mut cache);
3218 let po_c = paint_order_of(c, &parents, &mut cache);
3219 assert!(
3220 po_b < po_a && po_a < po_c,
3221 "z:-1 sibling paints first; equal-z siblings keep document order (got {po_b} {po_a} {po_c})"
3222 );
3223
3224 world.entity_mut(a).insert(ZIndex(5));
3226 let (parents, mut cache) = build_parent_map(&mut world);
3227 let po_a = paint_order_of(a, &parents, &mut cache);
3228 let po_c = paint_order_of(c, &parents, &mut cache);
3229 assert!(po_a > po_c, "z:5 sibling paints above z:auto");
3230 }
3231
3232 #[test]
3239 fn partially_clipped_child_is_extracted_fully_outside_is_culled() {
3240 use crate::components::Style;
3241 use crate::input::Scroll;
3242 let mut main = World::new();
3243 let mut render = World::new();
3244 render.insert_resource(RenderEntityMap::default());
3245
3246 let container = main
3248 .spawn((
3249 Transform::new(Vec2::ZERO, Vec2::new(960.0, 504.0)),
3250 Style::default(),
3251 Scroll::default(),
3252 ))
3253 .id();
3254 let tile = main
3257 .spawn((
3258 Transform::new(Vec2::new(4.0, 8.0), Vec2::new(960.0, 80.0)),
3259 Visuals {
3260 fill: Some(Fill::Solid(Color::rgb(1.0, 0.0, 0.0))),
3261 radius: 0.0,
3262 corner_radii: None,
3263 shadows: Vec::new(),
3264 border: None,
3265 },
3266 ChildOf(container),
3267 ))
3268 .id();
3269 let straddler = main
3271 .spawn((
3272 Transform::new(Vec2::new(4.0, 480.0), Vec2::new(960.0, 80.0)),
3273 Visuals {
3274 fill: Some(Fill::Solid(Color::rgb(0.0, 1.0, 0.0))),
3275 radius: 0.0,
3276 corner_radii: None,
3277 shadows: Vec::new(),
3278 border: None,
3279 },
3280 ChildOf(container),
3281 ))
3282 .id();
3283 let outside = main
3285 .spawn((
3286 Transform::new(Vec2::new(0.0, 600.0), Vec2::new(100.0, 50.0)),
3287 Visuals {
3288 fill: Some(Fill::Solid(Color::rgb(0.0, 0.0, 1.0))),
3289 radius: 0.0,
3290 corner_radii: None,
3291 shadows: Vec::new(),
3292 border: None,
3293 },
3294 ChildOf(container),
3295 ))
3296 .id();
3297
3298 extract_rects(&mut main, &mut render);
3299
3300 let map = render.resource::<RenderEntityMap>();
3301 assert!(
3302 map.rect.contains_key(&tile),
3303 "width:100% + margin tile (4px overhang) must extract"
3304 );
3305 assert!(
3306 map.rect.contains_key(&straddler),
3307 "row straddling the clip bottom must extract"
3308 );
3309 assert!(
3310 !map.rect.contains_key(&outside),
3311 "zero-overlap child stays culled"
3312 );
3313 let re = map.rect[&tile];
3315 let rect = render.get::<ExtractedRect>(re).cloned().unwrap();
3316 assert_eq!(rect.size, Vec2::new(960.0, 80.0));
3317 assert_eq!(rect.origin, Vec2::new(4.0, 8.0));
3318 }
3319
3320 #[test]
3325 fn extract_borders_emits_expected_widths_and_order() {
3326 use crate::components::{Border, Edges, Style};
3327 let mut main = World::new();
3328 let mut render = World::new();
3329 render.insert_resource(RenderEntityMap::default());
3330
3331 let bordered = main
3332 .spawn((
3333 Transform::new(Vec2::new(5.0, 6.0), Vec2::new(50.0, 40.0)),
3334 Style::default(),
3335 Visuals {
3336 fill: None,
3337 radius: 8.0,
3338 corner_radii: None,
3339 shadows: Vec::new(),
3340 border: Some(Border {
3341 widths: Edges {
3342 top: 1.0,
3343 right: 2.0,
3344 bottom: 3.0,
3345 left: 4.0,
3346 ..Edges::default()
3347 },
3348 color: Color::rgb(1.0, 0.0, 0.0),
3349 side_colors: None,
3350 }),
3351 },
3352 ))
3353 .id();
3354 let _plain = main
3355 .spawn((
3356 Transform::new(Vec2::ZERO, Vec2::new(10.0, 10.0)),
3357 Visuals {
3358 fill: Some(Fill::Solid(Color::rgb(0.0, 1.0, 0.0))),
3359 radius: 0.0,
3360 corner_radii: None,
3361 shadows: Vec::new(),
3362 border: None,
3363 },
3364 ))
3365 .id();
3366
3367 extract_borders(&mut main, &mut render);
3368
3369 let borders: Vec<ExtractedBorder> = {
3370 let mut q = render.query::<&ExtractedBorder>();
3371 q.iter(&render).copied().collect()
3372 };
3373 assert_eq!(borders.len(), 1, "only the bordered entity extracts");
3374 let b = borders[0];
3375 assert_eq!(b.origin, Vec2::new(5.0, 6.0));
3376 assert_eq!(b.size, Vec2::new(50.0, 40.0));
3377 assert_eq!(b.widths, [1.0, 2.0, 3.0, 4.0]);
3378 assert_eq!(b.radius, 8.0);
3379
3380 main.get_mut::<Visuals>(bordered).unwrap().border = None;
3382 extract_borders(&mut main, &mut render);
3383 let count = {
3384 let mut q = render.query::<&ExtractedBorder>();
3385 q.iter(&render).count()
3386 };
3387 assert_eq!(count, 0);
3388 }
3389
3390 #[test]
3394 fn a_faded_parent_fades_its_subtree_as_a_group() {
3395 let mut main = World::new();
3396 let mut render = World::new();
3397 render.insert_resource(RenderEntityMap::default());
3398 let parent = main
3399 .spawn((
3400 Transform::new(Vec2::ZERO, Vec2::new(100.0, 100.0)),
3401 Opacity(0.5),
3402 ))
3403 .id();
3404 let child = main
3405 .spawn((
3406 Transform::new(Vec2::ZERO, Vec2::new(50.0, 50.0)),
3407 Visuals {
3408 fill: Some(Fill::Solid(Color::rgba(1.0, 0.0, 0.0, 1.0))),
3409 radius: 0.0,
3410 corner_radii: None,
3411 shadows: Vec::new(),
3412 border: None,
3413 },
3414 Opacity(0.5),
3415 ChildOf(parent),
3416 ))
3417 .id();
3418 extract_rects(&mut main, &mut render);
3419 extract_opacity_groups(&mut main, &mut render);
3420 let rects: Vec<ExtractedRect> = {
3421 let mut q = render.query::<&ExtractedRect>();
3422 q.iter(&render).cloned().collect()
3423 };
3424 assert_eq!(rects.len(), 1);
3425 let Brush::Solid(c) = &rects[0].brush else {
3426 panic!("solid brush expected");
3427 };
3428 assert!((c.a - 0.5).abs() < 1e-4, "expected 0.5, got {}", c.a);
3430 let groups: Vec<ExtractedOpacityGroup> = {
3431 let mut q = render.query::<&ExtractedOpacityGroup>();
3432 q.iter(&render).copied().collect()
3433 };
3434 assert_eq!(groups.len(), 1, "only the parent has children to fade");
3435 let (parents, mut depth) = build_parent_map(&mut main);
3436 let parent_order = paint_order_of(parent, &parents, &mut depth);
3437 let child_order = paint_order_of(child, &parents, &mut depth);
3438 assert_eq!(groups[0].alpha, 0.5);
3439 assert_eq!(groups[0].start_order, parent_order.saturating_sub(1));
3440 assert_eq!(groups[0].end_order, child_order);
3441 }
3442
3443 #[test]
3446 fn an_overlay_under_a_faded_parent_carries_the_parent_s_opacity() {
3447 let mut main = World::new();
3448 let parent = main
3449 .spawn((
3450 Transform::new(Vec2::ZERO, Vec2::new(100.0, 100.0)),
3451 Opacity(0.5),
3452 ))
3453 .id();
3454 let inline = main.spawn((Transform::default(), ChildOf(parent))).id();
3455 let popup = main
3456 .spawn((Transform::default(), OverlayLayer, ChildOf(parent)))
3457 .id();
3458 let item = main.spawn((Transform::default(), ChildOf(popup))).id();
3459 let (parents, _) = build_parent_map(&mut main);
3460 let model = opacity_model(&mut main, &parents);
3461 assert_eq!(model.groups, vec![(parent, 0.5)]);
3462 assert_eq!(draw_opacity(&model.draw, parent).0, 1.0);
3463 assert_eq!(draw_opacity(&model.draw, inline).0, 1.0);
3464 assert_eq!(draw_opacity(&model.draw, popup).0, 0.5);
3465 assert_eq!(draw_opacity(&model.draw, item).0, 0.5);
3466 }
3467
3468 #[test]
3471 fn a_faded_element_with_no_visible_child_folds_its_own_opacity() {
3472 let mut main = World::new();
3473 let leaf = main.spawn((Transform::default(), Opacity(0.25))).id();
3474 main.spawn((Transform::default(), Visible(false), ChildOf(leaf)));
3475 let (parents, _) = build_parent_map(&mut main);
3476 let model = opacity_model(&mut main, &parents);
3477 assert!(model.groups.is_empty());
3478 assert_eq!(draw_opacity(&model.draw, leaf).0, 0.25);
3479 }
3480
3481 #[test]
3485 fn scrollbar_extracts_above_content_only_when_overflowing() {
3486 use crate::input::{Scroll, ScrollbarState};
3487 let mut main = World::new();
3488 main.insert_resource(crate::input::ScrollbarInteraction::default());
3489 let mut render = World::new();
3490 render.insert_resource(RenderEntityMap::default());
3491
3492 let scroller = main
3493 .spawn((
3494 Transform::new(Vec2::ZERO, Vec2::new(200.0, 400.0)),
3495 Scroll::vertical(),
3496 ScrollOffset::default(),
3497 ScrollbarState::default(),
3498 ))
3499 .id();
3500 let content = main
3502 .spawn((
3503 Transform::new(Vec2::ZERO, Vec2::new(200.0, 900.0)),
3504 ChildOf(scroller),
3505 ))
3506 .id();
3507 extract_scrollbars(&mut main, &mut render);
3508 let bars: Vec<ExtractedScrollbar> = {
3509 let mut q = render.query::<&ExtractedScrollbar>();
3510 q.iter(&render).cloned().collect()
3511 };
3512 assert_eq!(bars.len(), 1, "overflowing scroller gets a bar");
3513 assert!(!bars[0].draws.is_empty());
3514 let (parents, mut cache) = build_parent_map(&mut main);
3515 let content_order = paint_order_of(content, &parents, &mut cache);
3516 assert!(
3517 bars[0].order > content_order,
3518 "bar order {} must sit above content order {content_order}",
3519 bars[0].order
3520 );
3521
3522 main.get_mut::<Transform>(content).unwrap().size = Vec2::new(200.0, 300.0);
3524 extract_scrollbars(&mut main, &mut render);
3525 let count = {
3526 let mut q = render.query::<&ExtractedScrollbar>();
3527 q.iter(&render).count()
3528 };
3529 assert_eq!(count, 0, "as-needed visibility: no overflow, no bar");
3530 }
3531
3532 #[test]
3535 fn scrollbar_width_none_paints_nothing() {
3536 use crate::input::{Scroll, ScrollbarState, ScrollbarStyle, ScrollbarWidthMode};
3537 let mut main = World::new();
3538 main.insert_resource(crate::input::ScrollbarInteraction::default());
3539 let mut render = World::new();
3540 render.insert_resource(RenderEntityMap::default());
3541 let scroller = main
3542 .spawn((
3543 Transform::new(Vec2::ZERO, Vec2::new(200.0, 400.0)),
3544 Scroll::vertical(),
3545 ScrollOffset::default(),
3546 ScrollbarState::default(),
3547 ScrollbarStyle {
3548 width: ScrollbarWidthMode::None,
3549 ..Default::default()
3550 },
3551 ))
3552 .id();
3553 main.spawn((
3554 Transform::new(Vec2::ZERO, Vec2::new(200.0, 900.0)),
3555 ChildOf(scroller),
3556 ));
3557 extract_scrollbars(&mut main, &mut render);
3558 let count = {
3559 let mut q = render.query::<&ExtractedScrollbar>();
3560 q.iter(&render).count()
3561 };
3562 assert_eq!(count, 0);
3563 }
3564
3565 #[test]
3567 fn orphan_entities_sort_after_tree_content() {
3568 let mut world = World::new();
3569 let orphan = world.spawn_empty().id();
3570 let child = world.spawn_empty().id();
3571 let parent = world.spawn_empty().id();
3572 world.entity_mut(child).insert(ChildOf(parent));
3573
3574 let (parents, mut cache) = build_parent_map(&mut world);
3575 let po_orphan = paint_order_of(orphan, &parents, &mut cache);
3576 let po_child = paint_order_of(child, &parents, &mut cache);
3577 assert!(
3578 po_orphan > po_child,
3579 "orphan ({po_orphan}) must paint after tree content ({po_child})"
3580 );
3581 }
3582
3583 #[test]
3588 fn clip_ranges_bracket_exactly_descendants() {
3589 use crate::components::{Overflow, Style, Transform};
3590 let mut main = World::new();
3591 let mut render = World::new();
3592 render.insert_resource(RenderEntityMap::default());
3593
3594 let row1 = main.spawn(Transform::default()).id();
3596 let row2 = main.spawn(Transform::default()).id();
3597 let button = main.spawn(Transform::default()).id();
3598 let scrollbox = main
3599 .spawn((
3600 Style {
3601 overflow_y: Overflow::Hidden,
3602 ..Default::default()
3603 },
3604 Transform {
3605 absolute: Vec2::new(10.0, 10.0),
3606 size: Vec2::new(200.0, 100.0),
3607 baseline_y: None,
3608 },
3609 ))
3610 .id();
3611 let root = main.spawn(Transform::default()).id();
3612 main.entity_mut(scrollbox).insert(ChildOf(root));
3614 main.entity_mut(button).insert(ChildOf(root));
3615 main.entity_mut(row1).insert(ChildOf(scrollbox));
3616 main.entity_mut(row2).insert(ChildOf(scrollbox));
3617
3618 extract_clips(&mut main, &mut render);
3619
3620 let clip = {
3621 let mut q = render.query::<&ExtractedClipBox>();
3622 let boxes: Vec<ExtractedClipBox> = q.iter(&render).copied().collect();
3623 assert_eq!(boxes.len(), 1, "exactly one clip candidate");
3624 boxes[0]
3625 };
3626
3627 let (parents, mut cache) = build_parent_map(&mut main);
3628 let po_scrollbox = paint_order_of(scrollbox, &parents, &mut cache);
3629 let po_row1 = paint_order_of(row1, &parents, &mut cache);
3630 let po_row2 = paint_order_of(row2, &parents, &mut cache);
3631 let po_button = paint_order_of(button, &parents, &mut cache);
3632 let po_root = paint_order_of(root, &parents, &mut cache);
3633
3634 assert_eq!(clip.start_order, po_scrollbox);
3635 assert_eq!(clip.end_order, po_row1.max(po_row2));
3636 assert!(clip.start_order < po_row1 && po_row1 <= clip.end_order);
3638 assert!(clip.start_order < po_row2 && po_row2 <= clip.end_order);
3639 assert!(
3641 po_button > clip.end_order,
3642 "later sibling (order {po_button}) must not be swallowed by clip range [{}, {}]",
3643 clip.start_order,
3644 clip.end_order
3645 );
3646 assert!(po_root < clip.start_order);
3647 }
3648
3649 #[test]
3653 fn overlay_subtree_paints_after_all_normal_content() {
3654 let mut world = World::new();
3655 let root = world.spawn_empty().id();
3656 let dropdown_row = world.spawn(ChildOf(root)).id();
3658 let wrapper = world.spawn(ChildOf(dropdown_row)).id();
3659 let panel = world.spawn((ChildOf(wrapper), OverlayLayer)).id();
3660 let opt1 = world.spawn(ChildOf(panel)).id();
3661 let opt2 = world.spawn(ChildOf(panel)).id();
3662 let textarea_row = world.spawn(ChildOf(root)).id();
3663 let textarea = world.spawn(ChildOf(textarea_row)).id();
3664
3665 let (parents, mut cache) = build_parent_map(&mut world);
3666 let po_panel = paint_order_of(panel, &parents, &mut cache);
3667 let po_opt1 = paint_order_of(opt1, &parents, &mut cache);
3668 let po_opt2 = paint_order_of(opt2, &parents, &mut cache);
3669 for normal in [root, dropdown_row, wrapper, textarea_row, textarea] {
3670 let o = paint_order_of(normal, &parents, &mut cache);
3671 assert!(
3672 o < OVERLAY_ORDER_BASE,
3673 "normal content stays below the overlay band, got {o:#x}"
3674 );
3675 assert!(
3676 po_panel > o,
3677 "panel ({po_panel:#x}) must paint after normal content ({o:#x})"
3678 );
3679 }
3680 assert_eq!(po_opt1, po_panel + 2, "panel then first option");
3682 assert_eq!(po_opt2, po_opt1 + 2, "options keep sibling order");
3683 assert!((OVERLAY_ORDER_BASE..0x8000_0000).contains(&po_panel));
3685 }
3686
3687 #[test]
3690 fn overlays_stack_by_open_order() {
3691 let mut world = World::new();
3692 let root = world.spawn_empty().id();
3693 let a = world.spawn((ChildOf(root), OverlayLayer)).id();
3695 let _a_kid = world.spawn(ChildOf(a)).id();
3696 let b = world
3697 .spawn((ChildOf(root), OverlayLayer, Visible(false)))
3698 .id();
3699 let _b_kid = world.spawn(ChildOf(b)).id();
3700
3701 let (parents, mut cache) = build_parent_map(&mut world);
3703 let po_a_t1 = paint_order_of(a, &parents, &mut cache);
3704 assert!(po_a_t1 >= OVERLAY_ORDER_BASE);
3705
3706 world.entity_mut(b).insert(Visible(true));
3708 let (parents, mut cache) = build_parent_map(&mut world);
3709 let po_a = paint_order_of(a, &parents, &mut cache);
3710 let po_b = paint_order_of(b, &parents, &mut cache);
3711 assert!(
3712 po_b > po_a,
3713 "later-opened popup B ({po_b:#x}) must paint over A ({po_a:#x})"
3714 );
3715
3716 world.entity_mut(a).insert(Visible(false));
3718 let _ = build_parent_map(&mut world);
3719 world.entity_mut(a).insert(Visible(true));
3720 let (parents, mut cache) = build_parent_map(&mut world);
3721 let po_a = paint_order_of(a, &parents, &mut cache);
3722 let po_b = paint_order_of(b, &parents, &mut cache);
3723 assert!(
3724 po_a > po_b,
3725 "re-opened popup A ({po_a:#x}) must now paint over B ({po_b:#x})"
3726 );
3727 }
3728
3729 #[test]
3732 fn clip_ranges_exclude_overlay_subtrees() {
3733 use crate::components::{Overflow, Style, Transform};
3734 let mut main = World::new();
3735 let mut render = World::new();
3736 render.insert_resource(RenderEntityMap::default());
3737
3738 let root = main.spawn(Transform::default()).id();
3739 let scrollbox = main
3740 .spawn((
3741 Style {
3742 overflow_y: Overflow::Hidden,
3743 ..Default::default()
3744 },
3745 Transform {
3746 absolute: Vec2::new(0.0, 0.0),
3747 size: Vec2::new(200.0, 100.0),
3748 baseline_y: None,
3749 },
3750 ChildOf(root),
3751 ))
3752 .id();
3753 let row = main.spawn((Transform::default(), ChildOf(scrollbox))).id();
3754 let panel = main
3755 .spawn((Transform::default(), ChildOf(scrollbox), OverlayLayer))
3756 .id();
3757 let opt = main.spawn((Transform::default(), ChildOf(panel))).id();
3758
3759 extract_clips(&mut main, &mut render);
3760 let clip = {
3761 let mut q = render.query::<&ExtractedClipBox>();
3762 let boxes: Vec<ExtractedClipBox> = q.iter(&render).copied().collect();
3763 assert_eq!(boxes.len(), 1, "exactly one clip candidate");
3764 boxes[0]
3765 };
3766
3767 let (parents, mut cache) = build_parent_map(&mut main);
3768 let po_row = paint_order_of(row, &parents, &mut cache);
3769 let po_panel = paint_order_of(panel, &parents, &mut cache);
3770 let po_opt = paint_order_of(opt, &parents, &mut cache);
3771 assert_eq!(
3772 clip.end_order, po_row,
3773 "bracket ends at the last NORMAL descendant"
3774 );
3775 assert!(
3776 po_panel > clip.end_order && po_opt > clip.end_order,
3777 "overlay subtree ({po_panel:#x}, {po_opt:#x}) must fall outside the clip bracket [{:#x}, {:#x}]",
3778 clip.start_order,
3779 clip.end_order
3780 );
3781 }
3782
3783 #[test]
3786 fn overlay_internal_clip_brackets_intact() {
3787 use crate::components::{Overflow, Style, Transform};
3788 let mut main = World::new();
3789 let mut render = World::new();
3790 render.insert_resource(RenderEntityMap::default());
3791
3792 let root = main.spawn(Transform::default()).id();
3793 let panel = main
3794 .spawn((
3795 Style {
3796 overflow_y: Overflow::Hidden,
3797 ..Default::default()
3798 },
3799 Transform {
3800 absolute: Vec2::new(0.0, 0.0),
3801 size: Vec2::new(200.0, 100.0),
3802 baseline_y: None,
3803 },
3804 ChildOf(root),
3805 OverlayLayer,
3806 ))
3807 .id();
3808 let opt1 = main.spawn((Transform::default(), ChildOf(panel))).id();
3809 let opt2 = main.spawn((Transform::default(), ChildOf(panel))).id();
3810
3811 extract_clips(&mut main, &mut render);
3812 let clip = {
3813 let mut q = render.query::<&ExtractedClipBox>();
3814 let boxes: Vec<ExtractedClipBox> = q.iter(&render).copied().collect();
3815 assert_eq!(boxes.len(), 1);
3816 boxes[0]
3817 };
3818
3819 let (parents, mut cache) = build_parent_map(&mut main);
3820 let po_panel = paint_order_of(panel, &parents, &mut cache);
3821 let po_opt1 = paint_order_of(opt1, &parents, &mut cache);
3822 let po_opt2 = paint_order_of(opt2, &parents, &mut cache);
3823 assert_eq!(clip.start_order, po_panel);
3824 assert_eq!(clip.end_order, po_opt1.max(po_opt2));
3825 assert!(
3826 clip.start_order >= OVERLAY_ORDER_BASE,
3827 "bracket lives in the overlay band"
3828 );
3829 assert!(clip.start_order < po_opt1 && po_opt1 <= clip.end_order);
3830 assert!(clip.start_order < po_opt2 && po_opt2 <= clip.end_order);
3831 }
3832
3833 #[test]
3838 fn clip_rects_follow_the_scroll_offset_of_their_ancestors() {
3839 use crate::components::{Overflow, Style, Transform, Visuals};
3840 use crate::input::{Scroll, ScrollOffset};
3841 let mut main = World::new();
3842 let mut render = World::new();
3843 render.insert_resource(RenderEntityMap::default());
3844
3845 let scroller = main
3849 .spawn((
3850 Scroll::default(),
3851 ScrollOffset(Vec2::new(0.0, 200.0)),
3852 Style::default(),
3853 Transform {
3854 absolute: Vec2::ZERO,
3855 size: Vec2::new(200.0, 100.0),
3856 baseline_y: None,
3857 },
3858 ))
3859 .id();
3860 let card = main
3861 .spawn((
3862 Style {
3863 overflow_y: Overflow::Hidden,
3864 ..Default::default()
3865 },
3866 Transform {
3867 absolute: Vec2::new(0.0, 200.0),
3868 size: Vec2::new(200.0, 60.0),
3869 baseline_y: None,
3870 },
3871 ChildOf(scroller),
3872 ))
3873 .id();
3874 let inner = main
3875 .spawn((
3876 Transform {
3877 absolute: Vec2::new(0.0, 200.0),
3878 size: Vec2::new(50.0, 50.0),
3879 baseline_y: None,
3880 },
3881 Visuals {
3882 fill: Some(crate::components::Fill::Solid(Color::rgb(1.0, 0.0, 0.0))),
3883 ..Default::default()
3884 },
3885 ChildOf(card),
3886 ))
3887 .id();
3888
3889 let (parents, _) = build_parent_map(&mut main);
3890 let clips = parent_scroll_clip_rects(&mut main, &parents);
3891 assert_eq!(
3892 clips.get(&inner).copied(),
3893 Some((Vec2::new(0.0, 0.0), Vec2::new(200.0, 60.0))),
3894 "the card clips its child where the card is drawn"
3895 );
3896
3897 extract_clips(&mut main, &mut render);
3898 let boxes: Vec<ExtractedClipBox> = {
3899 let mut q = render.query::<&ExtractedClipBox>();
3900 q.iter(&render).copied().collect()
3901 };
3902 let card_box = boxes
3903 .iter()
3904 .find(|b| b.size == Vec2::new(200.0, 60.0))
3905 .expect("clip box for the card");
3906 assert_eq!(card_box.origin, Vec2::new(0.0, 0.0));
3907 let scroller_box = boxes
3908 .iter()
3909 .find(|b| b.size == Vec2::new(200.0, 100.0))
3910 .expect("clip box for the scroll container");
3911 assert_eq!(
3912 scroller_box.origin,
3913 Vec2::ZERO,
3914 "a scroll box moves its content, not its own frame"
3915 );
3916
3917 extract_rects(&mut main, &mut render);
3918 let origins: Vec<Vec2> = {
3919 let mut q = render.query::<&ExtractedRect>();
3920 q.iter(&render).map(|r| r.origin).collect()
3921 };
3922 assert_eq!(
3923 origins,
3924 vec![Vec2::ZERO],
3925 "the scrolled-into-view child still paints"
3926 );
3927 }
3928
3929 #[test]
3932 fn popup_content_escapes_ancestor_clip_rect() {
3933 use crate::components::{Overflow, Style, Transform};
3934 let mut main = World::new();
3935
3936 let root = main.spawn(Transform::default()).id();
3937 let scrollbox = main
3938 .spawn((
3939 Style {
3940 overflow_y: Overflow::Hidden,
3941 ..Default::default()
3942 },
3943 Transform {
3944 absolute: Vec2::new(0.0, 0.0),
3945 size: Vec2::new(200.0, 50.0),
3946 baseline_y: None,
3947 },
3948 ChildOf(root),
3949 ))
3950 .id();
3951 let row = main.spawn((Transform::default(), ChildOf(scrollbox))).id();
3952 let panel = main
3953 .spawn((Transform::default(), ChildOf(scrollbox), OverlayLayer))
3954 .id();
3955 let opt = main.spawn((Transform::default(), ChildOf(panel))).id();
3956
3957 let (parents, _) = build_parent_map(&mut main);
3958 let clips = parent_scroll_clip_rects(&mut main, &parents);
3959 assert!(
3960 clips.contains_key(&row),
3961 "normal child keeps its scroll-ancestor clip"
3962 );
3963 assert!(
3964 !clips.contains_key(&panel) && !clips.contains_key(&opt),
3965 "overlay subtree escapes the ancestor clip rect"
3966 );
3967 }
3968
3969 #[test]
3974 fn line_height_px_falls_back_to_default_multiplier() {
3975 use crate::components::{
3976 DEFAULT_LINE_HEIGHT_MULTIPLIER, TextContent, TextStyle, Transform,
3977 };
3978
3979 let mut main = World::new();
3980 let mut render = World::new();
3981 render.init_resource::<RenderEntityMap>();
3982 main.spawn((
3983 Transform::new(Vec2::ZERO, Vec2::new(120.0, 24.0)),
3984 TextContent("hi".to_string()),
3985 TextStyle {
3986 size_px: 20.0,
3987 ..Default::default()
3988 },
3989 ));
3990 extract_text(&mut main, &mut render);
3991 let mut q = render.query::<&ExtractedText>();
3992 let et = q.iter(&render).next().unwrap();
3993 assert_eq!(et.line_height_px, 20.0 * DEFAULT_LINE_HEIGHT_MULTIPLIER);
3994 }
3995
3996 #[test]
3999 fn line_height_px_honours_css_multiplier_override() {
4000 use crate::components::{LineHeightSpec, TextContent, TextStyle, Transform};
4001
4002 let mut main = World::new();
4003 let mut render = World::new();
4004 render.init_resource::<RenderEntityMap>();
4005 main.spawn((
4006 Transform::new(Vec2::ZERO, Vec2::new(120.0, 24.0)),
4007 TextContent("hi".to_string()),
4008 TextStyle {
4009 size_px: 20.0,
4010 line_height: Some(LineHeightSpec::Multiplier(1.5)),
4011 ..Default::default()
4012 },
4013 ));
4014 extract_text(&mut main, &mut render);
4015 let mut q = render.query::<&ExtractedText>();
4016 let et = q.iter(&render).next().unwrap();
4017 assert_eq!(et.line_height_px, 30.0);
4018 }
4019
4020 #[test]
4023 fn line_height_px_honours_css_absolute_override() {
4024 use crate::components::{LineHeightSpec, TextContent, TextStyle, Transform};
4025
4026 let mut main = World::new();
4027 let mut render = World::new();
4028 render.init_resource::<RenderEntityMap>();
4029 main.spawn((
4030 Transform::new(Vec2::ZERO, Vec2::new(120.0, 24.0)),
4031 TextContent("hi".to_string()),
4032 TextStyle {
4033 size_px: 20.0,
4034 line_height: Some(LineHeightSpec::Px(19.0)),
4035 ..Default::default()
4036 },
4037 ));
4038 extract_text(&mut main, &mut render);
4039 let mut q = render.query::<&ExtractedText>();
4040 let et = q.iter(&render).next().unwrap();
4041 assert_eq!(et.line_height_px, 19.0);
4042 }
4043
4044 #[test]
4049 fn caret_width_px_falls_back_then_honours_override() {
4050 use crate::components::{CaretWidth, TextContent, Transform};
4051
4052 let mut main = World::new();
4053 let mut render = World::new();
4054 render.init_resource::<RenderEntityMap>();
4055 let plain = main
4056 .spawn((
4057 Transform::new(Vec2::ZERO, Vec2::new(120.0, 24.0)),
4058 TextContent("a".to_string()),
4059 ))
4060 .id();
4061 let overridden = main
4062 .spawn((
4063 Transform::new(Vec2::new(0.0, 40.0), Vec2::new(120.0, 24.0)),
4064 TextContent("b".to_string()),
4065 CaretWidth(4.0),
4066 ))
4067 .id();
4068 extract_text(&mut main, &mut render);
4069 let map = render.resource::<RenderEntityMap>().text.clone();
4070 let plain_et = render.get::<ExtractedText>(map[&plain]).unwrap();
4071 let overridden_et = render.get::<ExtractedText>(map[&overridden]).unwrap();
4072 assert_eq!(plain_et.caret_width_px, CARET_WIDTH_PX);
4073 assert_eq!(overridden_et.caret_width_px, 4.0);
4074 }
4075
4076 #[test]
4081 fn a_distribution_places_the_run_the_way_it_places_an_item() {
4082 use crate::components::{FlexDirection, FlexJustify, Style};
4083
4084 let row = |justify| Style {
4085 justify,
4086 ..Style::default()
4087 };
4088 let cases = [
4089 (FlexJustify::Start, TextAlign::Start),
4090 (FlexJustify::SpaceBetween, TextAlign::Start),
4091 (FlexJustify::Center, TextAlign::Center),
4092 (FlexJustify::SpaceAround, TextAlign::Center),
4093 (FlexJustify::SpaceEvenly, TextAlign::Center),
4094 (FlexJustify::End, TextAlign::End),
4095 ];
4096 for (justify, want) in cases {
4097 assert_eq!(
4098 text_run_align(TextAlign::Start, Some(&row(justify))),
4099 want,
4100 "{justify:?} must leave the run at {want:?}"
4101 );
4102 }
4103
4104 let reversed = |justify| Style {
4106 justify,
4107 flex_direction: FlexDirection::RowReverse,
4108 ..Style::default()
4109 };
4110 assert_eq!(
4111 text_run_align(TextAlign::Start, Some(&reversed(FlexJustify::Start))),
4112 TextAlign::End
4113 );
4114 assert_eq!(
4115 text_run_align(TextAlign::Start, Some(&reversed(FlexJustify::End))),
4116 TextAlign::Start
4117 );
4118 assert_eq!(
4119 text_run_align(TextAlign::Start, Some(&reversed(FlexJustify::Center))),
4120 TextAlign::Center
4121 );
4122
4123 for direction in [FlexDirection::Column, FlexDirection::ColumnReverse] {
4126 let column = Style {
4127 justify: FlexJustify::Center,
4128 flex_direction: direction,
4129 ..Style::default()
4130 };
4131 assert_eq!(
4132 text_run_align(TextAlign::Start, Some(&column)),
4133 TextAlign::Start
4134 );
4135 }
4136 assert_eq!(text_run_align(TextAlign::Start, None), TextAlign::Start);
4137
4138 assert_eq!(
4140 text_run_align(TextAlign::End, Some(&row(FlexJustify::Center))),
4141 TextAlign::End
4142 );
4143 }
4144}