1use crate::components::{
18 CARET_WIDTH_PX, CaretBlink, CaretWidth, Color, EchoMode, Fill, ImeState, Opacity,
19 PASSWORD_MASK_CHAR, PasswordCharacter, TextAlign, TextBlockOrigin, TextContent, TextInput,
20 TextInputPaint, TextInputScroll, TextStyle, Transform, Visible, Visuals, resolve_line_height,
21 text_baseline_in_line, text_block_top,
22};
23use crate::input::{Focused, ScrollOffset};
24use bevy_ecs::prelude::*;
25use bevy_ecs::query::Or;
26use glam::Vec2;
27use std::collections::HashMap;
28use std::sync::atomic::{AtomicBool, Ordering};
29use std::sync::{Arc, Mutex};
30
31pub type ExtractFn = fn(&mut World, &mut World);
35
36#[derive(bevy_ecs::schedule::ScheduleLabel, Clone, Copy, Debug, Hash, PartialEq, Eq)]
38pub struct Render;
39
40#[derive(SystemSet, Clone, Copy, Debug, Hash, PartialEq, Eq)]
42pub enum RenderStage {
43 Prepare,
45 Render,
47}
48
49#[derive(Resource, Clone, Debug)]
52pub struct Viewport {
53 pub size: Vec2,
60 pub clear: Color,
62 pub scale_factor: f32,
67 pub monitor_scale: Option<f32>,
69 pub monitor_size: Option<Vec2>,
71 pub monitor_name: Option<String>,
73}
74
75impl Default for Viewport {
76 fn default() -> Self {
77 Self {
78 size: Vec2::new(800.0, 600.0),
79 clear: Color::rgba(0.0, 0.0, 0.0, 0.0),
80 scale_factor: 1.0,
81 monitor_scale: None,
82 monitor_size: None,
83 monitor_name: None,
84 }
85 }
86}
87
88#[derive(Resource, Debug)]
97pub struct FrameDirty {
98 pub dirty: bool,
100}
101
102impl Default for FrameDirty {
103 fn default() -> Self {
104 Self { dirty: true }
106 }
107}
108
109#[derive(Resource, Debug, Default)]
128pub struct AnimationsActive(std::sync::atomic::AtomicBool);
129
130impl AnimationsActive {
131 #[inline]
134 pub fn request(&self) {
135 self.0.store(true, std::sync::atomic::Ordering::Relaxed);
136 }
137
138 #[inline]
141 pub fn get(&self) -> bool {
142 self.0.load(std::sync::atomic::Ordering::Relaxed)
143 }
144
145 #[inline]
147 pub fn clear(&self) {
148 self.0.store(false, std::sync::atomic::Ordering::Relaxed);
149 }
150}
151
152pub fn reset_animations_active(flag: Res<AnimationsActive>) {
158 flag.clear();
159}
160
161#[derive(Clone, Copy, Debug, Default, PartialEq)]
163pub struct Rect {
164 pub origin: Vec2,
166 pub size: Vec2,
168}
169
170impl Rect {
171 pub const fn new(origin: Vec2, size: Vec2) -> Self {
173 Self { origin, size }
174 }
175}
176
177#[derive(Resource, Default, Debug)]
182pub struct FrameDamage(pub Vec<Rect>);
183
184impl FrameDamage {
185 pub fn clear(&mut self) {
187 self.0.clear();
188 }
189
190 pub fn push(&mut self, r: Rect) {
192 self.0.push(r);
193 }
194
195 pub fn is_empty(&self) -> bool {
197 self.0.is_empty()
198 }
199}
200
201#[derive(Resource, Default)]
233pub struct ExtractContextCache {
234 active: bool,
238 parent_map: Option<(HashMap<Entity, Entity>, HashMap<Entity, u32>)>,
240 hidden: Option<std::collections::HashSet<Entity>>,
242 scroll: Option<HashMap<Entity, Vec2>>,
244 opacities: Option<HashMap<Entity, f32>>,
246 clip: Option<HashMap<Entity, (Vec2, Vec2)>>,
248}
249
250impl ExtractContextCache {
251 pub fn begin_phase(&mut self) {
255 self.active = true;
256 self.parent_map = None;
257 self.hidden = None;
258 self.scroll = None;
259 self.opacities = None;
260 self.clip = None;
261 }
262
263 pub fn end_phase(&mut self) {
267 self.active = false;
268 self.parent_map = None;
269 self.hidden = None;
270 self.scroll = None;
271 self.opacities = None;
272 self.clip = None;
273 }
274}
275
276#[derive(SystemSet, Clone, Copy, Debug, Hash, PartialEq, Eq)]
281pub enum ExtractSet {
282 Extract,
284}
285
286#[derive(bevy_ecs::schedule::ScheduleLabel, Clone, Copy, Debug, Hash, PartialEq, Eq)]
291pub struct ExtractSchedule;
292
293#[allow(clippy::type_complexity, clippy::too_many_arguments)]
307pub fn roll_up_frame_dirty(
308 mut fd: ResMut<FrameDirty>,
309 viewport: Res<Viewport>,
310 property_store: Option<Res<crate::property_store::PropertyStore>>,
311 component_changed: Query<
312 (),
313 Or<(
314 bevy_ecs::query::Changed<Transform>,
315 bevy_ecs::query::Changed<Visuals>,
316 bevy_ecs::query::Changed<TextStyle>,
317 bevy_ecs::query::Changed<TextContent>,
318 bevy_ecs::query::Changed<TextInput>,
319 bevy_ecs::query::Changed<TextInputScroll>,
320 bevy_ecs::query::Changed<Opacity>,
321 bevy_ecs::query::Changed<Visible>,
322 bevy_ecs::query::Changed<crate::components::LumenClasses>,
323 bevy_ecs::query::Changed<crate::input::ScrollbarState>,
326 bevy_ecs::query::Changed<EchoMode>,
329 bevy_ecs::query::Added<EchoMode>,
330 bevy_ecs::query::Added<Visible>,
331 )>,
332 >,
333 mut removed_childof: RemovedComponents<bevy_ecs::hierarchy::ChildOf>,
334 trace: (
335 Query<Entity, bevy_ecs::query::Changed<Transform>>,
336 Query<Entity, bevy_ecs::query::Changed<Visuals>>,
337 Query<Entity, bevy_ecs::query::Changed<TextStyle>>,
338 Query<Entity, bevy_ecs::query::Changed<TextContent>>,
339 Query<Entity, bevy_ecs::query::Changed<TextInput>>,
340 Query<Entity, bevy_ecs::query::Changed<TextInputScroll>>,
341 Query<Entity, bevy_ecs::query::Changed<Opacity>>,
342 Query<Entity, bevy_ecs::query::Changed<Visible>>,
343 Query<Entity, bevy_ecs::query::Changed<crate::components::LumenClasses>>,
344 Query<Entity, bevy_ecs::query::Changed<crate::input::ScrollbarState>>,
345 ),
346) {
347 let removed_any = removed_childof.read().count() > 0;
354 if fd.dirty {
355 return;
356 }
357 let property_dirty = property_store
358 .as_ref()
359 .is_some_and(|s| !s.dirty_peek().is_empty());
360 if property_dirty || removed_any || viewport.is_changed() || !component_changed.is_empty() {
361 fd.dirty = true;
362 static TRACE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
365 let trace_on = *TRACE.get_or_init(|| std::env::var_os("LUMEN_TRACE_FRAME_DIRTY").is_some());
366 if trace_on {
367 let mut sources: Vec<String> = Vec::new();
368 if property_dirty {
369 let keys: Vec<String> = property_store
370 .as_ref()
371 .map(|s| s.dirty_peek().iter().map(|k| format!("{k:?}")).collect())
372 .unwrap_or_default();
373 sources.push(format!("property_store{keys:?}"));
374 }
375 if removed_any {
376 sources.push("removed ChildOf".into());
377 }
378 if viewport.is_changed() {
379 sources.push("viewport".into());
380 }
381 let names = [
382 "Transform",
383 "Visuals",
384 "TextStyle",
385 "TextContent",
386 "TextInput",
387 "TextInputScroll",
388 "Opacity",
389 "Visible",
390 "LumenClasses",
391 "ScrollbarState",
392 ];
393 let counts = [
394 trace.0.iter().count(),
395 trace.1.iter().count(),
396 trace.2.iter().count(),
397 trace.3.iter().count(),
398 trace.4.iter().count(),
399 trace.5.iter().count(),
400 trace.6.iter().count(),
401 trace.7.iter().count(),
402 trace.8.iter().count(),
403 trace.9.iter().count(),
404 ];
405 for (name, count) in names.iter().zip(counts) {
406 if count > 0 {
407 sources.push(format!("Changed<{name}>x{count}"));
408 }
409 }
410 eprintln!("lumen-core: FrameDirty raised by {}", sources.join(", "));
411 }
412 }
413}
414
415pub type PaintOrder = u32;
436
437pub const OVERLAY_ORDER_BASE: PaintOrder = 0x4000_0000;
441
442#[derive(Component, Clone, Copy, Debug, Default)]
450pub struct OverlayLayer;
451
452#[derive(Resource, Default, Debug)]
459pub struct OverlayOpenOrder {
460 pub stamps: HashMap<Entity, u64>,
462 pub next: u64,
464}
465
466#[derive(Clone, Debug, PartialEq)]
473pub enum Brush {
474 Solid(Color),
476 Linear {
478 angle_deg: f32,
480 stops: std::sync::Arc<[(f32, Color)]>,
482 },
483 Radial {
485 radius: f32,
487 stops: std::sync::Arc<[(f32, Color)]>,
489 },
490 Conic {
492 from_deg: f32,
494 stops: std::sync::Arc<[(f32, Color)]>,
496 },
497}
498
499impl Brush {
500 pub fn with_opacity(self, alpha: Opacity) -> Self {
503 match self {
504 Brush::Solid(c) => Brush::Solid(alpha.apply(c)),
505 Brush::Linear { angle_deg, stops } => {
506 let mapped: std::sync::Arc<[(f32, Color)]> =
507 stops.iter().map(|(o, c)| (*o, alpha.apply(*c))).collect();
508 Brush::Linear {
509 angle_deg,
510 stops: mapped,
511 }
512 }
513 Brush::Radial { radius, stops } => {
514 let mapped: std::sync::Arc<[(f32, Color)]> =
515 stops.iter().map(|(o, c)| (*o, alpha.apply(*c))).collect();
516 Brush::Radial {
517 radius,
518 stops: mapped,
519 }
520 }
521 Brush::Conic { from_deg, stops } => {
522 let mapped: std::sync::Arc<[(f32, Color)]> =
523 stops.iter().map(|(o, c)| (*o, alpha.apply(*c))).collect();
524 Brush::Conic {
525 from_deg,
526 stops: mapped,
527 }
528 }
529 }
530 }
531}
532
533impl From<&Fill> for Brush {
534 fn from(f: &Fill) -> Self {
535 match f {
536 Fill::Solid(c) => Brush::Solid(*c),
537 Fill::Linear { angle_deg, stops } => Brush::Linear {
538 angle_deg: *angle_deg,
539 stops: stops.iter().copied().collect(),
540 },
541 Fill::Radial { radius, stops } => Brush::Radial {
542 radius: *radius,
543 stops: stops.iter().copied().collect(),
544 },
545 Fill::Conic { from_deg, stops } => Brush::Conic {
546 from_deg: *from_deg,
547 stops: stops.iter().copied().collect(),
548 },
549 }
550 }
551}
552
553#[derive(Component, Clone, Debug)]
555pub struct ExtractedRect {
556 pub origin: Vec2,
558 pub size: Vec2,
560 pub brush: Brush,
562 pub radius: f32,
564 pub corner_radii: Option<[f32; 4]>,
567 pub order: PaintOrder,
569}
570
571#[derive(Component, Clone, Copy, Debug)]
573pub struct ExtractedShadow {
574 pub origin: Vec2,
576 pub size: Vec2,
578 pub radius: f32,
580 pub spread: f32,
583 pub blur: f32,
585 pub color: Color,
587 pub order: PaintOrder,
589 pub inner: bool,
591 pub rect_origin: Vec2,
593}
594
595#[derive(Component, Clone, Copy, Debug)]
603pub struct ExtractedBorder {
604 pub origin: Vec2,
606 pub size: Vec2,
608 pub widths: [f32; 4],
610 pub color: Color,
612 pub side_colors: Option<[Color; 4]>,
615 pub radius: f32,
617 pub corner_radii: Option<[f32; 4]>,
619 pub order: PaintOrder,
621}
622
623#[derive(Component, Clone, Copy, Debug)]
626pub struct ExtractedOutline {
627 pub origin: Vec2,
629 pub size: Vec2,
631 pub stroke: Color,
633 pub width: f32,
635 pub radius: f32,
637 pub order: PaintOrder,
639}
640
641#[derive(Component, Clone, Debug)]
644pub struct ExtractedImage {
645 pub origin: Vec2,
647 pub size: Vec2,
649 pub width: u32,
651 pub height: u32,
653 pub rgba: std::sync::Arc<[u8]>,
655 pub fit: crate::components::ImageFit,
657 pub order: PaintOrder,
659 pub alpha: f32,
661}
662
663pub const DEFAULT_SELECTION_BG: Color = Color::rgba(0.20, 0.51, 0.98, 0.40);
671
672#[derive(Component, Clone, Debug, PartialEq)]
678pub struct ExtractedText {
679 pub origin: Vec2,
681 pub text: String,
683 pub size_px: f32,
685 pub fill: Color,
687 pub caret: Option<usize>,
689 pub selection: Option<(usize, usize)>,
691 pub selection_color: Option<Color>,
695 pub selection_foreground: Option<Color>,
699 pub caret_color: Option<Color>,
702 pub order: PaintOrder,
704 pub container_width: f32,
706 pub align: TextAlign,
708 pub wrap: crate::components::TextWrap,
710 pub max_lines: Option<u32>,
712 pub family: Option<std::sync::Arc<str>>,
714 pub weight: u16,
716 pub line_height_px: f32,
720 pub caret_width_px: f32,
723}
724
725#[derive(Component, Clone, Copy, Debug)]
730pub struct ExtractedClipBox {
731 pub origin: Vec2,
733 pub size: Vec2,
735 pub radius: f32,
737 pub start_order: PaintOrder,
741 pub end_order: PaintOrder,
744}
745
746#[derive(Clone, Copy, Debug, PartialEq)]
748pub struct ScrollbarDrawRect {
749 pub origin: Vec2,
751 pub size: Vec2,
753 pub color: Color,
755 pub radius: f32,
757}
758
759#[derive(Component, Clone, Debug)]
767pub struct ExtractedScrollbar {
768 pub draws: Vec<ScrollbarDrawRect>,
770 pub order: PaintOrder,
772}
773
774pub fn extract_scrollbars(main: &mut World, render: &mut World) {
784 use crate::input::{
785 Scroll, ScrollbarAxisPick, ScrollbarInteraction, ScrollbarState, ScrollbarStyle,
786 horizontal_scrollbar, vertical_scrollbar,
787 };
788 let (parents, mut depth_cache) = build_parent_map(main);
789 let hidden = hidden_entities(main, &parents);
790 let scroll_offsets = parent_scroll_offsets(main, &parents);
791 let interaction = main
792 .get_resource::<ScrollbarInteraction>()
793 .copied()
794 .unwrap_or_default();
795
796 let mut children: std::collections::HashMap<Entity, Vec<Entity>> =
798 std::collections::HashMap::new();
799 for (&e, &p) in parents.iter() {
800 children.entry(p).or_default().push(e);
801 }
802 let transforms: std::collections::HashMap<Entity, Transform> = {
803 let mut q = main.query::<(Entity, &Transform)>();
804 q.iter(main).map(|(e, t)| (e, *t)).collect()
805 };
806
807 type Row<'a> = (
808 Entity,
809 &'a Transform,
810 &'a Scroll,
811 &'a ScrollOffset,
812 &'a ScrollbarState,
813 Option<&'a ScrollbarStyle>,
814 );
815 let mut q = main.query::<Row>();
816 let mut pairs: Vec<(Entity, ExtractedScrollbar)> = Vec::new();
817 for (e, tf, scroll, offset, state, style) in q.iter(main) {
818 if hidden.contains(&e) || state.alpha <= 0.001 {
819 continue;
820 }
821 let style = style.copied().unwrap_or_default();
822 let Some(metrics) = style.metrics() else {
824 continue;
825 };
826 let (mut content_w, mut content_h) = (0.0_f32, 0.0_f32);
829 if let Some(kids) = children.get(&e) {
830 for kid in kids {
831 if let Some(kt) = transforms.get(kid) {
832 content_w = content_w.max((kt.absolute.x - tf.absolute.x) + kt.size.x);
833 content_h = content_h.max((kt.absolute.y - tf.absolute.y) + kt.size.y);
834 }
835 }
836 }
837 let allow_y = scroll.axis.allows_y();
838 let allow_x = scroll.axis.allows_x();
839 let anc_off = scroll_offsets.get(&e).copied().unwrap_or(Vec2::ZERO);
842 let origin = tf.absolute - anc_off;
843 let v_overflow = allow_y && content_h - tf.size.y > 0.5;
844 let h_overflow = allow_x && content_w - tf.size.x > 0.5;
845 let v_geo = if v_overflow {
846 vertical_scrollbar(origin, tf.size, content_h, offset.0.y, h_overflow, metrics)
847 } else {
848 None
849 };
850 let h_geo = if h_overflow {
851 horizontal_scrollbar(origin, tf.size, content_w, offset.0.x, v_overflow, metrics)
852 } else {
853 None
854 };
855 if v_geo.is_none() && h_geo.is_none() {
856 continue;
857 }
858
859 let fade = state.alpha.clamp(0.0, 1.0);
860 let radius = metrics.thickness / 2.0;
861
862 let mut draws: Vec<ScrollbarDrawRect> = Vec::with_capacity(4);
863 let mut push_bar = |geo: crate::input::ScrollbarGeometry, axis: ScrollbarAxisPick| {
864 let hovered = interaction
865 .drag
866 .map(|d| d.entity == e && d.axis == axis)
867 .unwrap_or(false)
868 || interaction
869 .hover
870 .map(|(he, ha, _)| he == e && ha == axis)
871 .unwrap_or(false);
872 let track = match style.track {
876 Some(c) => Some(c),
877 None if hovered => Some(style.hover_track),
878 None => None,
879 };
880 if let Some(mut track) = track {
881 track.a *= fade;
882 draws.push(ScrollbarDrawRect {
883 origin: geo.track_origin,
884 size: geo.track_size,
885 color: track,
886 radius,
887 });
888 }
889 let mut thumb = style.thumb;
890 thumb.a =
891 (thumb.a * if hovered { style.hover_boost } else { 1.0 }).clamp(0.0, 1.0) * fade;
892 draws.push(ScrollbarDrawRect {
893 origin: geo.thumb_origin,
894 size: geo.thumb_size,
895 color: thumb,
896 radius,
897 });
898 };
899 if let Some(geo) = v_geo {
900 push_bar(geo, ScrollbarAxisPick::Vertical);
901 }
902 if let Some(geo) = h_geo {
903 push_bar(geo, ScrollbarAxisPick::Horizontal);
904 }
905
906 let mut end = paint_order_of(e, &parents, &mut depth_cache);
910 let mut stack = vec![e];
911 while let Some(n) = stack.pop() {
912 let o = paint_order_of(n, &parents, &mut depth_cache);
913 if o > end {
914 end = o;
915 }
916 if let Some(kids) = children.get(&n) {
917 stack.extend(kids.iter().copied());
918 }
919 }
920 pairs.push((
921 e,
922 ExtractedScrollbar {
923 draws,
924 order: end.saturating_add(1),
925 },
926 ));
927 }
928
929 let prior = std::mem::take(&mut render.resource_mut::<RenderEntityMap>().scrollbar);
932 let mut next: std::collections::HashMap<Entity, Entity> =
933 std::collections::HashMap::with_capacity(pairs.len());
934 for (main_e, bar) in pairs {
935 let reuse = prior
936 .get(&main_e)
937 .copied()
938 .filter(|&re| render.get_entity(re).is_ok());
939 let render_e = match reuse {
940 Some(re) => {
941 render.entity_mut(re).insert(bar);
942 re
943 }
944 None => render.spawn(bar).id(),
945 };
946 next.insert(main_e, render_e);
947 }
948 for (main_e, render_e) in &prior {
949 if !next.contains_key(main_e)
950 && let Ok(em) = render.get_entity_mut(*render_e)
951 {
952 em.despawn();
953 }
954 }
955 render.resource_mut::<RenderEntityMap>().scrollbar = next;
956}
957
958#[derive(Resource, Default, Debug)]
971pub struct HiddenExtracts(pub std::collections::HashSet<Entity>);
972
973pub fn stash_hidden_entities(main: &mut World, render: &mut World) {
980 let (parents, _) = build_parent_map(main);
981 let hidden = hidden_entities(main, &parents);
982 render.resource_mut::<HiddenExtracts>().0 = hidden;
983}
984
985pub fn cull_hidden(
996 mut commands: Commands,
997 hidden: Res<HiddenExtracts>,
998 mut map: ResMut<RenderEntityMap>,
999) {
1000 if hidden.0.is_empty() {
1001 return;
1002 }
1003 let h = &hidden.0;
1004 let mut victims: Vec<Entity> = Vec::new();
1005 let map = &mut *map;
1008 for m in [
1009 &mut map.rect,
1010 &mut map.text,
1011 &mut map.outline,
1012 &mut map.border,
1013 &mut map.image,
1014 &mut map.svg,
1015 &mut map.clip,
1016 &mut map.scrollbar,
1017 ] {
1018 m.retain(|main_e, render_e| {
1019 if h.contains(main_e) {
1020 victims.push(*render_e);
1021 false
1022 } else {
1023 true
1024 }
1025 });
1026 }
1027 map.shadow.retain(|main_e, render_es| {
1029 if h.contains(main_e) {
1030 victims.extend(render_es.iter().copied());
1031 false
1032 } else {
1033 true
1034 }
1035 });
1036 for e in victims {
1037 commands.entity(e).despawn();
1038 }
1039}
1040
1041pub fn cull_offscreen(
1046 mut commands: Commands,
1047 viewport: Res<Viewport>,
1048 rects: Query<(Entity, &ExtractedRect)>,
1049 texts: Query<(Entity, &ExtractedText)>,
1050) {
1051 let vw = viewport.size.x;
1052 let vh = viewport.size.y;
1053 for (e, r) in &rects {
1054 if r.origin.x + r.size.x <= 0.0
1055 || r.origin.y + r.size.y <= 0.0
1056 || r.origin.x >= vw
1057 || r.origin.y >= vh
1058 {
1059 commands.entity(e).despawn();
1060 }
1061 }
1062 for (e, t) in &texts {
1063 let h = t.size_px;
1065 let w = t.size_px * (t.text.chars().count() as f32 + 1.0);
1066 if t.origin.x + w <= 0.0
1067 || t.origin.y + h <= 0.0
1068 || t.origin.x >= vw
1069 || t.origin.y >= vh
1075 {
1076 commands.entity(e).despawn();
1077 }
1078 }
1079}
1080
1081#[derive(Resource, Default, Debug)]
1086pub struct RenderEntityMap {
1087 pub rect: std::collections::HashMap<Entity, Entity>,
1089 pub text: std::collections::HashMap<Entity, Entity>,
1091 pub shadow: std::collections::HashMap<Entity, Vec<Entity>>,
1093 pub outline: std::collections::HashMap<Entity, Entity>,
1095 pub border: std::collections::HashMap<Entity, Entity>,
1097 pub image: std::collections::HashMap<Entity, Entity>,
1099 pub svg: std::collections::HashMap<Entity, Entity>,
1101 pub clip: std::collections::HashMap<Entity, Entity>,
1103 pub scrollbar: std::collections::HashMap<Entity, Entity>,
1106}
1107
1108pub fn clear_extracted(render: &mut World) {
1113 let upserted: std::collections::HashSet<Entity> = {
1114 let map = render.resource::<RenderEntityMap>();
1115 let mut set: std::collections::HashSet<Entity> =
1116 std::collections::HashSet::with_capacity(map.rect.len() + map.shadow.len());
1117 set.extend(map.rect.values().copied());
1118 set.extend(map.shadow.values().flatten().copied());
1119 set.extend(map.text.values().copied());
1120 set.extend(map.outline.values().copied());
1121 set.extend(map.border.values().copied());
1122 set.extend(map.clip.values().copied());
1123 set.extend(map.image.values().copied());
1124 set.extend(map.svg.values().copied());
1125 set.extend(map.scrollbar.values().copied());
1126 set
1127 };
1128 let to_despawn: Vec<Entity> = render
1129 .query_filtered::<Entity, Or<(
1130 With<ExtractedRect>,
1131 With<ExtractedText>,
1132 With<ExtractedImage>,
1133 With<ExtractedOutline>,
1134 With<ExtractedBorder>,
1135 With<ExtractedShadow>,
1136 With<ExtractedScrollbar>,
1137 )>>()
1138 .iter(render)
1139 .filter(|e| !upserted.contains(e))
1140 .collect();
1141 for e in to_despawn {
1142 render.despawn(e);
1143 }
1144}
1145
1146pub fn extract_shadows(main: &mut World, render: &mut World) {
1149 let (parents, mut depth_cache) = build_parent_map(main);
1150 let hidden = hidden_entities(main, &parents);
1151 let scroll = parent_scroll_offsets(main, &parents);
1152 let inherited_alpha = parent_opacities(main, &parents);
1153 let mut q = main.query::<(Entity, &Transform, &Visuals, Option<&Opacity>)>();
1154 let mut groups: std::collections::HashMap<Entity, Vec<ExtractedShadow>> =
1156 std::collections::HashMap::new();
1157 for (e, t, v, opacity) in q.iter(main) {
1158 if hidden.contains(&e) || v.shadows.is_empty() {
1159 continue;
1160 }
1161 let alpha = effective_opacity(opacity, &inherited_alpha, e);
1162 let off = scroll.get(&e).copied().unwrap_or(Vec2::ZERO);
1163 let base_order = paint_order_of(e, &parents, &mut depth_cache).saturating_sub(1);
1164 let mut entries = Vec::with_capacity(v.shadows.len());
1165 for (idx, s) in v.shadows.iter().enumerate() {
1166 let order = base_order.saturating_add(idx as u32);
1167 entries.push(ExtractedShadow {
1168 origin: Vec2::new(t.absolute.x + s.offset_x, t.absolute.y + s.offset_y) - off,
1169 size: t.size,
1170 radius: v.radius,
1171 spread: s.spread,
1172 blur: s.blur,
1173 color: alpha.apply(s.color),
1174 order,
1175 inner: s.inner,
1176 rect_origin: t.absolute - off,
1177 });
1178 }
1179 groups.insert(e, entries);
1180 }
1181
1182 let prior = std::mem::take(&mut render.resource_mut::<RenderEntityMap>().shadow);
1184 let mut next: std::collections::HashMap<Entity, Vec<Entity>> =
1185 std::collections::HashMap::with_capacity(groups.len());
1186 for (main_e, shadows) in groups {
1187 let mut slots: Vec<Entity> = prior
1189 .get(&main_e)
1190 .cloned()
1191 .unwrap_or_default()
1192 .into_iter()
1193 .filter(|&re| render.get_entity(re).is_ok())
1194 .collect();
1195 while slots.len() < shadows.len() {
1196 slots.push(
1197 render
1198 .spawn(ExtractedShadow {
1199 origin: Vec2::ZERO,
1200 size: Vec2::ZERO,
1201 radius: 0.0,
1202 spread: 0.0,
1203 blur: 0.0,
1204 color: Color::rgba(0.0, 0.0, 0.0, 0.0),
1205 order: 0,
1206 inner: false,
1207 rect_origin: Vec2::ZERO,
1208 })
1209 .id(),
1210 );
1211 }
1212 while slots.len() > shadows.len() {
1213 let drop = slots.pop().unwrap();
1214 if let Ok(em) = render.get_entity_mut(drop) {
1215 em.despawn();
1216 }
1217 }
1218 for (re, s) in slots.iter().copied().zip(shadows) {
1219 render.entity_mut(re).insert(s);
1220 }
1221 next.insert(main_e, slots);
1222 }
1223 for (main_e, slots) in &prior {
1225 if !next.contains_key(main_e) {
1226 for re in slots {
1227 if let Ok(em) = render.get_entity_mut(*re) {
1228 em.despawn();
1229 }
1230 }
1231 }
1232 }
1233 render.resource_mut::<RenderEntityMap>().shadow = next;
1234}
1235
1236pub fn extract_rects(main: &mut World, render: &mut World) {
1241 let (parents, mut depth_cache) = build_parent_map(main);
1242 let hidden = hidden_entities(main, &parents);
1243 let scroll = parent_scroll_offsets(main, &parents);
1244 let inherited_alpha = parent_opacities(main, &parents);
1245 let clip = parent_scroll_clip_rects(main, &parents);
1246 let mut q = main.query::<(Entity, &Transform, &Visuals, Option<&Opacity>)>();
1247 let pairs: Vec<(Entity, ExtractedRect)> = q
1248 .iter(main)
1249 .filter(|(e, _, _, _)| !hidden.contains(e))
1250 .filter_map(|(e, t, v, opacity)| {
1251 let alpha = effective_opacity(opacity, &inherited_alpha, e);
1252 let brush = Brush::from(v.fill.as_ref()?).with_opacity(alpha);
1253 let off = scroll.get(&e).copied().unwrap_or(Vec2::ZERO);
1254 let origin = t.absolute - off;
1255 if let Some(clip_rect) = clip.get(&e)
1257 && aabb_outside(origin, t.size, *clip_rect)
1258 {
1259 return None;
1260 }
1261 Some((
1262 e,
1263 ExtractedRect {
1264 origin,
1265 size: t.size,
1266 brush,
1267 radius: v.radius,
1268 corner_radii: v.corner_radii,
1269 order: paint_order_of(e, &parents, &mut depth_cache),
1270 },
1271 ))
1272 })
1273 .collect();
1274
1275 let prior = std::mem::take(&mut render.resource_mut::<RenderEntityMap>().rect);
1279 let mut next: std::collections::HashMap<Entity, Entity> =
1280 std::collections::HashMap::with_capacity(pairs.len());
1281 for (main_e, rect) in pairs {
1282 let reuse = prior
1283 .get(&main_e)
1284 .copied()
1285 .filter(|&re| render.get_entity(re).is_ok());
1286 let render_e = match reuse {
1287 Some(re) => {
1288 render.entity_mut(re).insert(rect);
1289 re
1290 }
1291 None => render.spawn(rect).id(),
1292 };
1293 next.insert(main_e, render_e);
1294 }
1295 for (main_e, render_e) in &prior {
1297 if !next.contains_key(main_e)
1298 && let Ok(em) = render.get_entity_mut(*render_e)
1299 {
1300 em.despawn();
1301 }
1302 }
1303 render.resource_mut::<RenderEntityMap>().rect = next;
1304}
1305
1306pub fn extract_borders(main: &mut World, render: &mut World) {
1313 use crate::components::ResolvedDirection;
1314 let (parents, mut depth_cache) = build_parent_map(main);
1315 let hidden = hidden_entities(main, &parents);
1316 let scroll = parent_scroll_offsets(main, &parents);
1317 let inherited_alpha = parent_opacities(main, &parents);
1318 let clip = parent_scroll_clip_rects(main, &parents);
1319 type Row<'a> = (
1320 Entity,
1321 &'a Transform,
1322 &'a Visuals,
1323 Option<&'a Opacity>,
1324 Option<&'a ResolvedDirection>,
1325 );
1326 let mut q = main.query::<Row>();
1327 let pairs: Vec<(Entity, ExtractedBorder)> = q
1328 .iter(main)
1329 .filter(|(e, _, _, _, _)| !hidden.contains(e))
1330 .filter_map(|(e, t, v, opacity, dir)| {
1331 let border = v.border.as_ref()?;
1332 let widths = border
1333 .widths
1334 .resolved(dir.map(|d| d.direction()).unwrap_or_default());
1335 if widths.top <= 0.0
1336 && widths.right <= 0.0
1337 && widths.bottom <= 0.0
1338 && widths.left <= 0.0
1339 {
1340 return None;
1341 }
1342 let alpha = effective_opacity(opacity, &inherited_alpha, e);
1343 let off = scroll.get(&e).copied().unwrap_or(Vec2::ZERO);
1344 let origin = t.absolute - off;
1345 if let Some(clip_rect) = clip.get(&e)
1346 && aabb_outside(origin, t.size, *clip_rect)
1347 {
1348 return None;
1349 }
1350 Some((
1351 e,
1352 ExtractedBorder {
1353 origin,
1354 size: t.size,
1355 widths: [widths.top, widths.right, widths.bottom, widths.left],
1356 color: alpha.apply(border.color),
1357 side_colors: border.side_colors.map(|cs| cs.map(|c| alpha.apply(c))),
1358 radius: v.radius,
1359 corner_radii: v.corner_radii,
1360 order: paint_order_of(e, &parents, &mut depth_cache),
1361 },
1362 ))
1363 })
1364 .collect();
1365
1366 let prior = std::mem::take(&mut render.resource_mut::<RenderEntityMap>().border);
1369 let mut next: std::collections::HashMap<Entity, Entity> =
1370 std::collections::HashMap::with_capacity(pairs.len());
1371 for (main_e, border) in pairs {
1372 let reuse = prior
1373 .get(&main_e)
1374 .copied()
1375 .filter(|&re| render.get_entity(re).is_ok());
1376 let render_e = match reuse {
1377 Some(re) => {
1378 render.entity_mut(re).insert(border);
1379 re
1380 }
1381 None => render.spawn(border).id(),
1382 };
1383 next.insert(main_e, render_e);
1384 }
1385 for (main_e, render_e) in &prior {
1386 if !next.contains_key(main_e)
1387 && let Ok(em) = render.get_entity_mut(*render_e)
1388 {
1389 em.despawn();
1390 }
1391 }
1392 render.resource_mut::<RenderEntityMap>().border = next;
1393}
1394
1395pub fn parent_scroll_clip_rects(
1400 main: &mut World,
1401 parents: &std::collections::HashMap<Entity, Entity>,
1402) -> std::collections::HashMap<Entity, (Vec2, Vec2)> {
1403 use crate::components::{Overflow, Style, Transform};
1404 use crate::input::Scroll;
1405 if let Some(c) = main.get_resource::<ExtractContextCache>()
1406 && c.active
1407 && let Some(v) = &c.clip
1408 {
1409 return v.clone();
1410 }
1411 let clippers: std::collections::HashMap<Entity, (Vec2, Vec2)> = {
1413 let mut q = main.query::<(Entity, &Transform, &Style, Option<&Scroll>)>();
1414 q.iter(main)
1415 .filter_map(|(e, t, style, scroll)| {
1416 let qualifies = scroll.is_some()
1417 || matches!(style.overflow_y, Overflow::Hidden)
1418 || matches!(style.overflow_x, Overflow::Hidden);
1419 qualifies.then_some((e, (t.absolute, t.size)))
1420 })
1421 .collect()
1422 };
1423 if clippers.is_empty() {
1424 if let Some(mut c) = main.get_resource_mut::<ExtractContextCache>()
1425 && c.active
1426 {
1427 c.clip = Some(std::collections::HashMap::new());
1428 }
1429 return std::collections::HashMap::new();
1430 }
1431 let overlay: std::collections::HashSet<Entity> = {
1435 let mut oq = main.query_filtered::<Entity, With<OverlayLayer>>();
1436 oq.iter(main).collect()
1437 };
1438 let mut out: std::collections::HashMap<Entity, (Vec2, Vec2)> = std::collections::HashMap::new();
1439 let mut cache: std::collections::HashMap<Entity, Option<(Vec2, Vec2)>> =
1440 std::collections::HashMap::new();
1441 fn resolve(
1442 e: Entity,
1443 parents: &std::collections::HashMap<Entity, Entity>,
1444 clippers: &std::collections::HashMap<Entity, (Vec2, Vec2)>,
1445 overlay: &std::collections::HashSet<Entity>,
1446 cache: &mut std::collections::HashMap<Entity, Option<(Vec2, Vec2)>>,
1447 ) -> Option<(Vec2, Vec2)> {
1448 if let Some(v) = cache.get(&e) {
1449 return *v;
1450 }
1451 let result = if overlay.contains(&e) {
1452 None
1454 } else if let Some(p) = parents.get(&e).copied() {
1455 if let Some(rect) = clippers.get(&p) {
1456 Some(*rect)
1457 } else {
1458 resolve(p, parents, clippers, overlay, cache)
1459 }
1460 } else {
1461 None
1462 };
1463 cache.insert(e, result);
1464 result
1465 }
1466 for &e in parents.keys() {
1467 if let Some(rect) = resolve(e, parents, &clippers, &overlay, &mut cache) {
1468 out.insert(e, rect);
1469 }
1470 }
1471 if let Some(mut c) = main.get_resource_mut::<ExtractContextCache>()
1472 && c.active
1473 {
1474 c.clip = Some(out.clone());
1475 }
1476 out
1477}
1478
1479fn aabb_outside(origin: Vec2, size: Vec2, clip: (Vec2, Vec2)) -> bool {
1488 let (co, cs) = clip;
1489 origin.x + size.x <= co.x
1490 || origin.y + size.y <= co.y
1491 || origin.x >= co.x + cs.x
1492 || origin.y >= co.y + cs.y
1493}
1494
1495pub fn extract_clips(main: &mut World, render: &mut World) {
1501 use crate::components::{Overflow, Style, Transform};
1502 use crate::input::Scroll;
1503 let (parents, mut depth_cache) = build_parent_map(main);
1504 let hidden = hidden_entities(main, &parents);
1505 let mut children: std::collections::HashMap<Entity, Vec<Entity>> =
1507 std::collections::HashMap::new();
1508 for (&e, &p) in parents.iter() {
1509 children.entry(p).or_default().push(e);
1510 }
1511 let mut candidates: Vec<Entity> = Vec::new();
1513 {
1514 let mut q = main.query::<(Entity, &Style, Option<&Scroll>)>();
1515 for (e, style, scroll) in q.iter(main) {
1516 if hidden.contains(&e) {
1517 continue;
1518 }
1519 let clip_y = matches!(style.overflow_y, Overflow::Hidden) || scroll.is_some();
1520 let clip_x = matches!(style.overflow_x, Overflow::Hidden) || scroll.is_some();
1521 if clip_x || clip_y {
1522 candidates.push(e);
1523 }
1524 }
1525 }
1526 if candidates.is_empty() {
1527 let prior = std::mem::take(&mut render.resource_mut::<RenderEntityMap>().clip);
1529 for re in prior.values() {
1530 if let Ok(em) = render.get_entity_mut(*re) {
1531 em.despawn();
1532 }
1533 }
1534 return;
1535 }
1536 let transforms: std::collections::HashMap<Entity, Transform> = {
1538 let mut q = main.query::<(Entity, &Transform)>();
1539 q.iter(main).map(|(e, t)| (e, *t)).collect()
1540 };
1541 let overlay: std::collections::HashSet<Entity> = {
1544 let mut oq = main.query_filtered::<Entity, With<OverlayLayer>>();
1545 oq.iter(main).collect()
1546 };
1547 fn max_desc_order(
1551 root: Entity,
1552 children: &std::collections::HashMap<Entity, Vec<Entity>>,
1553 parents: &std::collections::HashMap<Entity, Entity>,
1554 overlay: &std::collections::HashSet<Entity>,
1555 depth_cache: &mut std::collections::HashMap<Entity, u32>,
1556 ) -> PaintOrder {
1557 let mut stack = vec![root];
1558 let mut best = paint_order_of(root, parents, depth_cache);
1559 while let Some(n) = stack.pop() {
1560 let order = paint_order_of(n, parents, depth_cache);
1561 if order > best {
1562 best = order;
1563 }
1564 if let Some(kids) = children.get(&n) {
1565 for &k in kids {
1566 if !overlay.contains(&k) {
1567 stack.push(k);
1568 }
1569 }
1570 }
1571 }
1572 best
1573 }
1574 let pairs: Vec<(Entity, ExtractedClipBox)> = candidates
1575 .into_iter()
1576 .filter_map(|e| {
1577 let t = transforms.get(&e).copied()?;
1578 let own = paint_order_of(e, &parents, &mut depth_cache);
1579 let end = max_desc_order(e, &children, &parents, &overlay, &mut depth_cache);
1580 let radius = main
1581 .get::<crate::components::Visuals>(e)
1582 .map(|v| v.radius)
1583 .unwrap_or(0.0);
1584 Some((
1585 e,
1586 ExtractedClipBox {
1587 origin: t.absolute,
1588 size: t.size,
1589 radius,
1590 start_order: own,
1591 end_order: end,
1592 },
1593 ))
1594 })
1595 .collect();
1596 let prior = std::mem::take(&mut render.resource_mut::<RenderEntityMap>().clip);
1598 let mut next: std::collections::HashMap<Entity, Entity> =
1599 std::collections::HashMap::with_capacity(pairs.len());
1600 for (main_e, clip) in pairs {
1601 let reuse = prior
1602 .get(&main_e)
1603 .copied()
1604 .filter(|&re| render.get_entity(re).is_ok());
1605 let render_e = match reuse {
1606 Some(re) => {
1607 render.entity_mut(re).insert(clip);
1608 re
1609 }
1610 None => render.spawn(clip).id(),
1611 };
1612 next.insert(main_e, render_e);
1613 }
1614 for (main_e, render_e) in &prior {
1615 if !next.contains_key(main_e)
1616 && let Ok(em) = render.get_entity_mut(*render_e)
1617 {
1618 em.despawn();
1619 }
1620 }
1621 render.resource_mut::<RenderEntityMap>().clip = next;
1622}
1623
1624pub fn build_parent_map(
1643 main: &mut World,
1644) -> (
1645 std::collections::HashMap<Entity, Entity>,
1646 std::collections::HashMap<Entity, u32>,
1647) {
1648 if let Some(c) = main.get_resource::<ExtractContextCache>()
1651 && c.active
1652 && let Some(pm) = &c.parent_map
1653 {
1654 return pm.clone();
1655 }
1656 use bevy_ecs::hierarchy::{ChildOf, Children};
1657 let mut pq = main.query::<(Entity, &ChildOf)>();
1658 let parents: std::collections::HashMap<Entity, Entity> =
1659 pq.iter(main).map(|(e, p)| (e, p.parent())).collect();
1660 let z_of: std::collections::HashMap<Entity, i32> = {
1663 let mut zq = main.query::<(Entity, &crate::components::ZIndex)>();
1664 zq.iter(main).map(|(e, z)| (e, z.0)).collect()
1665 };
1666 let children: std::collections::HashMap<Entity, Vec<Entity>> = {
1671 let mut cq = main.query::<(Entity, &Children)>();
1672 cq.iter(main)
1673 .map(|(e, kids)| {
1674 let mut list: Vec<Entity> = kids.iter().collect();
1675 if !z_of.is_empty() {
1676 list.sort_by_key(|c| z_of.get(c).copied().unwrap_or(0));
1677 }
1678 (e, list)
1679 })
1680 .collect()
1681 };
1682 let overlay_roots: Vec<Entity> = {
1684 let mut oq = main.query_filtered::<Entity, With<OverlayLayer>>();
1685 oq.iter(main).collect()
1686 };
1687 let overlay_set: std::collections::HashSet<Entity> = overlay_roots.iter().copied().collect();
1688 let mut roots: Vec<Entity> = children
1691 .keys()
1692 .copied()
1693 .filter(|e| !parents.contains_key(e))
1694 .collect();
1695 roots.sort_by_key(|e| e.to_bits());
1696 let mut order: std::collections::HashMap<Entity, u32> =
1699 std::collections::HashMap::with_capacity(parents.len() + roots.len());
1700 let mut stack: Vec<Entity> = roots
1701 .into_iter()
1702 .rev()
1703 .filter(|e| !overlay_set.contains(e))
1704 .collect();
1705 let mut rank: u32 = 0;
1706 while let Some(e) = stack.pop() {
1707 order.insert(e, rank.saturating_mul(2));
1708 rank = rank.saturating_add(1);
1709 if let Some(kids) = children.get(&e) {
1710 stack.extend(
1711 kids.iter()
1712 .rev()
1713 .copied()
1714 .filter(|k| !overlay_set.contains(k)),
1715 );
1716 }
1717 }
1718 if !overlay_roots.is_empty() {
1719 rank_overlay_band(
1720 main,
1721 &parents,
1722 &children,
1723 &overlay_roots,
1724 &overlay_set,
1725 &mut order,
1726 );
1727 }
1728 let result = (parents, order);
1729 if let Some(mut c) = main.get_resource_mut::<ExtractContextCache>()
1730 && c.active
1731 {
1732 c.parent_map = Some(result.clone());
1733 }
1734 result
1735}
1736
1737pub fn parent_opacities(
1744 main: &mut World,
1745 parents: &HashMap<Entity, Entity>,
1746) -> HashMap<Entity, f32> {
1747 if let Some(c) = main.get_resource::<ExtractContextCache>()
1748 && c.active
1749 && let Some(v) = &c.opacities
1750 {
1751 return v.clone();
1752 }
1753 let direct: HashMap<Entity, f32> = {
1754 let mut q = main.query::<(Entity, &Opacity)>();
1755 q.iter(main).map(|(e, o)| (e, o.0)).collect()
1756 };
1757 let by_entity: HashMap<Entity, f32> = if direct.is_empty() {
1758 HashMap::new()
1759 } else {
1760 fn cumulative(
1761 e: Entity,
1762 parents: &HashMap<Entity, Entity>,
1763 direct: &HashMap<Entity, f32>,
1764 cache: &mut HashMap<Entity, f32>,
1765 ) -> f32 {
1766 if let Some(v) = cache.get(&e) {
1767 return *v;
1768 }
1769 let parent_alpha = parents
1770 .get(&e)
1771 .map(|p| cumulative(*p, parents, direct, cache))
1772 .unwrap_or(1.0);
1773 let own = direct.get(&e).copied().unwrap_or(1.0);
1774 let total = parent_alpha * own;
1775 cache.insert(e, total);
1776 total
1777 }
1778 let mut cache: HashMap<Entity, f32> = HashMap::new();
1779 let mut by_entity: HashMap<Entity, f32> = HashMap::new();
1780 for &e in parents.keys() {
1781 if let Some(p) = parents.get(&e) {
1782 let alpha = cumulative(*p, parents, &direct, &mut cache);
1783 if alpha < 1.0 {
1784 by_entity.insert(e, alpha);
1785 }
1786 }
1787 }
1788 by_entity
1789 };
1790 if let Some(mut c) = main.get_resource_mut::<ExtractContextCache>()
1791 && c.active
1792 {
1793 c.opacities = Some(by_entity.clone());
1794 }
1795 by_entity
1796}
1797
1798pub(crate) fn effective_opacity(
1801 own: Option<&Opacity>,
1802 inherited: &HashMap<Entity, f32>,
1803 e: Entity,
1804) -> Opacity {
1805 let own = own.copied().unwrap_or_default().0;
1806 let anc = inherited.get(&e).copied().unwrap_or(1.0);
1807 Opacity(own * anc)
1808}
1809
1810fn rank_overlay_band(
1819 main: &mut World,
1820 parents: &std::collections::HashMap<Entity, Entity>,
1821 children: &std::collections::HashMap<Entity, Vec<Entity>>,
1822 overlay_roots: &[Entity],
1823 overlay_set: &std::collections::HashSet<Entity>,
1824 order: &mut std::collections::HashMap<Entity, u32>,
1825) {
1826 let hidden = hidden_entities(main, parents);
1827 if main.get_resource::<OverlayOpenOrder>().is_none() {
1828 main.insert_resource(OverlayOpenOrder::default());
1829 }
1830 let ordered_roots: Vec<Entity> = {
1831 let mut oo = main.resource_mut::<OverlayOpenOrder>();
1832 let oo = &mut *oo;
1833 oo.stamps
1835 .retain(|e, _| overlay_set.contains(e) && !hidden.contains(e));
1836 let mut newly_open: Vec<Entity> = overlay_roots
1839 .iter()
1840 .copied()
1841 .filter(|e| !hidden.contains(e) && !oo.stamps.contains_key(e))
1842 .collect();
1843 newly_open.sort_by_key(|e| e.to_bits());
1844 for e in newly_open {
1845 let s = oo.next;
1846 oo.next += 1;
1847 oo.stamps.insert(e, s);
1848 }
1849 let mut stamped: Vec<(u64, Entity)> = oo.stamps.iter().map(|(&e, &s)| (s, e)).collect();
1850 stamped.sort_unstable_by_key(|&(s, e)| (s, e.to_bits()));
1851 let mut list: Vec<Entity> = stamped.into_iter().map(|(_, e)| e).collect();
1852 let mut closed: Vec<Entity> = overlay_roots
1853 .iter()
1854 .copied()
1855 .filter(|e| hidden.contains(e))
1856 .collect();
1857 closed.sort_by_key(|e| e.to_bits());
1858 list.extend(closed);
1859 list
1860 };
1861 let mut orank: u32 = OVERLAY_ORDER_BASE / 2;
1863 for root in ordered_roots {
1864 let mut stack = vec![root];
1865 while let Some(e) = stack.pop() {
1866 order.insert(e, orank.saturating_mul(2));
1867 orank = orank.saturating_add(1);
1868 if let Some(kids) = children.get(&e) {
1869 stack.extend(
1870 kids.iter()
1871 .rev()
1872 .copied()
1873 .filter(|k| !overlay_set.contains(k)),
1874 );
1875 }
1876 }
1877 }
1878}
1879
1880pub fn parent_scroll_offsets(
1886 main: &mut World,
1887 parents: &HashMap<Entity, Entity>,
1888) -> HashMap<Entity, Vec2> {
1889 if let Some(c) = main.get_resource::<ExtractContextCache>()
1890 && c.active
1891 && let Some(v) = &c.scroll
1892 {
1893 return v.clone();
1894 }
1895 let direct: HashMap<Entity, Vec2> = {
1896 let mut q = main.query::<(Entity, &ScrollOffset)>();
1897 q.iter(main).map(|(e, o)| (e, o.0)).collect()
1898 };
1899 let by_entity: HashMap<Entity, Vec2> = if direct.is_empty() {
1900 HashMap::new()
1901 } else {
1902 fn cumulative(
1903 e: Entity,
1904 parents: &HashMap<Entity, Entity>,
1905 direct: &HashMap<Entity, Vec2>,
1906 cache: &mut HashMap<Entity, Vec2>,
1907 ) -> Vec2 {
1908 if let Some(v) = cache.get(&e) {
1909 return *v;
1910 }
1911 let parent_off = parents
1912 .get(&e)
1913 .map(|p| cumulative(*p, parents, direct, cache))
1914 .unwrap_or(Vec2::ZERO);
1915 let own = direct.get(&e).copied().unwrap_or(Vec2::ZERO);
1916 let total = parent_off + own;
1917 cache.insert(e, total);
1918 total
1919 }
1920 let mut cache: HashMap<Entity, Vec2> = HashMap::new();
1921 let mut by_entity: HashMap<Entity, Vec2> = HashMap::new();
1922 for &e in parents.keys() {
1923 if let Some(p) = parents.get(&e) {
1924 let off = cumulative(*p, parents, &direct, &mut cache);
1925 if off != Vec2::ZERO {
1926 by_entity.insert(e, off);
1927 }
1928 }
1929 }
1930 by_entity
1931 };
1932 if let Some(mut c) = main.get_resource_mut::<ExtractContextCache>()
1933 && c.active
1934 {
1935 c.scroll = Some(by_entity.clone());
1936 }
1937 by_entity
1938}
1939
1940pub fn hidden_entities(
1945 main: &mut World,
1946 parents: &std::collections::HashMap<Entity, Entity>,
1947) -> std::collections::HashSet<Entity> {
1948 if let Some(c) = main.get_resource::<ExtractContextCache>()
1949 && c.active
1950 && let Some(v) = &c.hidden
1951 {
1952 return v.clone();
1953 }
1954 let mut hide_roots = std::collections::HashSet::new();
1955 let mut q = main.query::<(Entity, &Visible)>();
1956 for (e, v) in q.iter(main) {
1957 if !v.0 {
1958 hide_roots.insert(e);
1959 }
1960 }
1961 let hidden: std::collections::HashSet<Entity> = if hide_roots.is_empty() {
1962 std::collections::HashSet::new()
1963 } else {
1964 let mut cache: std::collections::HashMap<Entity, bool> = std::collections::HashMap::new();
1965 let mut hidden = std::collections::HashSet::new();
1966 for &entity in parents.keys().chain(hide_roots.iter()) {
1967 if is_hidden_walk(entity, &hide_roots, parents, &mut cache) {
1968 hidden.insert(entity);
1969 }
1970 }
1971 hidden
1972 };
1973 if let Some(mut c) = main.get_resource_mut::<ExtractContextCache>()
1974 && c.active
1975 {
1976 c.hidden = Some(hidden.clone());
1977 }
1978 hidden
1979}
1980
1981fn is_hidden_walk(
1982 e: Entity,
1983 roots: &std::collections::HashSet<Entity>,
1984 parents: &std::collections::HashMap<Entity, Entity>,
1985 cache: &mut std::collections::HashMap<Entity, bool>,
1986) -> bool {
1987 if let Some(v) = cache.get(&e) {
1988 return *v;
1989 }
1990 let v = if roots.contains(&e) {
1991 true
1992 } else {
1993 match parents.get(&e) {
1994 Some(p) => is_hidden_walk(*p, roots, parents, cache),
1995 None => false,
1996 }
1997 };
1998 cache.insert(e, v);
1999 v
2000}
2001
2002pub fn paint_order_of(
2010 e: Entity,
2011 _parents: &std::collections::HashMap<Entity, Entity>,
2012 cache: &mut std::collections::HashMap<Entity, u32>,
2013) -> PaintOrder {
2014 if let Some(o) = cache.get(&e) {
2015 return *o;
2016 }
2017 let idx = (e.to_bits() as u32) & 0x3FFF_FFFF;
2018 let o = 0x8000_0000 | (idx << 1);
2019 cache.insert(e, o);
2020 o
2021}
2022
2023#[derive(Clone, Debug)]
2025pub struct SurfaceFrame {
2026 pub width: u32,
2028 pub height: u32,
2030 pub rgba8: Vec<u8>,
2032}
2033
2034#[derive(Resource, Clone, Default)]
2039pub struct SurfaceCapture {
2040 pub request: Arc<AtomicBool>,
2042 pub store: Arc<Mutex<Option<SurfaceFrame>>>,
2044 pub waker: Arc<std::sync::OnceLock<crate::app::EventLoopWaker>>,
2054}
2055
2056impl SurfaceCapture {
2057 pub fn is_requested(&self) -> bool {
2059 self.request.load(Ordering::Acquire)
2060 }
2061
2062 pub fn set_waker(&self, waker: crate::app::EventLoopWaker) {
2065 let _ = self.waker.set(waker);
2066 }
2067
2068 pub fn request(&self) {
2073 self.request.store(true, Ordering::Release);
2074 if let Some(waker) = self.waker.get() {
2075 waker.wake();
2076 }
2077 }
2078
2079 pub fn clear_request(&self) {
2081 self.request.store(false, Ordering::Release);
2082 }
2083
2084 pub fn write(&self, frame: SurfaceFrame) {
2086 if let Ok(mut slot) = self.store.lock() {
2087 *slot = Some(frame);
2088 }
2089 }
2090
2091 pub fn read(&self) -> Option<SurfaceFrame> {
2093 self.store.lock().ok().and_then(|g| g.clone())
2094 }
2095}
2096
2097fn masked_offset(text: &str, plain_byte: usize, mask: char) -> usize {
2102 let mut b = plain_byte.min(text.len());
2103 while b > 0 && !text.is_char_boundary(b) {
2104 b -= 1;
2105 }
2106 text[..b].chars().count() * mask.len_utf8()
2107}
2108
2109fn mask_echo(
2119 mode: EchoMode,
2120 text: &str,
2121 caret: Option<usize>,
2122 selection: Option<(usize, usize)>,
2123 mask: char,
2124) -> (String, Option<usize>, Option<(usize, usize)>) {
2125 match mode {
2126 EchoMode::NoEcho => (String::new(), caret.map(|_| 0), None),
2127 EchoMode::Password => {
2128 let scalars = text.chars().count();
2129 let masked: String = mask.to_string().repeat(scalars);
2130 let caret = caret.map(|c| masked_offset(text, c, mask));
2131 let selection = selection
2132 .map(|(s, e)| (masked_offset(text, s, mask), masked_offset(text, e, mask)));
2133 (masked, caret, selection)
2134 }
2135 EchoMode::Normal => (text.to_string(), caret, selection),
2136 }
2137}
2138
2139pub fn extract_text(main: &mut World, render: &mut World) {
2146 use crate::components::Style;
2147 let (parents, mut depth_cache) = build_parent_map(main);
2148 let hidden = hidden_entities(main, &parents);
2149 let scroll = parent_scroll_offsets(main, &parents);
2150 let inherited_alpha = parent_opacities(main, &parents);
2151 let clip = parent_scroll_clip_rects(main, &parents);
2152 let caret_visible = main
2157 .get_resource::<CaretBlink>()
2158 .map(|b| b.visible)
2159 .unwrap_or(true);
2160 type RowFor<'a> = (
2161 Entity,
2162 &'a Transform,
2163 &'a TextContent,
2164 Option<&'a TextStyle>,
2165 Option<&'a ImeState>,
2166 Option<&'a TextInput>,
2167 Option<&'a Focused>,
2168 Option<&'a Style>,
2169 Option<&'a Opacity>,
2170 Option<&'a TextInputScroll>,
2171 Option<&'a EchoMode>,
2172 Option<&'a TextInputPaint>,
2173 Option<&'a TextBlockOrigin>,
2174 Option<&'a CaretWidth>,
2175 Option<&'a PasswordCharacter>,
2176 );
2177 let mut q = main.query::<RowFor>();
2178 let pairs: Vec<(Entity, ExtractedText)> = q
2179 .iter(main)
2180 .filter(|(e, ..)| !hidden.contains(e))
2181 .filter_map(
2182 |(
2183 e,
2184 t,
2185 text,
2186 ts,
2187 ime,
2188 input,
2189 focused,
2190 style,
2191 opacity,
2192 edit_scroll,
2193 echo,
2194 paint,
2195 block_origin,
2196 caret_width,
2197 password_char,
2198 )| {
2199 let ts = ts.cloned().unwrap_or_default();
2200 let size_px = ts.size_px;
2201 let line_height_px = resolve_line_height(ts.line_height, size_px);
2202 let caret_width_px = caret_width.map(|w| w.0).unwrap_or(CARET_WIDTH_PX);
2203 let mask_char = password_char.map(|c| c.0).unwrap_or(PASSWORD_MASK_CHAR);
2204 let preedit = ime.map(|i| i.preedit.as_str()).unwrap_or("");
2205 let placeholder = match input {
2209 Some(i) if text.0.is_empty() && preedit.is_empty() => i.placeholder.as_str(),
2210 _ => "",
2211 };
2212 if input.is_none() && text.0.is_empty() && preedit.is_empty() {
2213 return None;
2214 }
2215 let caret = match (input, focused) {
2216 (Some(i), Some(_)) if caret_visible => {
2217 let base = i.cursor.min(text.0.len());
2219 Some(base + preedit.len())
2220 }
2221 _ => None,
2222 };
2223 let caret = caret.map(|c| if placeholder.is_empty() { c } else { 0 });
2226 let combined = if !placeholder.is_empty() {
2227 placeholder.to_string()
2228 } else {
2229 format!("{}{}", text.0, preedit)
2230 };
2231 let selection = match (input, focused) {
2233 (Some(i), Some(_)) => i.selection_anchor.and_then(|a| {
2234 let cur = i.cursor.min(text.0.len());
2235 let a = a.min(text.0.len());
2236 if a == cur {
2237 None
2238 } else {
2239 Some((a.min(cur), a.max(cur)))
2240 }
2241 }),
2242 _ => None,
2243 };
2244 let (combined, caret, selection) = match echo {
2252 Some(mode) if mode.is_concealed() && placeholder.is_empty() => {
2253 mask_echo(*mode, &combined, caret, selection, mask_char)
2254 }
2255 _ => (combined, caret, selection),
2256 };
2257 let pad_left = style.map(|s| s.padding.left).unwrap_or(0.0);
2262 let pad_right = style.map(|s| s.padding.right).unwrap_or(0.0);
2263 let pad_top = style.map(|s| s.padding.top).unwrap_or(0.0);
2264 let pad_bottom = style.map(|s| s.padding.bottom).unwrap_or(0.0);
2265 let inner_h = (t.size.y - pad_top - pad_bottom).max(size_px);
2266 let block_top = block_origin.map(|b| b.top).unwrap_or_else(|| {
2267 let stacked = input.is_some_and(|i| i.multiline) || combined.contains('\n');
2268 text_block_top(inner_h, line_height_px, stacked)
2269 });
2270 let baseline_y = t.absolute.y
2271 + pad_top
2272 + block_top
2273 + text_baseline_in_line(size_px, line_height_px);
2274 let container_width = (t.size.x - pad_left - pad_right).max(0.0);
2275 let alpha = effective_opacity(opacity, &inherited_alpha, e);
2276 let off = scroll.get(&e).copied().unwrap_or(Vec2::ZERO);
2277 if let Some(clip_rect) = clip.get(&e) {
2279 let probe_origin = Vec2::new(t.absolute.x, t.absolute.y) - off;
2280 if aabb_outside(probe_origin, t.size, *clip_rect) {
2281 return None;
2282 }
2283 }
2284 let edit_off = edit_scroll.map(|s| s.offset).unwrap_or(Vec2::ZERO);
2288 Some((
2289 e,
2290 ExtractedText {
2291 origin: Vec2::new(t.absolute.x + pad_left, baseline_y) - off - edit_off,
2292 text: combined,
2293 size_px,
2294 fill: alpha.apply(ts.color),
2295 caret,
2296 selection,
2297 selection_color: ts.selection_color.map(|c| alpha.apply(c)),
2298 selection_foreground: paint
2299 .and_then(|p| p.selection_foreground)
2300 .map(|c| alpha.apply(c)),
2301 caret_color: paint.and_then(|p| p.caret_color).map(|c| alpha.apply(c)),
2302 container_width,
2303 align: ts.align,
2304 wrap: ts.wrap,
2305 max_lines: ts.max_lines,
2306 family: ts.family.clone(),
2307 weight: ts.weight,
2308 line_height_px,
2309 caret_width_px,
2310 order: paint_order_of(e, &parents, &mut depth_cache),
2311 },
2312 ))
2313 },
2314 )
2315 .collect();
2316 let prior = std::mem::take(&mut render.resource_mut::<RenderEntityMap>().text);
2318 let mut next: std::collections::HashMap<Entity, Entity> =
2319 std::collections::HashMap::with_capacity(pairs.len());
2320 for (main_e, et) in pairs {
2321 let reuse = prior
2322 .get(&main_e)
2323 .copied()
2324 .filter(|&re| render.get_entity(re).is_ok());
2325 let render_e = match reuse {
2326 Some(re) => {
2327 render.entity_mut(re).insert(et);
2328 re
2329 }
2330 None => render.spawn(et).id(),
2331 };
2332 next.insert(main_e, render_e);
2333 }
2334 for (main_e, render_e) in &prior {
2335 if !next.contains_key(main_e)
2336 && let Ok(em) = render.get_entity_mut(*render_e)
2337 {
2338 em.despawn();
2339 }
2340 }
2341 render.resource_mut::<RenderEntityMap>().text = next;
2342}
2343
2344#[cfg(test)]
2345mod echo_mask_tests {
2346 use super::*;
2350
2351 #[test]
2352 fn password_masks_each_scalar_and_remaps_caret() {
2353 let (disp, caret, sel) =
2356 mask_echo(EchoMode::Password, "abc", Some(2), None, PASSWORD_MASK_CHAR);
2357 assert_eq!(disp.chars().count(), 3);
2358 assert!(disp.chars().all(|c| c == PASSWORD_MASK_CHAR));
2359 assert_eq!(caret, Some(6));
2360 assert_eq!(sel, None);
2361 }
2362
2363 #[test]
2364 fn password_remaps_selection_range() {
2365 let (_disp, _caret, sel) = mask_echo(
2367 EchoMode::Password,
2368 "abc",
2369 None,
2370 Some((1, 3)),
2371 PASSWORD_MASK_CHAR,
2372 );
2373 assert_eq!(sel, Some((3, 9)));
2374 }
2375
2376 #[test]
2377 fn password_scalar_count_not_byte_count() {
2378 let (disp, caret, _) = mask_echo(
2381 EchoMode::Password,
2382 "\u{e9}",
2383 Some(2),
2384 None,
2385 PASSWORD_MASK_CHAR,
2386 );
2387 assert_eq!(disp.chars().count(), 1);
2388 assert_eq!(caret, Some(3));
2389 }
2390
2391 #[test]
2396 fn password_character_override_changes_mask_glyph_and_width() {
2397 let (disp, caret, _) = mask_echo(EchoMode::Password, "abc", Some(2), None, '*');
2398 assert_eq!(disp, "***");
2399 assert_eq!(caret, Some(2), "1-byte mask -> caret byte == scalar count");
2400 }
2401
2402 #[test]
2403 fn masked_offset_snaps_mid_codepoint_down() {
2404 assert_eq!(EchoMode::Password.display_offset("\u{e9}", 1), 0);
2406 }
2407
2408 #[test]
2409 fn masked_offset_round_trips_to_the_plaintext_byte() {
2410 let plain = "a\u{e9}bc";
2413 for (i, _) in plain
2414 .char_indices()
2415 .chain(std::iter::once((plain.len(), 'x')))
2416 {
2417 let d = EchoMode::Password.display_offset(plain, i);
2418 assert_eq!(EchoMode::Password.plain_offset(plain, d), i);
2419 }
2420 }
2421
2422 #[test]
2423 fn no_echo_hides_everything_and_collapses_caret() {
2424 let (disp, caret, sel) = mask_echo(
2425 EchoMode::NoEcho,
2426 "secret",
2427 Some(4),
2428 Some((0, 6)),
2429 PASSWORD_MASK_CHAR,
2430 );
2431 assert!(disp.is_empty());
2432 assert_eq!(caret, Some(0), "caret collapses to the origin");
2433 assert_eq!(sel, None, "nothing to highlight under no-echo");
2434 }
2435
2436 #[test]
2440 fn extract_masks_password_run_but_not_placeholder() {
2441 use crate::components::{TextContent, TextInput, Transform};
2442 use crate::input::Focused;
2443
2444 fn masked_text_for(content: &str, placeholder: &str) -> String {
2445 let mut main = World::new();
2446 let mut render = World::new();
2447 render.init_resource::<RenderEntityMap>();
2448 let e = main
2449 .spawn((
2450 Transform::new(Vec2::ZERO, Vec2::new(120.0, 24.0)),
2451 TextContent(content.to_string()),
2452 TextInput {
2453 placeholder: placeholder.to_string(),
2454 cursor: content.len(),
2455 ..Default::default()
2456 },
2457 EchoMode::Password,
2458 Focused,
2459 ))
2460 .id();
2461 let _ = e;
2462 extract_text(&mut main, &mut render);
2463 let mut q = render.query::<&ExtractedText>();
2464 q.iter(&render).next().unwrap().text.clone()
2465 }
2466
2467 let masked = masked_text_for("hunter2", "");
2469 assert_eq!(masked.chars().count(), 7);
2470 assert!(masked.chars().all(|c| c == PASSWORD_MASK_CHAR));
2471
2472 assert_eq!(masked_text_for("", "Password"), "Password");
2475 }
2476
2477 #[test]
2482 fn extract_password_character_override_replaces_default_mask() {
2483 use crate::components::{TextContent, TextInput, Transform};
2484 use crate::input::Focused;
2485
2486 let mut main = World::new();
2487 let mut render = World::new();
2488 render.init_resource::<RenderEntityMap>();
2489 main.spawn((
2490 Transform::new(Vec2::ZERO, Vec2::new(120.0, 24.0)),
2491 TextContent("hunter2".to_string()),
2492 TextInput {
2493 cursor: 7,
2494 ..Default::default()
2495 },
2496 EchoMode::Password,
2497 PasswordCharacter('*'),
2498 Focused,
2499 ));
2500 extract_text(&mut main, &mut render);
2501 let mut q = render.query::<&ExtractedText>();
2502 let text = q.iter(&render).next().unwrap().text.clone();
2503 assert_eq!(text, "*******");
2504 }
2505}
2506
2507#[cfg(test)]
2508mod tests {
2509 use super::*;
2510 use bevy_ecs::hierarchy::ChildOf;
2511
2512 #[test]
2518 fn hidden_root_subtree_extracts_zero_paint_nodes() {
2519 use crate::components::{Fill, TextContent, Transform, Visible, Visuals};
2520
2521 let mut main = World::new();
2522 let mut render = World::new();
2523 render.insert_resource(RenderEntityMap::default());
2524
2525 let base = main
2527 .spawn((
2528 Transform {
2529 absolute: Vec2::ZERO,
2530 size: Vec2::new(100.0, 100.0),
2531 baseline_y: None,
2532 },
2533 Visuals {
2534 fill: Some(Fill::Solid(Color::rgba(1.0, 0.0, 0.0, 1.0))),
2535 ..Default::default()
2536 },
2537 ))
2538 .id();
2539
2540 let overlay = main
2542 .spawn((
2543 Transform {
2544 absolute: Vec2::ZERO,
2545 size: Vec2::new(100.0, 100.0),
2546 baseline_y: None,
2547 },
2548 Visuals {
2549 fill: Some(Fill::Solid(Color::rgba(0.0, 1.0, 0.0, 1.0))),
2550 ..Default::default()
2551 },
2552 Visible(false),
2553 ))
2554 .id();
2555 let panel = main
2556 .spawn((
2557 Transform {
2558 absolute: Vec2::new(10.0, 10.0),
2559 size: Vec2::new(50.0, 50.0),
2560 baseline_y: None,
2561 },
2562 Visuals {
2563 fill: Some(Fill::Solid(Color::rgba(0.0, 0.0, 1.0, 1.0))),
2564 ..Default::default()
2565 },
2566 ChildOf(overlay),
2567 ))
2568 .id();
2569 let _label = main
2570 .spawn((
2571 Transform {
2572 absolute: Vec2::new(12.0, 12.0),
2573 size: Vec2::new(40.0, 16.0),
2574 baseline_y: None,
2575 },
2576 TextContent("devtools".into()),
2577 ChildOf(panel),
2578 ))
2579 .id();
2580
2581 extract_rects(&mut main, &mut render);
2582 extract_text(&mut main, &mut render);
2583
2584 let rects: Vec<(Entity, Vec2)> = {
2586 let mut q = render.query::<(Entity, &ExtractedRect)>();
2587 q.iter(&render).map(|(e, r)| (e, r.origin)).collect()
2588 };
2589 assert_eq!(
2590 rects.len(),
2591 1,
2592 "only the visible base rect extracts; the hidden subtree contributes none (got {rects:?})"
2593 );
2594 let map = render.resource::<RenderEntityMap>();
2596 assert_eq!(map.rect.get(&base).copied(), Some(rects[0].0));
2597 assert!(!map.rect.contains_key(&overlay));
2598 assert!(!map.rect.contains_key(&panel));
2599
2600 let text_count = render.query::<&ExtractedText>().iter(&render).count();
2602 assert_eq!(text_count, 0, "hidden subtree text must not extract");
2603 }
2604
2605 #[test]
2610 fn cull_hidden_despawns_leaked_render_entities() {
2611 use crate::components::{Transform, Visible};
2612
2613 let mut main = World::new();
2614 let mut render = World::new();
2615 render.insert_resource(RenderEntityMap::default());
2616 render.insert_resource(HiddenExtracts::default());
2617
2618 let hidden_root = main.spawn((Transform::default(), Visible(false))).id();
2619 let child = main
2620 .spawn((Transform::default(), ChildOf(hidden_root)))
2621 .id();
2622
2623 let leaked_rect = render
2626 .spawn(ExtractedRect {
2627 origin: Vec2::ZERO,
2628 size: Vec2::new(10.0, 10.0),
2629 brush: Brush::Solid(Color::rgba(0.0, 1.0, 0.0, 1.0)),
2630 radius: 0.0,
2631 corner_radii: None,
2632 order: 0,
2633 })
2634 .id();
2635 let leaked_img = render
2636 .spawn(ExtractedRect {
2637 origin: Vec2::ZERO,
2638 size: Vec2::new(10.0, 10.0),
2639 brush: Brush::Solid(Color::rgba(0.0, 0.0, 1.0, 1.0)),
2640 radius: 0.0,
2641 corner_radii: None,
2642 order: 0,
2643 })
2644 .id();
2645 {
2646 let mut map = render.resource_mut::<RenderEntityMap>();
2647 map.rect.insert(hidden_root, leaked_rect);
2648 map.image.insert(child, leaked_img);
2649 }
2650
2651 stash_hidden_entities(&mut main, &mut render);
2653 let mut schedule = Schedule::default();
2654 schedule.add_systems(cull_hidden);
2655 schedule.run(&mut render);
2656
2657 assert!(
2658 render.get_entity(leaked_rect).is_err(),
2659 "leaked rect for the hidden root must be despawned"
2660 );
2661 assert!(
2662 render.get_entity(leaked_img).is_err(),
2663 "leaked image for the hidden child must be despawned"
2664 );
2665 let map = render.resource::<RenderEntityMap>();
2666 assert!(map.rect.is_empty(), "hidden slot pruned from rect map");
2667 assert!(map.image.is_empty(), "hidden slot pruned from image map");
2668 }
2669
2670 #[test]
2673 fn paint_order_follows_document_order_not_entity_ids() {
2674 let mut world = World::new();
2675 let child_a = world.spawn_empty().id();
2676 let child_b = world.spawn_empty().id();
2677 let grandchild = world.spawn_empty().id();
2678 let parent = world.spawn_empty().id();
2679 world.entity_mut(child_a).insert(ChildOf(parent));
2681 world.entity_mut(child_b).insert(ChildOf(parent));
2682 world.entity_mut(grandchild).insert(ChildOf(child_a));
2683
2684 let (parents, mut cache) = build_parent_map(&mut world);
2685 let po_parent = paint_order_of(parent, &parents, &mut cache);
2686 let po_a = paint_order_of(child_a, &parents, &mut cache);
2687 let po_gc = paint_order_of(grandchild, &parents, &mut cache);
2688 let po_b = paint_order_of(child_b, &parents, &mut cache);
2689
2690 assert!(
2691 po_parent < po_a && po_a < po_gc && po_gc < po_b,
2692 "expected pre-order parent < child_a < grandchild < child_b, got {po_parent} {po_a} {po_gc} {po_b}"
2693 );
2694 }
2695
2696 #[test]
2699 fn z_index_reorders_sibling_paint_order() {
2700 use crate::components::ZIndex;
2701 let mut world = World::new();
2702 let parent = world.spawn_empty().id();
2703 let a = world.spawn(ChildOf(parent)).id();
2704 let b = world.spawn((ChildOf(parent), ZIndex(-1))).id();
2705 let c = world.spawn(ChildOf(parent)).id();
2706
2707 let (parents, mut cache) = build_parent_map(&mut world);
2708 let po_a = paint_order_of(a, &parents, &mut cache);
2709 let po_b = paint_order_of(b, &parents, &mut cache);
2710 let po_c = paint_order_of(c, &parents, &mut cache);
2711 assert!(
2712 po_b < po_a && po_a < po_c,
2713 "z:-1 sibling paints first; equal-z siblings keep document order (got {po_b} {po_a} {po_c})"
2714 );
2715
2716 world.entity_mut(a).insert(ZIndex(5));
2718 let (parents, mut cache) = build_parent_map(&mut world);
2719 let po_a = paint_order_of(a, &parents, &mut cache);
2720 let po_c = paint_order_of(c, &parents, &mut cache);
2721 assert!(po_a > po_c, "z:5 sibling paints above z:auto");
2722 }
2723
2724 #[test]
2731 fn partially_clipped_child_is_extracted_fully_outside_is_culled() {
2732 use crate::components::Style;
2733 use crate::input::Scroll;
2734 let mut main = World::new();
2735 let mut render = World::new();
2736 render.insert_resource(RenderEntityMap::default());
2737
2738 let container = main
2740 .spawn((
2741 Transform::new(Vec2::ZERO, Vec2::new(960.0, 504.0)),
2742 Style::default(),
2743 Scroll::default(),
2744 ))
2745 .id();
2746 let tile = main
2749 .spawn((
2750 Transform::new(Vec2::new(4.0, 8.0), Vec2::new(960.0, 80.0)),
2751 Visuals {
2752 fill: Some(Fill::Solid(Color::rgb(1.0, 0.0, 0.0))),
2753 radius: 0.0,
2754 corner_radii: None,
2755 shadows: Vec::new(),
2756 border: None,
2757 },
2758 ChildOf(container),
2759 ))
2760 .id();
2761 let straddler = main
2763 .spawn((
2764 Transform::new(Vec2::new(4.0, 480.0), Vec2::new(960.0, 80.0)),
2765 Visuals {
2766 fill: Some(Fill::Solid(Color::rgb(0.0, 1.0, 0.0))),
2767 radius: 0.0,
2768 corner_radii: None,
2769 shadows: Vec::new(),
2770 border: None,
2771 },
2772 ChildOf(container),
2773 ))
2774 .id();
2775 let outside = main
2777 .spawn((
2778 Transform::new(Vec2::new(0.0, 600.0), Vec2::new(100.0, 50.0)),
2779 Visuals {
2780 fill: Some(Fill::Solid(Color::rgb(0.0, 0.0, 1.0))),
2781 radius: 0.0,
2782 corner_radii: None,
2783 shadows: Vec::new(),
2784 border: None,
2785 },
2786 ChildOf(container),
2787 ))
2788 .id();
2789
2790 extract_rects(&mut main, &mut render);
2791
2792 let map = render.resource::<RenderEntityMap>();
2793 assert!(
2794 map.rect.contains_key(&tile),
2795 "width:100% + margin tile (4px overhang) must extract"
2796 );
2797 assert!(
2798 map.rect.contains_key(&straddler),
2799 "row straddling the clip bottom must extract"
2800 );
2801 assert!(
2802 !map.rect.contains_key(&outside),
2803 "zero-overlap child stays culled"
2804 );
2805 let re = map.rect[&tile];
2807 let rect = render.get::<ExtractedRect>(re).cloned().unwrap();
2808 assert_eq!(rect.size, Vec2::new(960.0, 80.0));
2809 assert_eq!(rect.origin, Vec2::new(4.0, 8.0));
2810 }
2811
2812 #[test]
2817 fn extract_borders_emits_expected_widths_and_order() {
2818 use crate::components::{Border, Edges, Style};
2819 let mut main = World::new();
2820 let mut render = World::new();
2821 render.insert_resource(RenderEntityMap::default());
2822
2823 let bordered = main
2824 .spawn((
2825 Transform::new(Vec2::new(5.0, 6.0), Vec2::new(50.0, 40.0)),
2826 Style::default(),
2827 Visuals {
2828 fill: None,
2829 radius: 8.0,
2830 corner_radii: None,
2831 shadows: Vec::new(),
2832 border: Some(Border {
2833 widths: Edges {
2834 top: 1.0,
2835 right: 2.0,
2836 bottom: 3.0,
2837 left: 4.0,
2838 ..Edges::default()
2839 },
2840 color: Color::rgb(1.0, 0.0, 0.0),
2841 side_colors: None,
2842 }),
2843 },
2844 ))
2845 .id();
2846 let _plain = main
2847 .spawn((
2848 Transform::new(Vec2::ZERO, Vec2::new(10.0, 10.0)),
2849 Visuals {
2850 fill: Some(Fill::Solid(Color::rgb(0.0, 1.0, 0.0))),
2851 radius: 0.0,
2852 corner_radii: None,
2853 shadows: Vec::new(),
2854 border: None,
2855 },
2856 ))
2857 .id();
2858
2859 extract_borders(&mut main, &mut render);
2860
2861 let borders: Vec<ExtractedBorder> = {
2862 let mut q = render.query::<&ExtractedBorder>();
2863 q.iter(&render).copied().collect()
2864 };
2865 assert_eq!(borders.len(), 1, "only the bordered entity extracts");
2866 let b = borders[0];
2867 assert_eq!(b.origin, Vec2::new(5.0, 6.0));
2868 assert_eq!(b.size, Vec2::new(50.0, 40.0));
2869 assert_eq!(b.widths, [1.0, 2.0, 3.0, 4.0]);
2870 assert_eq!(b.radius, 8.0);
2871
2872 main.get_mut::<Visuals>(bordered).unwrap().border = None;
2874 extract_borders(&mut main, &mut render);
2875 let count = {
2876 let mut q = render.query::<&ExtractedBorder>();
2877 q.iter(&render).count()
2878 };
2879 assert_eq!(count, 0);
2880 }
2881
2882 #[test]
2886 fn ancestor_opacity_multiplies_into_descendant_fill() {
2887 let mut main = World::new();
2888 let mut render = World::new();
2889 render.insert_resource(RenderEntityMap::default());
2890 let parent = main
2891 .spawn((
2892 Transform::new(Vec2::ZERO, Vec2::new(100.0, 100.0)),
2893 Opacity(0.5),
2894 ))
2895 .id();
2896 main.spawn((
2897 Transform::new(Vec2::ZERO, Vec2::new(50.0, 50.0)),
2898 Visuals {
2899 fill: Some(Fill::Solid(Color::rgba(1.0, 0.0, 0.0, 1.0))),
2900 radius: 0.0,
2901 corner_radii: None,
2902 shadows: Vec::new(),
2903 border: None,
2904 },
2905 Opacity(0.5),
2906 ChildOf(parent),
2907 ));
2908 extract_rects(&mut main, &mut render);
2909 let rects: Vec<ExtractedRect> = {
2910 let mut q = render.query::<&ExtractedRect>();
2911 q.iter(&render).cloned().collect()
2912 };
2913 assert_eq!(rects.len(), 1);
2914 let Brush::Solid(c) = &rects[0].brush else {
2915 panic!("solid brush expected");
2916 };
2917 assert!((c.a - 0.25).abs() < 1e-4, "expected 0.25, got {}", c.a);
2919 }
2920
2921 #[test]
2925 fn scrollbar_extracts_above_content_only_when_overflowing() {
2926 use crate::input::{Scroll, ScrollbarState};
2927 let mut main = World::new();
2928 main.insert_resource(crate::input::ScrollbarInteraction::default());
2929 let mut render = World::new();
2930 render.insert_resource(RenderEntityMap::default());
2931
2932 let scroller = main
2933 .spawn((
2934 Transform::new(Vec2::ZERO, Vec2::new(200.0, 400.0)),
2935 Scroll::vertical(),
2936 ScrollOffset::default(),
2937 ScrollbarState::default(),
2938 ))
2939 .id();
2940 let content = main
2942 .spawn((
2943 Transform::new(Vec2::ZERO, Vec2::new(200.0, 900.0)),
2944 ChildOf(scroller),
2945 ))
2946 .id();
2947 extract_scrollbars(&mut main, &mut render);
2948 let bars: Vec<ExtractedScrollbar> = {
2949 let mut q = render.query::<&ExtractedScrollbar>();
2950 q.iter(&render).cloned().collect()
2951 };
2952 assert_eq!(bars.len(), 1, "overflowing scroller gets a bar");
2953 assert!(!bars[0].draws.is_empty());
2954 let (parents, mut cache) = build_parent_map(&mut main);
2955 let content_order = paint_order_of(content, &parents, &mut cache);
2956 assert!(
2957 bars[0].order > content_order,
2958 "bar order {} must sit above content order {content_order}",
2959 bars[0].order
2960 );
2961
2962 main.get_mut::<Transform>(content).unwrap().size = Vec2::new(200.0, 300.0);
2964 extract_scrollbars(&mut main, &mut render);
2965 let count = {
2966 let mut q = render.query::<&ExtractedScrollbar>();
2967 q.iter(&render).count()
2968 };
2969 assert_eq!(count, 0, "as-needed visibility: no overflow, no bar");
2970 }
2971
2972 #[test]
2975 fn scrollbar_width_none_paints_nothing() {
2976 use crate::input::{Scroll, ScrollbarState, ScrollbarStyle, ScrollbarWidthMode};
2977 let mut main = World::new();
2978 main.insert_resource(crate::input::ScrollbarInteraction::default());
2979 let mut render = World::new();
2980 render.insert_resource(RenderEntityMap::default());
2981 let scroller = main
2982 .spawn((
2983 Transform::new(Vec2::ZERO, Vec2::new(200.0, 400.0)),
2984 Scroll::vertical(),
2985 ScrollOffset::default(),
2986 ScrollbarState::default(),
2987 ScrollbarStyle {
2988 width: ScrollbarWidthMode::None,
2989 ..Default::default()
2990 },
2991 ))
2992 .id();
2993 main.spawn((
2994 Transform::new(Vec2::ZERO, Vec2::new(200.0, 900.0)),
2995 ChildOf(scroller),
2996 ));
2997 extract_scrollbars(&mut main, &mut render);
2998 let count = {
2999 let mut q = render.query::<&ExtractedScrollbar>();
3000 q.iter(&render).count()
3001 };
3002 assert_eq!(count, 0);
3003 }
3004
3005 #[test]
3007 fn orphan_entities_sort_after_tree_content() {
3008 let mut world = World::new();
3009 let orphan = world.spawn_empty().id();
3010 let child = world.spawn_empty().id();
3011 let parent = world.spawn_empty().id();
3012 world.entity_mut(child).insert(ChildOf(parent));
3013
3014 let (parents, mut cache) = build_parent_map(&mut world);
3015 let po_orphan = paint_order_of(orphan, &parents, &mut cache);
3016 let po_child = paint_order_of(child, &parents, &mut cache);
3017 assert!(
3018 po_orphan > po_child,
3019 "orphan ({po_orphan}) must paint after tree content ({po_child})"
3020 );
3021 }
3022
3023 #[test]
3028 fn clip_ranges_bracket_exactly_descendants() {
3029 use crate::components::{Overflow, Style, Transform};
3030 let mut main = World::new();
3031 let mut render = World::new();
3032 render.insert_resource(RenderEntityMap::default());
3033
3034 let row1 = main.spawn(Transform::default()).id();
3036 let row2 = main.spawn(Transform::default()).id();
3037 let button = main.spawn(Transform::default()).id();
3038 let scrollbox = main
3039 .spawn((
3040 Style {
3041 overflow_y: Overflow::Hidden,
3042 ..Default::default()
3043 },
3044 Transform {
3045 absolute: Vec2::new(10.0, 10.0),
3046 size: Vec2::new(200.0, 100.0),
3047 baseline_y: None,
3048 },
3049 ))
3050 .id();
3051 let root = main.spawn(Transform::default()).id();
3052 main.entity_mut(scrollbox).insert(ChildOf(root));
3054 main.entity_mut(button).insert(ChildOf(root));
3055 main.entity_mut(row1).insert(ChildOf(scrollbox));
3056 main.entity_mut(row2).insert(ChildOf(scrollbox));
3057
3058 extract_clips(&mut main, &mut render);
3059
3060 let clip = {
3061 let mut q = render.query::<&ExtractedClipBox>();
3062 let boxes: Vec<ExtractedClipBox> = q.iter(&render).copied().collect();
3063 assert_eq!(boxes.len(), 1, "exactly one clip candidate");
3064 boxes[0]
3065 };
3066
3067 let (parents, mut cache) = build_parent_map(&mut main);
3068 let po_scrollbox = paint_order_of(scrollbox, &parents, &mut cache);
3069 let po_row1 = paint_order_of(row1, &parents, &mut cache);
3070 let po_row2 = paint_order_of(row2, &parents, &mut cache);
3071 let po_button = paint_order_of(button, &parents, &mut cache);
3072 let po_root = paint_order_of(root, &parents, &mut cache);
3073
3074 assert_eq!(clip.start_order, po_scrollbox);
3075 assert_eq!(clip.end_order, po_row1.max(po_row2));
3076 assert!(clip.start_order < po_row1 && po_row1 <= clip.end_order);
3078 assert!(clip.start_order < po_row2 && po_row2 <= clip.end_order);
3079 assert!(
3081 po_button > clip.end_order,
3082 "later sibling (order {po_button}) must not be swallowed by clip range [{}, {}]",
3083 clip.start_order,
3084 clip.end_order
3085 );
3086 assert!(po_root < clip.start_order);
3087 }
3088
3089 #[test]
3093 fn overlay_subtree_paints_after_all_normal_content() {
3094 let mut world = World::new();
3095 let root = world.spawn_empty().id();
3096 let dropdown_row = world.spawn(ChildOf(root)).id();
3098 let wrapper = world.spawn(ChildOf(dropdown_row)).id();
3099 let panel = world.spawn((ChildOf(wrapper), OverlayLayer)).id();
3100 let opt1 = world.spawn(ChildOf(panel)).id();
3101 let opt2 = world.spawn(ChildOf(panel)).id();
3102 let textarea_row = world.spawn(ChildOf(root)).id();
3103 let textarea = world.spawn(ChildOf(textarea_row)).id();
3104
3105 let (parents, mut cache) = build_parent_map(&mut world);
3106 let po_panel = paint_order_of(panel, &parents, &mut cache);
3107 let po_opt1 = paint_order_of(opt1, &parents, &mut cache);
3108 let po_opt2 = paint_order_of(opt2, &parents, &mut cache);
3109 for normal in [root, dropdown_row, wrapper, textarea_row, textarea] {
3110 let o = paint_order_of(normal, &parents, &mut cache);
3111 assert!(
3112 o < OVERLAY_ORDER_BASE,
3113 "normal content stays below the overlay band, got {o:#x}"
3114 );
3115 assert!(
3116 po_panel > o,
3117 "panel ({po_panel:#x}) must paint after normal content ({o:#x})"
3118 );
3119 }
3120 assert_eq!(po_opt1, po_panel + 2, "panel then first option");
3122 assert_eq!(po_opt2, po_opt1 + 2, "options keep sibling order");
3123 assert!((OVERLAY_ORDER_BASE..0x8000_0000).contains(&po_panel));
3125 }
3126
3127 #[test]
3130 fn overlays_stack_by_open_order() {
3131 let mut world = World::new();
3132 let root = world.spawn_empty().id();
3133 let a = world.spawn((ChildOf(root), OverlayLayer)).id();
3135 let _a_kid = world.spawn(ChildOf(a)).id();
3136 let b = world
3137 .spawn((ChildOf(root), OverlayLayer, Visible(false)))
3138 .id();
3139 let _b_kid = world.spawn(ChildOf(b)).id();
3140
3141 let (parents, mut cache) = build_parent_map(&mut world);
3143 let po_a_t1 = paint_order_of(a, &parents, &mut cache);
3144 assert!(po_a_t1 >= OVERLAY_ORDER_BASE);
3145
3146 world.entity_mut(b).insert(Visible(true));
3148 let (parents, mut cache) = build_parent_map(&mut world);
3149 let po_a = paint_order_of(a, &parents, &mut cache);
3150 let po_b = paint_order_of(b, &parents, &mut cache);
3151 assert!(
3152 po_b > po_a,
3153 "later-opened popup B ({po_b:#x}) must paint over A ({po_a:#x})"
3154 );
3155
3156 world.entity_mut(a).insert(Visible(false));
3158 let _ = build_parent_map(&mut world);
3159 world.entity_mut(a).insert(Visible(true));
3160 let (parents, mut cache) = build_parent_map(&mut world);
3161 let po_a = paint_order_of(a, &parents, &mut cache);
3162 let po_b = paint_order_of(b, &parents, &mut cache);
3163 assert!(
3164 po_a > po_b,
3165 "re-opened popup A ({po_a:#x}) must now paint over B ({po_b:#x})"
3166 );
3167 }
3168
3169 #[test]
3172 fn clip_ranges_exclude_overlay_subtrees() {
3173 use crate::components::{Overflow, Style, Transform};
3174 let mut main = World::new();
3175 let mut render = World::new();
3176 render.insert_resource(RenderEntityMap::default());
3177
3178 let root = main.spawn(Transform::default()).id();
3179 let scrollbox = main
3180 .spawn((
3181 Style {
3182 overflow_y: Overflow::Hidden,
3183 ..Default::default()
3184 },
3185 Transform {
3186 absolute: Vec2::new(0.0, 0.0),
3187 size: Vec2::new(200.0, 100.0),
3188 baseline_y: None,
3189 },
3190 ChildOf(root),
3191 ))
3192 .id();
3193 let row = main.spawn((Transform::default(), ChildOf(scrollbox))).id();
3194 let panel = main
3195 .spawn((Transform::default(), ChildOf(scrollbox), OverlayLayer))
3196 .id();
3197 let opt = main.spawn((Transform::default(), ChildOf(panel))).id();
3198
3199 extract_clips(&mut main, &mut render);
3200 let clip = {
3201 let mut q = render.query::<&ExtractedClipBox>();
3202 let boxes: Vec<ExtractedClipBox> = q.iter(&render).copied().collect();
3203 assert_eq!(boxes.len(), 1, "exactly one clip candidate");
3204 boxes[0]
3205 };
3206
3207 let (parents, mut cache) = build_parent_map(&mut main);
3208 let po_row = paint_order_of(row, &parents, &mut cache);
3209 let po_panel = paint_order_of(panel, &parents, &mut cache);
3210 let po_opt = paint_order_of(opt, &parents, &mut cache);
3211 assert_eq!(
3212 clip.end_order, po_row,
3213 "bracket ends at the last NORMAL descendant"
3214 );
3215 assert!(
3216 po_panel > clip.end_order && po_opt > clip.end_order,
3217 "overlay subtree ({po_panel:#x}, {po_opt:#x}) must fall outside the clip bracket [{:#x}, {:#x}]",
3218 clip.start_order,
3219 clip.end_order
3220 );
3221 }
3222
3223 #[test]
3226 fn overlay_internal_clip_brackets_intact() {
3227 use crate::components::{Overflow, Style, Transform};
3228 let mut main = World::new();
3229 let mut render = World::new();
3230 render.insert_resource(RenderEntityMap::default());
3231
3232 let root = main.spawn(Transform::default()).id();
3233 let panel = main
3234 .spawn((
3235 Style {
3236 overflow_y: Overflow::Hidden,
3237 ..Default::default()
3238 },
3239 Transform {
3240 absolute: Vec2::new(0.0, 0.0),
3241 size: Vec2::new(200.0, 100.0),
3242 baseline_y: None,
3243 },
3244 ChildOf(root),
3245 OverlayLayer,
3246 ))
3247 .id();
3248 let opt1 = main.spawn((Transform::default(), ChildOf(panel))).id();
3249 let opt2 = main.spawn((Transform::default(), ChildOf(panel))).id();
3250
3251 extract_clips(&mut main, &mut render);
3252 let clip = {
3253 let mut q = render.query::<&ExtractedClipBox>();
3254 let boxes: Vec<ExtractedClipBox> = q.iter(&render).copied().collect();
3255 assert_eq!(boxes.len(), 1);
3256 boxes[0]
3257 };
3258
3259 let (parents, mut cache) = build_parent_map(&mut main);
3260 let po_panel = paint_order_of(panel, &parents, &mut cache);
3261 let po_opt1 = paint_order_of(opt1, &parents, &mut cache);
3262 let po_opt2 = paint_order_of(opt2, &parents, &mut cache);
3263 assert_eq!(clip.start_order, po_panel);
3264 assert_eq!(clip.end_order, po_opt1.max(po_opt2));
3265 assert!(
3266 clip.start_order >= OVERLAY_ORDER_BASE,
3267 "bracket lives in the overlay band"
3268 );
3269 assert!(clip.start_order < po_opt1 && po_opt1 <= clip.end_order);
3270 assert!(clip.start_order < po_opt2 && po_opt2 <= clip.end_order);
3271 }
3272
3273 #[test]
3276 fn popup_content_escapes_ancestor_clip_rect() {
3277 use crate::components::{Overflow, Style, Transform};
3278 let mut main = World::new();
3279
3280 let root = main.spawn(Transform::default()).id();
3281 let scrollbox = main
3282 .spawn((
3283 Style {
3284 overflow_y: Overflow::Hidden,
3285 ..Default::default()
3286 },
3287 Transform {
3288 absolute: Vec2::new(0.0, 0.0),
3289 size: Vec2::new(200.0, 50.0),
3290 baseline_y: None,
3291 },
3292 ChildOf(root),
3293 ))
3294 .id();
3295 let row = main.spawn((Transform::default(), ChildOf(scrollbox))).id();
3296 let panel = main
3297 .spawn((Transform::default(), ChildOf(scrollbox), OverlayLayer))
3298 .id();
3299 let opt = main.spawn((Transform::default(), ChildOf(panel))).id();
3300
3301 let (parents, _) = build_parent_map(&mut main);
3302 let clips = parent_scroll_clip_rects(&mut main, &parents);
3303 assert!(
3304 clips.contains_key(&row),
3305 "normal child keeps its scroll-ancestor clip"
3306 );
3307 assert!(
3308 !clips.contains_key(&panel) && !clips.contains_key(&opt),
3309 "overlay subtree escapes the ancestor clip rect"
3310 );
3311 }
3312
3313 #[test]
3318 fn line_height_px_falls_back_to_default_multiplier() {
3319 use crate::components::{
3320 DEFAULT_LINE_HEIGHT_MULTIPLIER, TextContent, TextStyle, Transform,
3321 };
3322
3323 let mut main = World::new();
3324 let mut render = World::new();
3325 render.init_resource::<RenderEntityMap>();
3326 main.spawn((
3327 Transform::new(Vec2::ZERO, Vec2::new(120.0, 24.0)),
3328 TextContent("hi".to_string()),
3329 TextStyle {
3330 size_px: 20.0,
3331 ..Default::default()
3332 },
3333 ));
3334 extract_text(&mut main, &mut render);
3335 let mut q = render.query::<&ExtractedText>();
3336 let et = q.iter(&render).next().unwrap();
3337 assert_eq!(et.line_height_px, 20.0 * DEFAULT_LINE_HEIGHT_MULTIPLIER);
3338 }
3339
3340 #[test]
3343 fn line_height_px_honours_css_multiplier_override() {
3344 use crate::components::{LineHeightSpec, TextContent, TextStyle, Transform};
3345
3346 let mut main = World::new();
3347 let mut render = World::new();
3348 render.init_resource::<RenderEntityMap>();
3349 main.spawn((
3350 Transform::new(Vec2::ZERO, Vec2::new(120.0, 24.0)),
3351 TextContent("hi".to_string()),
3352 TextStyle {
3353 size_px: 20.0,
3354 line_height: Some(LineHeightSpec::Multiplier(1.5)),
3355 ..Default::default()
3356 },
3357 ));
3358 extract_text(&mut main, &mut render);
3359 let mut q = render.query::<&ExtractedText>();
3360 let et = q.iter(&render).next().unwrap();
3361 assert_eq!(et.line_height_px, 30.0);
3362 }
3363
3364 #[test]
3367 fn line_height_px_honours_css_absolute_override() {
3368 use crate::components::{LineHeightSpec, TextContent, TextStyle, Transform};
3369
3370 let mut main = World::new();
3371 let mut render = World::new();
3372 render.init_resource::<RenderEntityMap>();
3373 main.spawn((
3374 Transform::new(Vec2::ZERO, Vec2::new(120.0, 24.0)),
3375 TextContent("hi".to_string()),
3376 TextStyle {
3377 size_px: 20.0,
3378 line_height: Some(LineHeightSpec::Px(19.0)),
3379 ..Default::default()
3380 },
3381 ));
3382 extract_text(&mut main, &mut render);
3383 let mut q = render.query::<&ExtractedText>();
3384 let et = q.iter(&render).next().unwrap();
3385 assert_eq!(et.line_height_px, 19.0);
3386 }
3387
3388 #[test]
3393 fn caret_width_px_falls_back_then_honours_override() {
3394 use crate::components::{CaretWidth, TextContent, Transform};
3395
3396 let mut main = World::new();
3397 let mut render = World::new();
3398 render.init_resource::<RenderEntityMap>();
3399 let plain = main
3400 .spawn((
3401 Transform::new(Vec2::ZERO, Vec2::new(120.0, 24.0)),
3402 TextContent("a".to_string()),
3403 ))
3404 .id();
3405 let overridden = main
3406 .spawn((
3407 Transform::new(Vec2::new(0.0, 40.0), Vec2::new(120.0, 24.0)),
3408 TextContent("b".to_string()),
3409 CaretWidth(4.0),
3410 ))
3411 .id();
3412 extract_text(&mut main, &mut render);
3413 let map = render.resource::<RenderEntityMap>().text.clone();
3414 let plain_et = render.get::<ExtractedText>(map[&plain]).unwrap();
3415 let overridden_et = render.get::<ExtractedText>(map[&overridden]).unwrap();
3416 assert_eq!(plain_et.caret_width_px, CARET_WIDTH_PX);
3417 assert_eq!(overridden_et.caret_width_px, 4.0);
3418 }
3419}