diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..3eb6e5e --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "flax"] + path = flax + url = https://github.com/ten3roberts/flax diff --git a/Cargo.lock b/Cargo.lock index 6764b7d..3530d99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -630,7 +630,6 @@ dependencies = [ [[package]] name = "flax" version = "0.6.2" -source = "git+https://github.com/ten3roberts/flax#bb23e6eaf91195371b96d8725cfef42efdc4b19c" dependencies = [ "anyhow", "atomic_refcell", @@ -648,7 +647,6 @@ dependencies = [ [[package]] name = "flax-derive" version = "0.6.0" -source = "git+https://github.com/ten3roberts/flax#bb23e6eaf91195371b96d8725cfef42efdc4b19c" dependencies = [ "itertools 0.11.0", "proc-macro-crate", @@ -2500,6 +2498,7 @@ dependencies = [ "tracing-tree", "violet-core", "violet-wgpu", + "web-time 1.0.0", ] [[package]] @@ -2550,6 +2549,7 @@ dependencies = [ "futures", "glam", "itertools 0.12.1", + "puffin", "tracing-subscriber", "tracing-tree", "tracing-web", diff --git a/Cargo.toml b/Cargo.toml index f867acc..579c4bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ "violet-wgpu", "violet-core", "violet-demo" ] +exclude = ["flax"] [workspace.package] version = "0.0.1" @@ -23,7 +24,7 @@ documentation = "https://docs.rs/violet" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [workspace.dependencies] -flax = { git = "https://github.com/ten3roberts/flax", version = "0.6.0", features = [ +flax = { path = "./flax", version = "0.6.0", features = [ "derive", "puffin", ] } @@ -82,6 +83,7 @@ palette.workspace = true futures-signals.workspace = true flax.workspace = true lru.workspace = true +web-time.workspace = true [dev-dependencies] anyhow.workspace = true diff --git a/examples/basic.rs b/examples/basic.rs index f7d3ba1..36059a5 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -24,7 +24,7 @@ use violet_core::{ danger_item, primary_background, secondary_background, spacing_medium, spacing_small, Background, SizeExt, ValueOrRef, }, - widget::{BoxSized, ContainerStyle}, + widget::ContainerStyle, }; struct MainApp; @@ -50,7 +50,7 @@ impl Widget for MainApp { .map(|i| { let size = Vec2::splat(128.0 / i as f32); Stack::new( - BoxSized::new(Image::new("./assets/images/statue.jpg")) + Image::new("./assets/images/statue.jpg") .with_min_size(Unit::px(size)) .with_aspect_ratio(1.0), ) @@ -100,10 +100,10 @@ impl Widget for MainApp { .with_padding(spacing_small()) .with_background(Background::new(primary_background())), Stack::new(( - BoxSized::new(Rectangle::new(danger_item())) + Rectangle::new(danger_item()) .with_min_size(Unit::px(vec2(100.0, 30.0))) .with_size(Unit::px(vec2(50.0, 30.0))), - BoxSized::new(Rectangle::new(danger_item())) + Rectangle::new(danger_item()) .with_min_size(Unit::px(vec2(200.0, 10.0))) .with_size(Unit::px(vec2(50.0, 10.0))), Text::new("This is some text").with_font_size(16.0), @@ -170,14 +170,14 @@ impl Widget for LayoutFlexTest { List::new( (0..8) .map(|i| { - let size = vec2(100.0, 20.0); + let size = vec2(50.0, 20.0); Stack::new( - BoxSized::new(Rectangle::new(ValueOrRef::value( + Rectangle::new(ValueOrRef::value( Hsva::new(i as f32 * 30.0, 1.0, 1.0, 1.0).into_color(), - ))) + )) .with_min_size(Unit::px(size)) - .with_size(Unit::px(size * vec2(i as f32, 1.0))), + .with_maximize(Vec2::X * i as f32), ) .with_margin(spacing_small()) }) diff --git a/examples/flow.rs b/examples/flow.rs index 761b510..b3555b8 100644 --- a/examples/flow.rs +++ b/examples/flow.rs @@ -18,28 +18,13 @@ use violet_core::{ style::{ self, colors::{EERIE_BLACK_600, EERIE_BLACK_DEFAULT}, - secondary_background, spacing_small, Background, SizeExt, + spacing_small, Background, SizeExt, }, text::Wrap, - widget::{ - card, column, row, BoxSized, Button, ButtonStyle, ContainerStyle, SliderWithLabel, - TextInput, - }, + widget::{card, column, label, row, Button, ButtonStyle, SliderWithLabel, TextInput}, }; use violet_wgpu::renderer::RendererConfig; -fn label(text: impl Into) -> Stack { - Stack::new(Text::new(text.into())) - .with_padding(spacing_small()) - .with_margin(spacing_small()) -} - -fn pill(widget: impl Widget) -> impl Widget { - Stack::new(widget).with_style(ContainerStyle { - background: Some(Background::new(secondary_background())), - }) -} - pub fn main() -> anyhow::Result<()> { registry() .with( @@ -94,8 +79,7 @@ impl Widget for MainApp { )) .with_stretch(true), ), - BoxSized::new(Rectangle::new(EERIE_BLACK_600)) - .with_size(Unit::rel2(1.0, 0.0) + Unit::px2(0.0, 1.0)), + Rectangle::new(EERIE_BLACK_600).with_size(Unit::rel2(1.0, 0.0) + Unit::px2(0.0, 1.0)), card(column(( column(( row(( @@ -147,7 +131,7 @@ impl Widget for Tints { ); card(column(( - BoxSized::new(Rectangle::new(color)).with_size(Unit::px2(100.0, 40.0)), + Rectangle::new(color).with_size(Unit::px2(100.0, 40.0)), label(format!("{tint}")), label(color_string), ))) diff --git a/examples/row.rs b/examples/row.rs index 3de8791..5509c44 100644 --- a/examples/row.rs +++ b/examples/row.rs @@ -1,6 +1,8 @@ use std::iter::repeat; +use glam::{vec2, Vec2}; use itertools::Itertools; +use palette::named::DARKCYAN; use tracing_subscriber::{layer::SubscriberExt, registry, util::SubscriberInitExt, EnvFilter}; use tracing_tree::HierarchicalLayer; @@ -15,10 +17,13 @@ use violet::core::{ }; use violet_core::{ style::{ - colors::{JADE_400, JADE_DEFAULT, LION_DEFAULT, REDWOOD_100, ULTRA_VIOLET_DEFAULT}, + colors::{ + DARK_CYAN_DEFAULT, JADE_400, JADE_DEFAULT, LION_DEFAULT, REDWOOD_100, + ULTRA_VIOLET_DEFAULT, + }, spacing_medium, spacing_small, SizeExt, }, - widget::{card, column, label, row, Stack}, + widget::{card, centered, column, label, row, Image, Stack}, }; use violet_wgpu::renderer::RendererConfig; @@ -66,7 +71,17 @@ impl Widget for MainApp { // .with_min_size(Unit::px2(100.0, 100.0)) // .with_size(Unit::px2(0.0, 100.0) + Unit::rel2(1.0, 0.0)), // .with_margin(spacing_medium()), - row((0..16).map(|_| Stack::new(Item)).collect_vec()), + row((0..4) + .map(|_| Box::new(Stack::new(Item)) as Box) + .chain([Box::new( + centered((Rectangle::new(JADE_DEFAULT) + .with_maximize(vec2(1.0, 0.0)) + .with_size(Unit::px2(0.0, 50.0)) + .with_max_size(Unit::px2(1000.0, 100.0)),)) + .with_maximize(Vec2::ONE), + ) as Box]) + .collect_vec()) + .with_padding(spacing_small()), )) // .with_padding(spacing_medium()) .contain_margins(true), @@ -81,8 +96,9 @@ struct Item; impl Widget for Item { fn mount(self, scope: &mut Scope<'_>) { - Rectangle::new(ULTRA_VIOLET_DEFAULT) + Image::new("./assets/images/statue.jpg") .with_size(Unit::px2(100.0, 100.0)) + // .with_aspect_ratio(1.0) .with_margin(spacing_small()) .mount(scope) } diff --git a/examples/sizing.rs b/examples/sizing.rs index 5e95038..9029756 100644 --- a/examples/sizing.rs +++ b/examples/sizing.rs @@ -23,19 +23,12 @@ use violet_core::{ state::MapRef, style::{colors::DARK_CYAN_DEFAULT, SizeExt}, text::Wrap, - widget::{card, centered, column, row, Slider}, + widget::{card, centered, column, label, row, Slider}, }; use violet_wgpu::renderer::RendererConfig; const MARGIN_SM: Edges = Edges::even(4.0); -fn label(text: impl Into) -> Stack { - Stack::new(Text::new(text.into())) - .with_padding(MARGIN_SM) - .with_margin(MARGIN_SM) - .with_background(Background::new(EERIE_BLACK_400)) -} - pub fn main() -> anyhow::Result<()> { registry() .with( diff --git a/flax b/flax new file mode 160000 index 0000000..77d280e --- /dev/null +++ b/flax @@ -0,0 +1 @@ +Subproject commit 77d280efca0eb8b860df2037d01abe70d413e90f diff --git a/recipes.json b/recipes.json index 71f4881..d8affb8 100644 --- a/recipes.json +++ b/recipes.json @@ -1,6 +1,6 @@ { "check": { - "cmd": "cargo check --all-targets --all-features" + "cmd": "cargo check --all-targets --all-features --workspace" }, "run demo": { "cmd": "cargo run --package violet-demo" @@ -56,10 +56,10 @@ }, "build web": { "cmd": "wasm-pack build --target web", - "cwd": "./violet-web-example/" + "cwd": "./violet-demo/" }, "host": { "cmd": "python3 -m http.server 8080", - "cwd": "./violet-web-example/" + "cwd": "./violet-demo/" } } diff --git a/src/lib.rs b/src/lib.rs index fb896e7..037eb4d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,3 +5,4 @@ pub use flax; pub use futures_signals; pub use glam; pub use palette; +pub use web_time; diff --git a/violet-core/src/components.rs b/violet-core/src/components.rs index 147dbf0..1a6c5a0 100644 --- a/violet-core/src/components.rs +++ b/violet-core/src/components.rs @@ -66,6 +66,8 @@ component! { /// A margin is in essence a minimum allowed distance to another items bounds pub margin: Edges => [ Debuggable ], + pub maximize: Vec2 => [ Debuggable ], + pub text: Vec => [ ], pub text_wrap: Wrap => [ Debuggable ], pub font_size: f32 => [ Debuggable ], diff --git a/violet-core/src/effect/mod.rs b/violet-core/src/effect/mod.rs index c858e0e..5ac02a5 100644 --- a/violet-core/src/effect/mod.rs +++ b/violet-core/src/effect/mod.rs @@ -7,6 +7,7 @@ use std::{ }; pub use future::FutureEffect; +use pin_project::pin_project; pub use stream::StreamEffect; /// An asynchronous computation which has access to `Data` when polled @@ -16,4 +17,42 @@ pub use stream::StreamEffect; pub trait Effect { /// Polls the effect fn poll(self: Pin<&mut Self>, context: &mut Context<'_>, data: &mut Data) -> Poll<()>; + fn label(&self) -> Option<&str> { + None + } + + fn with_label(self, label: impl Into) -> EffectWithLabel + where + Self: Sized, + { + EffectWithLabel::new(self, Some(label.into())) + } +} + +#[pin_project] +#[doc(hidden)] +pub struct EffectWithLabel { + #[pin] + effect: E, + label: Option, +} + +impl EffectWithLabel { + pub fn new(effect: E, label: Option) -> Self { + Self { effect, label } + } +} + +impl Effect for EffectWithLabel +where + E: Effect, +{ + #[inline] + fn poll(self: Pin<&mut Self>, context: &mut Context<'_>, data: &mut Data) -> Poll<()> { + self.project().effect.poll(context, data) + } + + fn label(&self) -> Option<&str> { + self.label.as_deref() + } } diff --git a/violet-core/src/executor.rs b/violet-core/src/executor.rs index 3db4c46..4238ffc 100644 --- a/violet-core/src/executor.rs +++ b/violet-core/src/executor.rs @@ -174,8 +174,10 @@ impl Executor { } pub fn tick(&mut self, data: &mut Data) { + puffin::profile_function!(); assert!(self.processing.is_empty()); loop { + puffin::profile_scope!("tick"); // Add new tasks self.processing .extend(self.incoming.borrow_mut().drain(..).map(|task| { @@ -201,6 +203,8 @@ impl Executor { // external waker continue; }; + + puffin::profile_scope!("process task", task.effect.label().unwrap_or_default()); let mut context = Context::from_waker(&*waker); tracing::trace!(?id, "Polling task"); diff --git a/violet-core/src/input.rs b/violet-core/src/input.rs index c205731..611c28b 100644 --- a/violet-core/src/input.rs +++ b/violet-core/src/input.rs @@ -1,8 +1,6 @@ -use std::str::FromStr; - use flax::{ component, components::child_of, entity_ids, fetch::Satisfied, filter::All, Component, Entity, - EntityIds, EntityRef, Fetch, FetchExt, Mutable, Query, Topo, World, + EntityIds, EntityRef, Fetch, FetchExt, Query, Topo, World, }; use glam::Vec2; diff --git a/violet-core/src/layout/flow.rs b/violet-core/src/layout/flow.rs index efeec59..553526a 100644 --- a/violet-core/src/layout/flow.rs +++ b/violet-core/src/layout/flow.rs @@ -165,6 +165,7 @@ pub(crate) struct Row { pub(crate) blocks: Arc>, pub(crate) margin: Edges, pub(crate) hints: SizingHints, + maximize_sum: Vec2, } #[derive(Default, Debug, Clone)] @@ -225,25 +226,9 @@ impl FlowLayout { // If everything was squished as much as possible let minimum_inner_size = row.min.size().dot(axis); - // if minimum_inner_size > limits.max_size.dot(axis) { - // tracing::error!( - // ?minimum_inner_size, - // ?limits.max_size, - // "minimum inner size exceeded max size", - // ); - // } - // If everything could take as much space as it wants let preferred_inner_size = row.preferred.size().dot(axis); - // if minimum_inner_size > preferred_inner_size { - // tracing::error!( - // ?minimum_inner_size, - // ?preferred_inner_size, - // "minimum inner size exceeded preferred size", - // ); - // } - // How much space there is left to distribute to the children let distribute_size = (preferred_inner_size - minimum_inner_size).max(0.0); // tracing::info!(?distribute_size); @@ -253,14 +238,9 @@ impl FlowLayout { .min(limits.max_size.dot(axis) - minimum_inner_size) .max(0.0); - // tracing::info!( - // ?row.preferred, - // distribute_size, - // target_inner_size, - // blocks = row.blocks.len(), - // "query size" - // ); + let remaining_size = (limits.max_size.dot(axis) - preferred_inner_size).max(0.0); + // for cross let available_size = limits.max_size; let mut cursor = @@ -300,10 +280,17 @@ impl FlowLayout { remaining / distribute_size }; - let given_size = block_min_size + target_inner_size * ratio; + let mut given_size = block_min_size + target_inner_size * ratio; sum += ratio; + let maximize = sizing.maximize.dot(axis); + + if maximize > 0.0 { + given_size += remaining_size * (maximize / row.maximize_sum.dot(axis)); + } + let axis_sizing = given_size * axis; + // tracing::info!(ratio, %axis_sizing, block_min_size, target_inner_size); assert!( @@ -342,23 +329,6 @@ impl FlowLayout { can_grow |= block.can_grow; tracing::debug!(?block, "updated subtree"); - // block.rect = block - // .rect - // .clamp_size(child_limits.min_size, child_limits.max_size); - - // if block.rect.size().x > child_limits.max_size.x - // || block.rect.size().y > child_limits.max_size.y - // { - // tracing::error!( - // block_min_size, - // block_preferred_size, - // "child {} exceeded max size: {:?} > {:?}", - // entity, - // block.rect.size(), - // child_limits.max_size, - // ); - // } - cursor.put(&block); (entity, block) @@ -425,25 +395,9 @@ impl FlowLayout { // If everything was squished as much as possible let minimum_inner_size = row.min.size().dot(axis); - // if minimum_inner_size > limits.max_size.dot(axis) { - // tracing::error!( - // ?minimum_inner_size, - // ?limits.max_size, - // "minimum inner size exceeded max size", - // ); - // } - // If everything could take as much space as it wants let preferred_inner_size = row.preferred.size().dot(axis); - // if minimum_inner_size > preferred_inner_size { - // tracing::error!( - // ?minimum_inner_size, - // ?preferred_inner_size, - // "minimum inner size exceeded preferred size", - // ); - // } - // How much space there is left to distribute out let distribute_size = (preferred_inner_size - minimum_inner_size).max(0.0); // tracing::info!(?distribute_size); @@ -453,6 +407,8 @@ impl FlowLayout { .min(args.limits.max_size.dot(axis) - minimum_inner_size) .max(0.0); + let remaining_size = args.limits.max_size.dot(axis) - preferred_inner_size; + tracing::debug!( min=?row.min.size(), preferre2=?row.preferred.size(), @@ -505,9 +461,16 @@ impl FlowLayout { remaining / distribute_size }; - let given_size = block_min_size + target_inner_size * ratio; + let mut given_size = block_min_size + target_inner_size * ratio; sum += ratio; + let maximize = sizing.maximize.dot(axis); + + if maximize > 0.0 { + given_size = + given_size.max(remaining_size * (maximize / row.maximize_sum).dot(axis)); + } + let axis_sizing = given_size * axis; // tracing::info!(ratio, %axis_sizing, block_min_size, target_inner_size); @@ -551,6 +514,7 @@ impl FlowLayout { direction: args.direction, }); + hints = hints.combine(sizing.hints); tracing::debug!(min=%sizing.min.size(), preferred=%sizing.preferred.size(), ?child_limits, "query"); @@ -573,6 +537,7 @@ impl FlowLayout { preferred: rect.max_size(args.limits.min_size), margin, hints, + maximize: row.maximize_sum, } } @@ -586,7 +551,7 @@ impl FlowLayout { puffin::profile_function!(); if let Some(value) = cache.query_row.as_ref() { if validate_cached_row(value, args.limits, args.content_area) { - return value.value.clone(); + // return value.value.clone(); } } @@ -606,6 +571,8 @@ impl FlowLayout { let mut hints = SizingHints::default(); + let mut maximize = Vec2::ZERO; + let blocks = children .iter() .map(|&child| { @@ -642,6 +609,7 @@ impl FlowLayout { }, ); + maximize += sizing.maximize; hints = hints.combine(sizing.hints); min_cursor.put(&Block::new( @@ -678,6 +646,7 @@ impl FlowLayout { blocks: Arc::new(blocks), hints, margin, + maximize_sum: maximize, }; cache.insert_query_row(CachedValue::new( @@ -775,6 +744,7 @@ impl FlowLayout { can_grow: can_grow | row.hints.can_grow, ..row.hints }, + maximize: row.maximize_sum, } } } diff --git a/violet-core/src/layout/mod.rs b/violet-core/src/layout/mod.rs index ba6e5dd..0b07e8a 100644 --- a/violet-core/src/layout/mod.rs +++ b/violet-core/src/layout/mod.rs @@ -9,8 +9,8 @@ use glam::{vec2, BVec2, Vec2}; use crate::{ components::{ - self, anchor, aspect_ratio, children, layout, max_size, min_size, offset, padding, size, - size_resolver, + self, anchor, aspect_ratio, children, layout, max_size, maximize, min_size, offset, + padding, size, size_resolver, }, layout::cache::{validate_cached_layout, validate_cached_query, CachedValue, LAYOUT_TOLERANCE}, Edges, Rect, @@ -137,6 +137,7 @@ pub struct Sizing { preferred: Rect, margin: Edges, pub hints: SizingHints, + maximize: Vec2, } impl Display for Sizing { @@ -283,7 +284,7 @@ fn validate_block(entity: &EntityRef, block: &Block, limits: LayoutLimits) { } } -pub(crate) fn query_size(world: &World, entity: &EntityRef, mut args: QueryArgs) -> Sizing { +pub(crate) fn query_size(world: &World, entity: &EntityRef, args: QueryArgs) -> Sizing { puffin::profile_function!(format!("{entity} {args:?}")); // assert!(limits.min_size.x <= limits.max_size.x); // assert!(limits.min_size.y <= limits.max_size.y); @@ -312,21 +313,19 @@ pub(crate) fn query_size(world: &World, entity: &EntityRef, mut args: QueryArgs) min_size.is_relative() | max_size.map(|v| v.is_relative()).unwrap_or(BVec2::FALSE); let min_size = min_size.resolve(args.content_area); - let max_size = max_size.map(|v| v.resolve(args.content_area)); - args.limits.min_size = args.limits.min_size.max(min_size); - - let external_max_size = args.limits.max_size; - - // Minimum size is *always* respected, even if that entails overflowing - args.limits.max_size = args.limits.max_size.max(args.limits.min_size); - - if let Some(max_size) = max_size { - args.limits.max_size = args.limits.max_size.min(max_size); - } + let max_size = max_size + .map(|v| v.resolve(args.content_area)) + .unwrap_or(Vec2::INFINITY); + + let mut limits = LayoutLimits { + // Minimum size is *always* respected, even if that entails overflowing + min_size: args.limits.min_size.max(min_size), + max_size: args.limits.max_size.min(max_size), + }; // Check if cache is valid if let Some(cache) = cache.get_query(args.direction) { - if validate_cached_query(cache, args.limits, args.content_area) { + if validate_cached_query(cache, limits, args.content_area) { return cache.value; } } @@ -339,12 +338,14 @@ pub(crate) fn query_size(world: &World, entity: &EntityRef, mut args: QueryArgs) let children = children.map(Vec::as_slice).unwrap_or(&[]); let resolved_size = size.resolve(args.content_area); - let hints = SizingHints { + + let maximized = entity.get_copy(maximize()).unwrap_or_default(); + let mut hints = SizingHints { relative_size: fixed_boundary_size | size.is_relative(), can_grow: BVec2::new( - resolved_size.x > external_max_size.x, - resolved_size.y > external_max_size.y, - ), + resolved_size.x > args.limits.max_size.x, + resolved_size.y > args.limits.max_size.y, + ) | maximized.cmpgt(Vec2::ZERO), coupled_size: false, }; @@ -353,7 +354,7 @@ pub(crate) fn query_size(world: &World, entity: &EntityRef, mut args: QueryArgs) // } // Clamp max size here since we ensure it is > min_size - let resolved_size = resolved_size.clamp(args.limits.min_size, args.limits.max_size); + let resolved_size = resolved_size.clamp(limits.min_size, limits.max_size); // Flow let mut sizing = if let Some(layout) = layout { @@ -363,8 +364,8 @@ pub(crate) fn query_size(world: &World, entity: &EntityRef, mut args: QueryArgs) children, QueryArgs { limits: LayoutLimits { - min_size: (args.limits.min_size - padding.size()).max(Vec2::ZERO), - max_size: (args.limits.max_size - padding.size()).max(Vec2::ZERO), + min_size: (limits.min_size - padding.size()).max(Vec2::ZERO), + max_size: (limits.max_size - padding.size()).max(Vec2::ZERO), }, content_area: args.content_area - padding.size(), ..args @@ -377,6 +378,7 @@ pub(crate) fn query_size(world: &World, entity: &EntityRef, mut args: QueryArgs) min: sizing.min.pad(&padding), preferred: sizing.preferred.pad(&padding), hints: sizing.hints.combine(hints), + maximize: sizing.maximize + entity.get_copy(maximize()).unwrap_or_default(), } } else if let [child] = children { let child = world.entity(*child).unwrap(); @@ -387,22 +389,27 @@ pub(crate) fn query_size(world: &World, entity: &EntityRef, mut args: QueryArgs) .unwrap_or((Vec2::ZERO, Vec2::ZERO, SizingHints::default())); // If intrinsic_min_size > max_size we overflow, but respect the minimum size nonetheless - args.limits.min_size = args.limits.min_size.max(instrisic_min_size); + limits.min_size = limits.min_size.max(instrisic_min_size); let size = intrinsic_size.max(resolved_size); - let min_size = instrisic_min_size.max(args.limits.min_size); + let min_size = instrisic_min_size.max(limits.min_size); Sizing { min: Rect::from_size(min_size), preferred: Rect::from_size(size), margin, hints: intrinsic_hints.combine(hints), + maximize: entity.get_copy(maximize()).unwrap_or_default(), } }; let constraints = Constraints::from_entity(entity); + if constraints.aspect_ratio.is_some() { + hints.coupled_size = true; + } + sizing.min = sizing.min.with_size(constraints.apply(sizing.min.size())); sizing.preferred = sizing .preferred @@ -429,7 +436,7 @@ pub(crate) fn query_size(world: &World, entity: &EntityRef, mut args: QueryArgs) cache.insert_query( args.direction, - CachedValue::new(args.limits, args.content_area, sizing), + CachedValue::new(limits, args.content_area, sizing), ); sizing @@ -444,7 +451,7 @@ pub(crate) fn apply_layout( entity: &EntityRef, // The size of the potentially available space for the subtree content_area: Vec2, - mut limits: LayoutLimits, + limits: LayoutLimits, ) -> Block { puffin::profile_function!(format!("{entity}")); // assert!(limits.min_size.x <= limits.max_size.x); @@ -467,17 +474,18 @@ pub(crate) fn apply_layout( let mut query = entity.query(&query); let (cache, &margin, &padding, min_size, max_size, size, size_resolver, children, layout) = query.get().unwrap(); - let min_size = min_size.resolve(content_area); - let max_size = max_size.map(|v| v.resolve(content_area)); - let external_max_size = limits.max_size; - - tracing::debug!(%min_size, ?max_size, %limits); - limits.min_size = limits.min_size.max(min_size); - limits.max_size = limits.max_size.max(limits.min_size); - if let Some(max_size) = max_size { - limits.max_size = limits.max_size.min(max_size); - } + let min_size = min_size.resolve(content_area); + let max_size = max_size + .map(|v| v.resolve(content_area)) + .unwrap_or(Vec2::INFINITY); + + let external_limits = limits; + let limits = LayoutLimits { + // Minimum size is *always* respected, even if that entails overflowing + min_size: limits.min_size.max(min_size), + max_size: limits.max_size.min(max_size), + }; // Check if cache is still valid @@ -486,8 +494,6 @@ pub(crate) fn apply_layout( tracing::debug!(%entity, %value.value.rect, %value.value.can_grow, "found valid cached layout"); return value.value; - } else { - tracing::debug!(%entity, ?value, "invalid cached layout"); } } @@ -500,12 +506,24 @@ pub(crate) fn apply_layout( let children = children.map(Vec::as_slice).unwrap_or(&[]); - let resolved_size = size.resolve(content_area); + let mut resolved_size = size.resolve(content_area); + + let maximized = entity.get_copy(maximize()).unwrap_or_default(); + + if maximized.x > 0.0 { + resolved_size.x = limits.max_size.x; + } + + if maximized.y > 0.0 { + resolved_size.y = limits.max_size.y; + } + + let can_maximize = maximized.cmpgt(Vec2::ZERO); let can_grow = BVec2::new( - resolved_size.x > external_max_size.x, - resolved_size.y > external_max_size.y, - ); + resolved_size.x > external_limits.max_size.x, + resolved_size.y > external_limits.max_size.y, + ) | can_maximize; // tracing::trace!(%entity, ?resolved_size, ?external_max_size, %can_grow); @@ -628,10 +646,11 @@ impl Constraints { fn apply(&self, mut size: Vec2) -> Vec2 { if let Some(aspect_ratio) = self.aspect_ratio { if aspect_ratio > 0.0 { - if size.x > size.y { - size = vec2(size.y * aspect_ratio, size.y); + // > 1.0 means width > height + if aspect_ratio > 1.0 { + size = vec2(size.x, size.y / aspect_ratio); } else { - size = vec2(size.x, size.x / aspect_ratio); + size = vec2(size.y * aspect_ratio, size.y); } } } diff --git a/violet-core/src/layout/stack.rs b/violet-core/src/layout/stack.rs index 203e471..e16879e 100644 --- a/violet-core/src/layout/stack.rs +++ b/violet-core/src/layout/stack.rs @@ -9,7 +9,7 @@ use crate::{ }; use super::{ - resolve_pos, apply_layout, Alignment, Block, Direction, LayoutLimits, QueryArgs, Sizing, + apply_layout, resolve_pos, Alignment, Block, Direction, LayoutLimits, QueryArgs, Sizing, }; #[derive(Debug)] @@ -185,6 +185,7 @@ impl StackLayout { let mut preferred_bounds = StackableBounds::from_rect(min_rect); let mut hints = SizingHints::default(); + let mut maximize = Vec2::ZERO; for &child in children.iter() { let entity = world.entity(child).expect("invalid child"); @@ -202,6 +203,8 @@ impl StackLayout { }, ); + maximize = maximize + sizing.maximize; + hints = hints.combine(sizing.hints); min_bounds = min_bounds.merge(&StackableBounds::new(sizing.min, sizing.margin)); @@ -220,6 +223,7 @@ impl StackLayout { // .clamp_size(limits.min_size, limits.max_size), margin: min_margin.max(preferred_margin), hints, + maximize, } } } diff --git a/violet-core/src/scope.rs b/violet-core/src/scope.rs index e63fd9e..0c41c15 100644 --- a/violet-core/src/scope.rs +++ b/violet-core/src/scope.rs @@ -257,4 +257,8 @@ impl Effect>> Effect for ScopedEffect { Poll::Ready(()) } } + + fn label(&self) -> Option<&str> { + self.effect.label() + } } diff --git a/violet-core/src/style/mod.rs b/violet-core/src/style/mod.rs index c2d2925..3b09902 100644 --- a/violet-core/src/style/mod.rs +++ b/violet-core/src/style/mod.rs @@ -8,15 +8,15 @@ use glam::Vec2; use palette::{IntoColor, Oklab, Srgba}; use crate::{ - components::{color, draw_shape, margin, max_size, min_size, padding, size}, + components::{color, draw_shape, margin, max_size, maximize, min_size, padding, size}, shape::shape_rectangle, unit::Unit, Edges, Scope, }; use self::colors::{ - EERIE_BLACK_300, EERIE_BLACK_600, EERIE_BLACK_700, EERIE_BLACK_DEFAULT, JADE_400, JADE_600, - JADE_DEFAULT, LION_DEFAULT, PLATINUM_DEFAULT, REDWOOD_DEFAULT, + EERIE_BLACK_300, EERIE_BLACK_700, EERIE_BLACK_DEFAULT, JADE_400, JADE_600, JADE_DEFAULT, + LION_DEFAULT, PLATINUM_DEFAULT, REDWOOD_DEFAULT, }; #[macro_export] @@ -57,6 +57,7 @@ pub struct WidgetSize { pub max_size: Option>, pub margin: Option>, pub padding: Option>, + pub maximize: Option, } impl WidgetSize { @@ -75,7 +76,8 @@ impl WidgetSize { .set_opt(padding(), p) .set_opt(size(), self.size) .set_opt(min_size(), self.min_size) - .set_opt(max_size(), self.max_size); + .set_opt(max_size(), self.max_size) + .set_opt(maximize(), self.maximize); } /// Set the size @@ -107,6 +109,12 @@ impl WidgetSize { self.padding = Some(padding.into()); self } + + /// Maximize the widget to the available size with the given weight. + pub fn with_maximize(mut self, maximize: Vec2) -> Self { + self.maximize = Some(maximize); + self + } } /// A widget that allows you to set its sizing properties @@ -170,6 +178,14 @@ pub trait SizeExt { self } + fn with_maximize(mut self, maximize: Vec2) -> Self + where + Self: Sized, + { + self.size_mut().maximize = Some(maximize); + self + } + fn size_mut(&mut self) -> &mut WidgetSize; } @@ -211,10 +227,7 @@ impl ValueOrRef { pub(crate) fn resolve(self, stylesheet: EntityRef<'_>) -> T { match self { ValueOrRef::Value(value) => value, - ValueOrRef::Ref(component) => { - let value = stylesheet.get_copy(component).unwrap(); - value - } + ValueOrRef::Ref(component) => stylesheet.get_copy(component).unwrap(), } } } diff --git a/violet-core/src/utils.rs b/violet-core/src/utils.rs index e6ff011..334013a 100644 --- a/violet-core/src/utils.rs +++ b/violet-core/src/utils.rs @@ -1,6 +1,6 @@ use std::task::Poll; -use futures::Stream; +use futures::{ready, Future, Stream}; #[macro_export] macro_rules! to_owned { @@ -86,3 +86,62 @@ where } } } + +#[pin_project::pin_project] +pub struct Throttle { + #[pin] + stream: S, + #[pin] + fut: Option, + throttle: C, +} + +impl Throttle { + pub fn new(stream: S, throttle: C) -> Self { + Self { + stream, + fut: None, + throttle, + } + } +} + +impl Stream for Throttle +where + S: Stream, + F: Future, + C: FnMut() -> F, +{ + type Item = S::Item; + + fn poll_next( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + let mut p = self.project(); + + if let Some(fut) = p.fut.as_mut().as_pin_mut() { + ready!(fut.poll(cx)); + p.fut.set(None); + } + + let item = ready!(p.stream.poll_next(cx)); + + if let Some(item) = item { + p.fut.set(Some((p.throttle)())); + Poll::Ready(Some(item)) + } else { + Poll::Ready(None) + } + } +} + +/// Throttles a stream with the provided future +pub fn throttle(stream: S, throttle: C) -> Throttle +where + S: Stream, + F: Future, + C: FnMut() -> F, +{ + Throttle::new(stream, throttle) +} diff --git a/violet-core/src/widget/basic.rs b/violet-core/src/widget/basic.rs index 8634489..e44b539 100644 --- a/violet-core/src/widget/basic.rs +++ b/violet-core/src/widget/basic.rs @@ -8,7 +8,10 @@ use crate::{ self, aspect_ratio, color, draw_shape, font_size, min_size, size, text, text_wrap, }, shape, - style::{spacing_large, spacing_small, SizeExt, StyleExt, ValueOrRef, WidgetSize}, + style::{ + colors::REDWOOD_DEFAULT, spacing_large, spacing_small, SizeExt, StyleExt, ValueOrRef, + WidgetSize, + }, text::{TextSegment, Wrap}, unit::Unit, Scope, Widget, @@ -50,11 +53,22 @@ impl SizeExt for Rectangle { pub struct Image { image: K, + size: WidgetSize, + aspect_ratio: Option, } impl Image { pub fn new(image: K) -> Self { - Self { image } + Self { + image, + size: Default::default(), + aspect_ratio: None, + } + } + + pub fn with_aspect_ratio(mut self, aspect_ratio: f32) -> Self { + self.aspect_ratio = Some(aspect_ratio); + self } } @@ -65,18 +79,26 @@ where fn mount(self, scope: &mut Scope) { let image = scope.assets_mut().try_load(&self.image).ok(); if let Some(image) = image { + self.size.mount(scope); scope .set(color(), Srgba::new(1.0, 1.0, 1.0, 1.0)) .set(draw_shape(shape::shape_rectangle()), ()) - .set(components::image(), image); + .set(components::image(), image) + .set_opt(components::aspect_ratio(), self.aspect_ratio); } else { - Text::new("Image not found") - .with_color(Srgba::new(1.0, 0.0, 0.0, 1.0)) + label("Image not found") + .with_color(REDWOOD_DEFAULT) .mount(scope); } } } +impl SizeExt for Image { + fn size_mut(&mut self) -> &mut WidgetSize { + &mut self.size + } +} + /// Style and decorate text pub struct TextStyle { font_size: f32, @@ -204,48 +226,3 @@ where scope.set(components::offset(), self.offset); } } - -pub struct BoxSized { - size: Unit, - min_size: Unit, - aspect_ratio: f32, - widget: W, -} - -impl BoxSized { - pub fn new(widget: W) -> Self { - Self { - size: Unit::ZERO, - min_size: Unit::ZERO, - widget, - aspect_ratio: 0.0, - } - } - - pub fn with_size(mut self, size: Unit) -> Self { - self.size = size; - self - } - - pub fn with_min_size(mut self, min_size: Unit) -> Self { - self.min_size = min_size; - self - } - - /// Set the aspect ratio - pub fn with_aspect_ratio(mut self, aspect_ratio: f32) -> Self { - self.aspect_ratio = aspect_ratio; - self - } -} - -impl Widget for BoxSized { - fn mount(self, scope: &mut Scope<'_>) { - self.widget.mount(scope); - - scope - .set(size(), self.size) - .set(min_size(), self.min_size) - .set(aspect_ratio(), self.aspect_ratio); - } -} diff --git a/violet-core/src/widget/container.rs b/violet-core/src/widget/container.rs index 7ef8743..3b0609d 100644 --- a/violet-core/src/widget/container.rs +++ b/violet-core/src/widget/container.rs @@ -3,16 +3,15 @@ use glam::Vec2; use winit::event::ElementState; use crate::{ - components::{anchor, layout, margin, max_size, min_size, offset, padding, rect}, + components::{anchor, layout, offset, rect}, input::{focusable, on_cursor_move, on_mouse_input}, layout::{Alignment, Direction, FlowLayout, Layout, StackLayout}, style::{ - colors::{EERIE_BLACK_300, EERIE_BLACK_400}, primary_background, secondary_background, spacing_medium, spacing_small, Background, SizeExt, StyleExt, WidgetSize, }, unit::Unit, - Edges, Frame, Scope, Widget, WidgetCollection, + Frame, Scope, Widget, WidgetCollection, }; /// Style for most container type widgets. diff --git a/violet-core/src/widget/future.rs b/violet-core/src/widget/future.rs index afb72e1..dec5434 100644 --- a/violet-core/src/widget/future.rs +++ b/violet-core/src/widget/future.rs @@ -1,7 +1,7 @@ use futures::{Future, Stream}; use futures_signals::signal::{self, SignalExt}; -use crate::{components::layout, layout::Layout, FutureEffect, Scope, StreamEffect, Widget}; +use crate::{effect::Effect, FutureEffect, Scope, StreamEffect, Widget}; pub struct SignalWidget(pub S); @@ -15,25 +15,26 @@ impl SignalWidget { } } -impl Widget for SignalWidget +impl Widget for SignalWidget where - S: 'static + signal::Signal, - W: Widget, + S: 'static + signal::Signal, + S::Item: Widget, { fn mount(self, scope: &mut crate::Scope<'_>) { let mut child = None; let stream = self.0.to_stream(); + let label = std::any::type_name::(); - scope.spawn_effect(StreamEffect::new( - stream, - move |scope: &mut Scope<'_>, v| { + scope.spawn_effect( + StreamEffect::new(stream, move |scope: &mut Scope<'_>, v| { if let Some(child) = child { scope.detach(child); } child = Some(scope.attach(v)); - }, - )); + }) + .with_label(label), + ); } } @@ -42,24 +43,31 @@ where S: Stream, S::Item: Widget; -impl Widget for StreamWidget +impl Widget for StreamWidget where - S: 'static + Stream, - W: Widget, + S: 'static + Stream, + S::Item: Widget, { fn mount(self, scope: &mut crate::Scope<'_>) { let mut child = None; - scope.spawn_effect(StreamEffect::new( - self.0, - move |scope: &mut Scope<'_>, v| { + let label = std::any::type_name::(); + + scope.spawn_effect( + StreamEffect::new(self.0, move |scope: &mut Scope<'_>, v| { + puffin::profile_scope!("StreamWidget::mount", "update child widget"); if let Some(child) = child { + puffin::profile_scope!("detach"); scope.detach(child); } - child = Some(scope.attach(v)); - }, - )); + { + puffin::profile_scope!("attach"); + child = Some(scope.attach(v)); + } + }) + .with_label(label), + ); } } @@ -68,17 +76,18 @@ where S: Future, S::Output: Widget; -impl Widget for FutureWidget +impl Widget for FutureWidget where - S: 'static + Future, - W: Widget, + S: 'static + Future, + S::Output: Widget, { fn mount(self, scope: &mut crate::Scope<'_>) { - scope.spawn_effect(FutureEffect::new( - self.0, - move |scope: &mut Scope<'_>, v| { + let label = std::any::type_name::(); + scope.spawn_effect( + FutureEffect::new(self.0, move |scope: &mut Scope<'_>, v| { scope.attach(v); - }, - )); + }) + .with_label(label), + ); } } diff --git a/violet-core/src/widget/interactive/button.rs b/violet-core/src/widget/interactive/button.rs index 77e6332..b743fb0 100644 --- a/violet-core/src/widget/interactive/button.rs +++ b/violet-core/src/widget/interactive/button.rs @@ -1,5 +1,3 @@ -use futures::Stream; -use futures_signals::signal::Mutable; use palette::Srgba; use winit::event::{ElementState, MouseButton}; @@ -7,14 +5,13 @@ use crate::{ components::{self, color}, input::{focusable, on_mouse_input}, layout::Alignment, - state::{StateDuplex, StateMut, StateStream, WatchState}, + state::{StateDuplex, StateStream, WatchState}, style::{ danger_item, interactive_inactive, interactive_pressed, spacing_medium, success_item, warning_item, Background, SizeExt, StyleExt, ValueOrRef, WidgetSize, }, - to_owned, unit::Unit, - widget::{ContainerStyle, Rectangle, Stack, Text}, + widget::{ContainerStyle, Stack, Text}, Frame, Scope, Widget, }; diff --git a/violet-core/src/widget/interactive/input.rs b/violet-core/src/widget/interactive/input.rs index 66ad76c..69144ca 100644 --- a/violet-core/src/widget/interactive/input.rs +++ b/violet-core/src/widget/interactive/input.rs @@ -6,6 +6,7 @@ use futures_signals::signal::{self, Mutable, SignalExt}; use glam::{vec2, Vec2}; use itertools::Itertools; use palette::Srgba; +use web_time::Duration; use winit::{ event::ElementState, keyboard::{Key, NamedKey}, @@ -21,8 +22,10 @@ use crate::{ ValueOrRef, WidgetSize, }, text::{LayoutGlyphs, TextSegment}, + time::sleep, to_owned, unit::Unit, + utils::throttle, widget::{ row, NoOp, Positioned, Rectangle, SignalWidget, Stack, StreamWidget, Text, WidgetExt, }, @@ -114,7 +117,7 @@ impl Widget for TextInput { let mut cursor_pos = Vec2::ZERO; - let mut new_text = content.stream().fuse(); + let mut new_text = throttle(content.stream(), || sleep(Duration::from_millis(100))).fuse(); let mut focused = false; loop { diff --git a/violet-core/src/widget/interactive/slider.rs b/violet-core/src/widget/interactive/slider.rs index 03b4734..4288267 100644 --- a/violet-core/src/widget/interactive/slider.rs +++ b/violet-core/src/widget/interactive/slider.rs @@ -5,6 +5,7 @@ use futures::{stream::BoxStream, StreamExt}; use futures_signals::signal::Mutable; use glam::Vec2; use palette::Srgba; +use web_time::Duration; use winit::event::ElementState; use crate::{ @@ -13,10 +14,11 @@ use crate::{ layout::Alignment, state::{State, StateDuplex, StateStream}, style::{interactive_active, interactive_inactive, spacing_small, SizeExt, StyleExt}, + time::sleep, to_owned, unit::Unit, - utils::zip_latest, - widget::{row, BoxSized, ContainerStyle, Positioned, Rectangle, Stack, StreamWidget, Text}, + utils::{throttle, zip_latest}, + widget::{row, ContainerStyle, Positioned, Rectangle, Stack, StreamWidget, Text}, Scope, StreamEffect, Widget, }; @@ -90,7 +92,7 @@ impl Widget for Slider { let handle_size = self.style.handle_size; let track_size = self.style.track_size; - let track = scope.attach(BoxSized::new(Rectangle::new(track_color)).with_size(track_size)); + let track = scope.attach(Rectangle::new(track_color).with_size(track_size)); let min = self.min.to_progress(); let max = self.max.to_progress(); @@ -175,11 +177,9 @@ impl Widget for SliderHandle { } })); - Positioned::new( - BoxSized::new(Rectangle::new(self.handle_color)).with_min_size(self.handle_size), - ) - .with_anchor(Unit::rel2(0.5, 0.0)) - .mount(scope) + Positioned::new(Rectangle::new(self.handle_color).with_min_size(self.handle_size)) + .with_anchor(Unit::rel2(0.5, 0.0)) + .mount(scope) } } diff --git a/violet-demo/Cargo.toml b/violet-demo/Cargo.toml index 88e6b3f..1798624 100644 --- a/violet-demo/Cargo.toml +++ b/violet-demo/Cargo.toml @@ -28,3 +28,4 @@ futures.workspace = true wasm-bindgen-futures = "0.4" itertools.workspace = true tracing-tree.workspace = true +puffin.workspace = true diff --git a/violet-demo/index.html b/violet-demo/index.html index 30987a7..afa5406 100644 --- a/violet-demo/index.html +++ b/violet-demo/index.html @@ -12,7 +12,9 @@ /* This allows the flexbox to grow to max size, this is needed for WebGPU */ flex: 1; /* This forces CSS to ignore the width/height of the canvas, this is needed for WebGL */ - /* contain: size; */ + contain: size; + width: 100%; + height: 100%; background-color: #001133; } @@ -26,7 +28,7 @@ -
+
diff --git a/violet-demo/src/lib.rs b/violet-demo/src/lib.rs index 2f5fb76..dd30371 100644 --- a/violet-demo/src/lib.rs +++ b/violet-demo/src/lib.rs @@ -12,9 +12,10 @@ use violet::{ layout::Alignment, state::{DynStateDuplex, State, StateMut, StateStream, StateStreamRef}, style::{primary_background, Background, SizeExt, ValueOrRef}, + time::sleep, to_owned, unit::Unit, - utils::zip_latest_ref, + utils::{throttle, zip_latest_ref}, widget::{ card, column, label, pill, row, Button, Radio, Rectangle, SliderWithLabel, Stack, StreamWidget, Text, TextInput, WidgetExt, @@ -24,6 +25,7 @@ use violet::{ futures_signals::signal::Mutable, glam::vec2, palette::{FromColor, IntoColor, OklabHue, Oklch, Srgb}, + web_time::Duration, wgpu::renderer::RendererConfig, }; use wasm_bindgen::prelude::*; @@ -65,7 +67,7 @@ pub fn run() { setup(); violet::wgpu::App::new() - .with_renderer_config(RendererConfig { debug_mode: true }) + .with_renderer_config(RendererConfig { debug_mode: false }) .run(MainApp) .unwrap(); } @@ -108,6 +110,7 @@ impl Tints { impl Widget for Tints { fn mount(self, scope: &mut Scope<'_>) { + puffin::profile_function!(); row((1..=9) .map(|i| { let f = (i as f32) / 10.0; @@ -183,13 +186,20 @@ impl Widget for Palettes { to_owned![current_choice]; let discard = &discard; move |(i, item)| { + puffin::profile_scope!("Update palette item", format!("{i}")); let checkbox = Radio::new( current_choice .clone() .map(move |v| v == Some(i), move |state| state.then_some(i)), ); - card(row((checkbox, discard(i), StreamWidget(item.stream())))) + card(row(( + checkbox, + discard(i), + StreamWidget(throttle(item.stream(), || { + sleep(Duration::from_millis(100)) + })), + ))) } }) .collect_vec(); @@ -225,6 +235,7 @@ pub struct PaletteColor { impl Widget for PaletteColor { fn mount(self, scope: &mut Scope<'_>) { + puffin::profile_function!(); Stack::new(( row((Tints::new(self.color, self.falloff),)), pill(label(color_hex(self.color))), @@ -259,7 +270,8 @@ impl Widget for PaletteEditor { let color_rect = color.stream().map(|v| { Rectangle::new(ValueOrRef::value(v.into_color())) - .with_size(Unit::new(vec2(0.0, 100.0), vec2(1.0, 0.0))) + .with_min_size(Unit::px2(100.0, 100.0)) + .with_maximize(Vec2::X) // .with_min_size(Unit::new(vec2(0.0, 100.0), vec2(1.0, 0.0))) .with_name("ColorPreview") }); diff --git a/violet-wgpu/src/app.rs b/violet-wgpu/src/app.rs index 31f5054..56440b2 100644 --- a/violet-wgpu/src/app.rs +++ b/violet-wgpu/src/app.rs @@ -207,10 +207,7 @@ impl App { stats.record_frame(frame_time); - { - puffin::profile_scope!("Tick"); - ex.tick(&mut frame); - } + ex.tick(&mut frame); update_animations(&mut frame, cur_time - start_time); diff --git a/violet-wgpu/src/renderer/debug_renderer.rs b/violet-wgpu/src/renderer/debug_renderer.rs index 330133a..2b9cffb 100644 --- a/violet-wgpu/src/renderer/debug_renderer.rs +++ b/violet-wgpu/src/renderer/debug_renderer.rs @@ -139,7 +139,7 @@ impl DebugRenderer { let mut query = query.borrow(&frame.world); let clamped_indicators = query.iter().filter_map(|(entity, v)| { - let clamped_query_vertical = if v + let can_grow_vert = if v .get_query(Direction::Vertical) .as_ref() .is_some_and(|v| v.value.hints.can_grow.any()) @@ -149,7 +149,7 @@ impl DebugRenderer { Vec3::ZERO }; - let clamped_query_horizontal = if v + let can_grow_hor = if v .get_query(Direction::Horizontal) .as_ref() .is_some_and(|v| v.value.hints.can_grow.any()) @@ -159,19 +159,13 @@ impl DebugRenderer { Vec3::ZERO }; - let clamped_layout = if v.layout().is_some_and(|v| v.value.can_grow.any()) { + let can_grow = if v.layout().is_some_and(|v| v.value.can_grow.any()) { vec3(0.0, 0.0, 0.5) } else { Vec3::ZERO }; - let color: Vec3 = [ - clamped_query_vertical, - clamped_query_horizontal, - clamped_layout, - ] - .into_iter() - .sum(); + let color: Vec3 = [can_grow_vert, can_grow_hor, can_grow].into_iter().sum(); if color == Vec3::ZERO { None