diff --git a/Cargo.lock b/Cargo.lock index 2ce6932..2c0e88f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -686,20 +686,18 @@ dependencies = [ [[package]] name = "cosmic-text" -version = "0.12.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59fd57d82eb4bfe7ffa9b1cec0c05e2fd378155b47f255a67983cb4afe0e80c2" +checksum = "da46a9d5a8905cc538a4a5bceb6a4510de7a51049c5588c0114efce102bcbbe8" dependencies = [ "bitflags 2.6.0", "fontdb", - "hashbrown", - "libm", "log", "rangemap", - "rayon", "rustc-hash", "rustybuzz", "self_cell", + "smol_str", "swash", "sys-locale", "ttf-parser 0.21.1", @@ -1035,9 +1033,9 @@ dependencies = [ [[package]] name = "font-types" -version = "0.6.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f0189ccb084f77c5523e08288d418cbaa09c451a08515678a0aa265df9a8b60" +checksum = "511e2c18a516c666d27867d2f9821f76e7d591f762e9fc41dd6cc5c90fe54b0b" dependencies = [ "bytemuck", ] @@ -1049,6 +1047,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0299020c3ef3f60f526a4f64ab4a3d4ce116b1acbf24cdd22da0068e5d81dc3" dependencies = [ "log", + "memmap2", "slotmap", "tinyvec", "ttf-parser 0.20.0", @@ -1906,6 +1905,27 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "nucleo" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5262af4c94921c2646c5ac6ff7900c2af9cbb08dc26a797e18130a7019c039d4" +dependencies = [ + "nucleo-matcher", + "parking_lot", + "rayon", +] + +[[package]] +name = "nucleo-matcher" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf33f538733d1a5a3494b836ba913207f14d9d4a1d3cd67030c5061bdd2cac85" +dependencies = [ + "memchr", + "unicode-segmentation", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2583,9 +2603,9 @@ dependencies = [ [[package]] name = "read-fonts" -version = "0.20.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c141b9980e1150201b2a3a32879001c8f975fe313ec3df5471a9b5c79a880cd" +checksum = "6717cf23b488adf64b9d711329542ba34de147df262370221940dfabc2c91358" dependencies = [ "bytemuck", "font-types", @@ -2878,9 +2898,9 @@ checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "skrifa" -version = "0.20.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abea4738067b1e628c6ce28b2c216c19e9ea95715cdb332680e821c3bec2ef23" +checksum = "8c31071dedf532758ecf3fed987cdb4bd9509f900e026ab684b4ecb81ea49841" dependencies = [ "bytemuck", "read-fonts", @@ -2988,9 +3008,9 @@ checksum = "20e16a0f46cf5fd675563ef54f26e83e20f2366bcf027bcb3cc3ed2b98aaf2ca" [[package]] name = "swash" -version = "0.1.18" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93cdc334a50fcc2aa3f04761af3b28196280a6aaadb1ef11215c478ae32615ac" +checksum = "47846491253e976bdd07d0f9cc24b7daf24720d11309302ccbbc6e6b6e53550a" dependencies = [ "skrifa", "yazi", @@ -3492,6 +3512,7 @@ dependencies = [ "image", "itertools 0.14.0", "more-asserts", + "nucleo", "once_cell", "palette", "parking_lot", @@ -4331,9 +4352,9 @@ checksum = "539a77ee7c0de333dcc6da69b177380a0b81e0dacfa4f7344c465a36871ee601" [[package]] name = "yazi" -version = "0.1.6" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c94451ac9513335b5e23d7a8a2b61a7102398b8cca5160829d313e84c9d98be1" +checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5" [[package]] name = "zbus" @@ -4401,9 +4422,9 @@ dependencies = [ [[package]] name = "zeno" -version = "0.2.3" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd15f8e0dbb966fd9245e7498c7e9e5055d9e5c8b676b95bd67091cd11a1e697" +checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" [[package]] name = "zerocopy" diff --git a/Cargo.toml b/Cargo.toml index 732be2c..cf27c94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,10 @@ 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", branch = "dev", version = "0.8.0", features = ["derive", "puffin"] } +flax = { git = "https://github.com/ten3roberts/flax", branch = "dev", version = "0.8.0", features = [ + "derive", + "puffin", +] } atomic_refcell = "0.1" futures-signals = "0.3" @@ -38,7 +41,10 @@ anyhow = "1.0" once_cell = "1.19" slab = "0.4" tynm = "0.1" -tokio = { version = "1.39", default-features = false, features = ["macros", "rt"] } +tokio = { version = "1.39", default-features = false, features = [ + "macros", + "rt", +] } arrayvec = "0.7" sync_wrapper = "1.0" smallvec = "1.13" @@ -47,10 +53,17 @@ tween = { version = "2.0.4", features = ["glam"] } bytemuck = { version = "1.17", features = ["derive"] } winit = "0.30" -wgpu = { version = "22.1", default-features = false, features = ["fragile-send-sync-non-atomic-wasm", "webgl", "wgsl"] } +wgpu = { version = "22.1", default-features = false, features = [ + "fragile-send-sync-non-atomic-wasm", + "webgl", + "wgsl", +] } palette = { version = "0.7", features = ["serializing"] } dashmap = "6.0" -image = { version = "0.25", default-features = false, features = ["png", "jpeg"] } +image = { version = "0.25", default-features = false, features = [ + "png", + "jpeg", +] } color-hex = "0.2" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" @@ -60,7 +73,12 @@ rfd = "0.14" tracing = "0.1" pin-project = "1.1" -cosmic-text = { version = "0.12", default-features = false, features = ["wasm-web", "rayon", "swash", "no_std"] } +cosmic-text = { version = "0.14", default-features = false, features = [ + "shape-run-cache", + "wasm-web", + "swash", + "std", +] } guillotiere = "0.6" bytes = "1.7" more-asserts = "0.3" diff --git a/examples/layouting.rs b/examples/layouting.rs index 41cb16f..5f0d6cc 100644 --- a/examples/layouting.rs +++ b/examples/layouting.rs @@ -1,14 +1,17 @@ +use core::f32; + use glam::Vec2; -use palette::Srgba; use tracing_subscriber::{layer::SubscriberExt, registry, util::SubscriberInitExt, EnvFilter}; use tracing_tree::HierarchicalLayer; use violet_core::{ - style::{base_colors::*, surface_primary, surface_secondary, SizeExt, StylesheetOptions}, + style::{ + base_colors::*, spacing_medium, surface_primary, surface_secondary, SizeExt, + StylesheetOptions, + }, unit::Unit, - widget::{bold, card, col, label, pill, raised_card, row, Button, Rectangle}, - Edges, Widget, + widget::{col, row, Rectangle, Stack}, + Widget, WidgetCollection, }; -use violet_lucide::icons::*; use violet_wgpu::renderer::MainRendererConfig; pub fn main() -> anyhow::Result<()> { @@ -31,54 +34,36 @@ pub fn main() -> anyhow::Result<()> { .build(), ) .with_renderer_config(MainRendererConfig { debug_mode: false }) - .run(main_ui()) + .run(layout_using_flow()) } -fn main_ui() -> impl Widget { +fn layout_using_flow() -> impl Widget { + fn container(content: impl WidgetCollection) -> Stack { + Stack::new(content).with_background(surface_secondary()) + // .with_margin(spacing_small()) + } row(( col(( - card(()).with_maximize(Vec2::ONE), - window(LUCIDE_LIST_TREE, "Bottom Panel", card(())), + container(()) + .with_margin(spacing_medium()) + .with_background(STONE_700) + .with_maximize(Vec2::ONE) + // .with_min_size(Unit::px2(0.0, 200.0)) + .with_size(Unit::px2(600.0, 400.0)), + Stack::new(Rectangle::new(SAPPHIRE_500).with_min_size(Unit::px2(260.0, 40.0))), + container(()) + .with_margin(spacing_medium()) + .with_background(RUBY_800) + .with_maximize(Vec2::ONE), + // .with_size(Unit::px2(1.0, 1.0)), )), - // window( - // LUCIDE_SATELLITE, - // "Right Panel", - // col( - // // Slider::new(Mutable::new(50), 0, 100), - // // Slider::new(Mutable::new(20), 0, 100), - // // col(( - // // row((Stack::new(label("Value")).with_maximize(Vec2::X), TextInput::new(Mutable::new("Editable".to_string())).input_box())), - // // )), - // raised_card(label("lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.").with_wrap(cosmic_text::Wrap::WordOrGlyph)) - // ), - // ) - // .with_max_size(Unit::px2(400.0, f32::MAX)) - // .with_maximize(Vec2::Y), + container(()) + .with_margin(spacing_medium()) + .with_background(STONE_900) + .with_max_size(Unit::px2(40.0, f32::MAX)) + .with_min_size(Unit::px2(40.0, 0.0)) + .with_maximize(Vec2::Y), )) .with_background(surface_primary()) .with_contain_margins(true) } - -fn window( - icon: impl Into, - title: impl Into, - content: impl Widget, -) -> impl Widget + SizeExt { - card( - col(( - row(( - label(icon).with_margin(Edges::right(8.0)), - bold(title), - // Rectangle::new(Srgba::new(0.0, 0.0, 0.0, 0.0)).with_maximize(Vec2::X), - pill(row(( - Button::label(LUCIDE_MAXIMIZE).success(), - Button::label(LUCIDE_MINUS).warning(), - Button::label(LUCIDE_X).danger(), - ))), - )) - .center(), - content, - )), // .with_stretch(true), - ) - .with_background(surface_secondary()) -} diff --git a/examples/too_small.rs b/examples/too_small.rs index 45bf7e9..20c9a88 100644 --- a/examples/too_small.rs +++ b/examples/too_small.rs @@ -1,17 +1,11 @@ -use glam::Vec2; -use palette::Srgba; use tracing_subscriber::{layer::SubscriberExt, registry, util::SubscriberInitExt, EnvFilter}; use tracing_tree::HierarchicalLayer; use violet_core::{ - style::{ - base_colors::*, default_corner_radius, surface_primary, surface_secondary, SizeExt, - StylesheetOptions, - }, + style::{base_colors::*, surface_primary, SizeExt, StylesheetOptions}, unit::Unit, - widget::{bold, card, col, label, pill, raised_card, row, Button, Rectangle}, + widget::{col, label, pill, row, Rectangle}, Edges, Widget, }; -use violet_lucide::icons::*; use violet_wgpu::renderer::MainRendererConfig; pub fn main() -> anyhow::Result<()> { diff --git a/violet-core/Cargo.toml b/violet-core/Cargo.toml index 2e0e5bb..3d50bb0 100644 --- a/violet-core/Cargo.toml +++ b/violet-core/Cargo.toml @@ -28,8 +28,9 @@ slab.workspace = true tynm.workspace = true arrayvec.workspace = true sync_wrapper.workspace = true -ropey.workspace = true -tween.workspace = true + ropey.workspace = true + tween.workspace = true + nucleo = "0.5" bytemuck.workspace = true palette.workspace = true diff --git a/violet-core/src/constraints.rs b/violet-core/src/constraints.rs index 2a6a941..bdb446c 100644 --- a/violet-core/src/constraints.rs +++ b/violet-core/src/constraints.rs @@ -9,7 +9,7 @@ pub struct FixedAreaConstraint { impl SizeResolver for FixedAreaConstraint { fn query_size(&mut self, _: &flax::EntityRef, args: QueryArgs) -> (Vec2, Vec2, SizingHints) { - let size = (args.limits.max_size / self.unit_size) + let size = (args.limits.layout_max_size / self.unit_size) .floor() .max(Vec2::ONE); @@ -30,7 +30,9 @@ impl SizeResolver for FixedAreaConstraint { } fn apply_layout(&mut self, _: &flax::EntityRef, args: LayoutArgs) -> (Vec2, BVec2) { - let width = (args.limits.max_size.x / self.unit_size).floor().max(1.0); + let width = (args.limits.layout_max_size.x / self.unit_size) + .floor() + .max(1.0); let height = (self.area / width).ceil(); diff --git a/violet-core/src/layout/cache.rs b/violet-core/src/layout/cache.rs index 876b742..f06df30 100644 --- a/violet-core/src/layout/cache.rs +++ b/violet-core/src/layout/cache.rs @@ -10,7 +10,7 @@ pub struct CachedValue { pub value: T, } -const ENABLE: bool = false; +const ENABLE: bool = true; pub const LAYOUT_TOLERANCE: f32 = 0.1; impl CachedValue { @@ -141,21 +141,21 @@ pub(crate) fn validate_cached_query( let hints = &value.hints; #[allow(clippy::nonminimal_bool)] - if hints.can_grow.x && cache.limits.max_size.x < limits.max_size.x - || (hints.can_grow.x && cache.limits.max_size.y < limits.max_size.y) + if hints.can_grow.x && cache.limits.layout_max_size.x < limits.layout_max_size.x + || (hints.can_grow.x && cache.limits.layout_max_size.y < limits.layout_max_size.y) { - // tracing::info!(%hints.can_grow, ?cache.limits.max_size, %limits.max_size, "invalidated by can_grow"); + // tracing::debug!(%hints.can_grow, ?cache.limits.max_size, %limits.max_size, "invalidated by can_grow"); } - min_size.x >= limits.min_size.x - LAYOUT_TOLERANCE - && min_size.y >= limits.min_size.y - LAYOUT_TOLERANCE + min_size.x >= limits.layout_min_size.x - LAYOUT_TOLERANCE + && min_size.y >= limits.layout_min_size.y - LAYOUT_TOLERANCE // Min may be larger than preferred for the orthogonal optimization direction - && min_size.x <= limits.max_size.x + LAYOUT_TOLERANCE - && min_size.y <= limits.max_size.y + LAYOUT_TOLERANCE - && preferred_size.x <= limits.max_size.x + LAYOUT_TOLERANCE - && preferred_size.y <= limits.max_size.y + LAYOUT_TOLERANCE - && (!hints.can_grow.x || cache.limits.max_size.x >= limits.max_size.x - LAYOUT_TOLERANCE) - && (!hints.can_grow.y || cache.limits.max_size.y >= limits.max_size.y - LAYOUT_TOLERANCE) + && min_size.x <= limits.layout_max_size.x + LAYOUT_TOLERANCE + && min_size.y <= limits.layout_max_size.y + LAYOUT_TOLERANCE + && preferred_size.x <= limits.layout_max_size.x + LAYOUT_TOLERANCE + && preferred_size.y <= limits.layout_max_size.y + LAYOUT_TOLERANCE + && (!hints.can_grow.x || cache.limits.layout_max_size.x >= limits.layout_max_size.x - LAYOUT_TOLERANCE) + && (!hints.can_grow.y || cache.limits.layout_max_size.y >= limits.layout_max_size.y - LAYOUT_TOLERANCE) && (!hints.relative_size.x || (cache.content_area.x - content_area.x).abs() < LAYOUT_TOLERANCE) && (!hints.relative_size.y || (cache.content_area.y - content_area.y).abs() < LAYOUT_TOLERANCE) } @@ -169,15 +169,15 @@ pub(crate) fn validate_cached_layout( ) -> bool { let value = &cache.value; - let size = value.rect.size().min(cache.limits.max_size); + let size = value.rect.size().min(cache.limits.layout_max_size); - size.x >= limits.min_size.x - LAYOUT_TOLERANCE - && size.y >= limits.min_size.y - LAYOUT_TOLERANCE + size.x >= limits.layout_min_size.x - LAYOUT_TOLERANCE + && size.y >= limits.layout_min_size.y - LAYOUT_TOLERANCE // Min may be larger than preferred for the orthogonal optimization direction - && size.x <= limits.max_size.x + LAYOUT_TOLERANCE - && size.y <= limits.max_size.y + LAYOUT_TOLERANCE - && (!value.can_grow.x || cache.limits.max_size.x >= limits.max_size.x - LAYOUT_TOLERANCE) - && (!value.can_grow.y || cache.limits.max_size.y >= limits.max_size.y - LAYOUT_TOLERANCE) + && size.x <= limits.layout_max_size.x + LAYOUT_TOLERANCE + && size.y <= limits.layout_max_size.y + LAYOUT_TOLERANCE + && (!value.can_grow.x || cache.limits.layout_max_size.x >= limits.layout_max_size.x - LAYOUT_TOLERANCE) + && (!value.can_grow.y || cache.limits.layout_max_size.y >= limits.layout_max_size.y - LAYOUT_TOLERANCE) && (!relative_size.x || (cache.content_area.x - content_area.x).abs() < LAYOUT_TOLERANCE) && (!relative_size.y || (cache.content_area.y - content_area.y).abs() < LAYOUT_TOLERANCE) } @@ -195,15 +195,15 @@ pub(crate) fn validate_cached_row( // tracing::debug!( ?preferred_size, %cache.limits.max_size, %limits.max_size, "validate_cached_row"); - min_size.x >= limits.min_size.x - LAYOUT_TOLERANCE - && min_size.y >= limits.min_size.y - LAYOUT_TOLERANCE + min_size.x >= limits.layout_min_size.x - LAYOUT_TOLERANCE + && min_size.y >= limits.layout_min_size.y - LAYOUT_TOLERANCE // Min may be larger than preferred for the orthogonal optimization direction - && min_size.x <= limits.max_size.x + LAYOUT_TOLERANCE - && min_size.y <= limits.max_size.y + LAYOUT_TOLERANCE - && preferred_size.x <= limits.max_size.x + LAYOUT_TOLERANCE - && preferred_size.y <= limits.max_size.y + LAYOUT_TOLERANCE - && (!hints.can_grow.x || cache.limits.max_size.x >= limits.max_size.x - LAYOUT_TOLERANCE) - && (!hints.can_grow.y || cache.limits.max_size.y >= limits.max_size.y - LAYOUT_TOLERANCE) + && min_size.x <= limits.layout_max_size.x + LAYOUT_TOLERANCE + && min_size.y <= limits.layout_max_size.y + LAYOUT_TOLERANCE + && preferred_size.x <= limits.layout_max_size.x + LAYOUT_TOLERANCE + && preferred_size.y <= limits.layout_max_size.y + LAYOUT_TOLERANCE + && (!hints.can_grow.x || cache.limits.layout_max_size.x >= limits.layout_max_size.x - LAYOUT_TOLERANCE) + && (!hints.can_grow.y || cache.limits.layout_max_size.y >= limits.layout_max_size.y - LAYOUT_TOLERANCE) && (!hints.relative_size.x || (cache.content_area.x - content_area.x).abs() < LAYOUT_TOLERANCE) && (!hints.relative_size.y || (cache.content_area.y - content_area.y).abs() < LAYOUT_TOLERANCE) } diff --git a/violet-core/src/layout/float.rs b/violet-core/src/layout/float.rs index a8bb841..f158625 100644 --- a/violet-core/src/layout/float.rs +++ b/violet-core/src/layout/float.rs @@ -1,10 +1,13 @@ use flax::{Entity, EntityRef, World}; use glam::{BVec2, Vec2}; -use super::{apply_layout, ApplyLayoutArgs, LayoutBlock, LayoutLimits, QueryArgs, Sizing}; +use super::{apply_layout, LayoutBlock, LayoutLimits, QueryArgs, Sizing}; use crate::{ components, - layout::{query_layout_size, Direction, LayoutArgs, SizingHints}, + layout::{ + query_layout_size, ContainerLayoutArgs, ContainerQueryArgs, Direction, LayoutArgs, + SizingHints, + }, Edges, Rect, }; @@ -17,16 +20,22 @@ use crate::{ pub struct FloatLayout {} impl FloatLayout { - pub(crate) fn apply(&self, world: &World, _: &EntityRef, args: ApplyLayoutArgs) -> LayoutBlock { + pub(crate) fn apply( + &self, + world: &World, + _: &EntityRef, + args: ContainerLayoutArgs, + ) -> LayoutBlock { puffin::profile_function!(); let _span = tracing::debug_span!("FloatLayout::apply").entered(); + let mut maximize = Vec2::ZERO; args.children.iter().for_each(|&child| { let entity = world.entity(child).expect("invalid child"); let limits = LayoutLimits { - min_size: Vec2::ZERO, - max_size: Vec2::MAX, + layout_min_size: Vec2::ZERO, + layout_max_size: Vec2::MAX, }; let block = apply_layout( @@ -38,18 +47,19 @@ impl FloatLayout { }, ); + maximize = (maximize + block.maximize).min(Vec2::ONE); entity.update_dedup(components::rect(), block.rect); entity.update_dedup(components::local_position(), Vec2::ZERO); }); - LayoutBlock::new(Rect::ZERO, Edges::ZERO, BVec2::FALSE) + LayoutBlock::new(Rect::ZERO, Edges::ZERO, BVec2::FALSE, maximize) } pub(crate) fn query_size( &self, world: &World, children: &[Entity], - args: QueryArgs, + args: ContainerQueryArgs, _: Vec2, ) -> Sizing { puffin::profile_function!(); @@ -64,8 +74,8 @@ impl FloatLayout { &entity, QueryArgs { limits: LayoutLimits { - min_size: Vec2::ZERO, - max_size: Vec2::MAX, + layout_min_size: Vec2::ZERO, + layout_max_size: Vec2::MAX, }, content_area: args.content_area, direction: Direction::Horizontal, diff --git a/violet-core/src/layout/flow.rs b/violet-core/src/layout/flow.rs index 328ebf1..aad78f7 100644 --- a/violet-core/src/layout/flow.rs +++ b/violet-core/src/layout/flow.rs @@ -5,14 +5,14 @@ use glam::{vec2, BVec2, Vec2}; use itertools::Itertools; use super::{ - apply_layout, cache::LayoutCache, resolve_pos, ApplyLayoutArgs, Direction, LayoutArgs, - LayoutBlock, LayoutLimits, QueryArgs, Sizing, + apply_layout, cache::LayoutCache, resolve_pos, Direction, LayoutArgs, LayoutBlock, + LayoutLimits, QueryArgs, Sizing, }; use crate::{ components, layout::{ cache::{validate_cached_row, CachedValue}, - query_layout_size, SizingHints, + clamp_maximize, query_layout_size, ContainerLayoutArgs, ContainerQueryArgs, SizingHints, }, Edges, Rect, }; @@ -52,11 +52,13 @@ impl QueryCursor { } fn put(&mut self, block: &LayoutBlock) -> (Vec2, f32) { - if block.rect.size() == Vec2::ZERO { - let placement_pos = self.main_cursor * self.axis + self.cross_cursor * self.cross_axis; - - return (placement_pos, 0.0); - } + // Only skip margin if zero-size and not maximizing in axis + // let maximizing_in_axis = block.maximize.dot(self.axis) > 0.0; + // if block.rect.size() == Vec2::ZERO && !maximizing_in_axis { + // let placement_pos = self.main_cursor * self.axis + self.cross_cursor * self.cross_axis; + // + // return (placement_pos, 0.0); + // } let (back_margin, front_margin) = block.margin.in_axis(self.axis); @@ -173,11 +175,13 @@ impl AlignCursor { } fn put(&mut self, block: &LayoutBlock) -> Vec2 { - if block.rect.size() == Vec2::ZERO { - let placement_pos = self.main_cursor * self.axis + self.cross_cursor * self.cross_axis; - - return placement_pos; - } + // Only skip margin if zero-size and not maximizing in axis + // let maximizing_in_axis = block.maximize.dot(self.axis) > 0.0; + // if block.rect.size() == Vec2::ZERO && !maximizing_in_axis { + // let placement_pos = self.main_cursor * self.axis + self.cross_cursor * self.cross_axis; + // + // return placement_pos; + // } let (back_margin, front_margin) = block.margin.in_axis(self.axis); @@ -321,7 +325,7 @@ impl FlowLayout { &self, world: &World, entity: &EntityRef, - args: ApplyLayoutArgs, + args: ContainerLayoutArgs, ) -> LayoutBlock { puffin::profile_function!(); let _span = tracing::debug_span!("Flow::apply", ?args.limits, flow=?self).entered(); @@ -339,7 +343,7 @@ impl FlowLayout { }, ); - let _guard = tracing::info_span!( + let _guard = tracing::debug_span!( "query_row", ?self.direction, min=?row.min.size(), @@ -350,19 +354,20 @@ impl FlowLayout { // In the apply stage, we must respect the parent's max_size, but also ensure we never shrink below the minimum required by children. // This allows fixed-width panels and windows to restrict layout, but lets children "prop up" the parent if their minimum is larger. let new_limits = LayoutLimits { - min_size: args.limits.min_size, - max_size: args.limits.max_size.max(row.min.size()), + layout_min_size: args.limits.layout_min_size, + layout_max_size: args.limits.layout_max_size.max(row.min.size()), }; - tracing::info!(?new_limits, ?args.limits, "apply"); + tracing::debug!(?new_limits, ?args.limits, "apply"); self.distribute_children( world, entity, &row, - LayoutArgs { + ContainerLayoutArgs { content_area: args.content_area, limits: new_limits, + ..args }, args.desired_size, args.offset, @@ -374,7 +379,7 @@ impl FlowLayout { world: &World, entity: &EntityRef, row: &Row, - args: LayoutArgs, + args: ContainerLayoutArgs, preferred_size: Vec2, offset: Vec2, ) -> LayoutBlock { @@ -393,15 +398,17 @@ impl FlowLayout { // Clamp distributable size to parent's max_size let target_inner_size = distribute_size - .min(args.limits.max_size.dot(axis) - minimum_inner_size) + .min(args.limits.layout_max_size.dot(axis) - minimum_inner_size) .max(0.0); - let remaining_size = (args.limits.max_size.dot(axis) - preferred_inner_size).max(0.0); + let remaining_size = + (args.limits.layout_max_size.dot(axis) - preferred_inner_size).max(0.0); + // tracing::info!(preferred_inner_size, target_inner_size, remaining_size); let contain_margins = self.contain_margins as i32 as f32; // for cross - let available_size = args.limits.max_size; + let available_size = args.limits.layout_max_size; let mut cursor = QueryCursor::new(offset, axis, cross_axis, self.contain_margins); @@ -411,10 +418,12 @@ impl FlowLayout { let cross_size = row .preferred .size() - .max(args.limits.min_size) + .max(args.limits.layout_min_size) .max(preferred_size) .dot(cross_axis); + let mut maximized = Vec2::ZERO; + let mut can_grow = BVec2::FALSE; // Distribute the size to the widgets and apply their layout let blocks = row @@ -427,9 +436,10 @@ impl FlowLayout { let block_min_size = sizing.min.size().dot(axis); let block_preferred_size = sizing.desired.size().dot(axis); - tracing::info!(block_preferred_size, block_min_size, "layout"); + tracing::debug!(block_preferred_size, block_min_size, "layout"); if block_min_size > block_preferred_size { tracing::error!( + %entity, ?block_min_size, block_preferred_size, "min is larger than preferred", @@ -472,19 +482,19 @@ impl FlowLayout { let child_limits = if self.stretch { let cross_size = cross_size - child_margin.size().dot(cross_axis); LayoutLimits { - min_size: cross_size * cross_axis, - max_size: axis_sizing + cross_size * cross_axis, + layout_min_size: cross_size * cross_axis, + layout_max_size: axis_sizing + cross_size * cross_axis, } } else { let cross_size = available_size - child_margin.size(); LayoutLimits { - min_size: Vec2::ZERO, - max_size: axis_sizing + cross_size * cross_axis, + layout_min_size: Vec2::ZERO, + layout_max_size: axis_sizing + cross_size * cross_axis, } }; - tracing::info!( %child_limits.max_size, "layout child"); + tracing::debug!( %child_limits.layout_max_size, "layout child"); let block = apply_layout( world, &entity, @@ -494,6 +504,8 @@ impl FlowLayout { }, ); + maximized = (maximized + clamp_maximize(sizing.maximize, args.immediate_max_size)) + .min(Vec2::ONE); can_grow |= block.can_grow; tracing::debug!(?block, "updated subtree"); @@ -511,8 +523,8 @@ impl FlowLayout { let start = match (self.direction, self.reverse) { (Direction::Horizontal, false) => offset, (Direction::Vertical, false) => offset, - (Direction::Horizontal, true) => vec2(args.limits.max_size.x, offset.y), - (Direction::Vertical, true) => vec2(offset.x, args.limits.max_size.y), + (Direction::Horizontal, true) => vec2(args.limits.layout_max_size.x, offset.y), + (Direction::Vertical, true) => vec2(offset.x, args.limits.layout_max_size.y), }; let offset = resolve_pos(entity, args.content_area, line_size); @@ -541,7 +553,7 @@ impl FlowLayout { let rect = cursor .finish() .max_size(preferred_size) - .max_size(args.limits.min_size); + .max_size(args.limits.layout_min_size); let margin = self.direction @@ -549,14 +561,14 @@ impl FlowLayout { tracing::debug!(%rect, %entity, %margin, %args.limits); - LayoutBlock::new(rect, margin, can_grow) + LayoutBlock::new(rect, margin, can_grow, maximized) } fn distribute_query( &self, world: &World, row: &Row, - args: QueryArgs, + args: ContainerQueryArgs, preferred_size: Vec2, ) -> Sizing { puffin::profile_function!(); @@ -574,10 +586,10 @@ impl FlowLayout { // Clipped maximum that we remap to let target_inner_size = distribute_size - .min(args.limits.max_size.dot(axis) - minimum_inner_size) + .min(args.limits.layout_max_size.dot(axis) - minimum_inner_size) .max(0.0); - let remaining_size = args.limits.max_size.dot(axis) - preferred_inner_size; + let remaining_size = args.limits.layout_max_size.dot(axis) - preferred_inner_size; tracing::debug!( min=?row.min.size(), @@ -587,7 +599,7 @@ impl FlowLayout { "distribute" ); - let available_size = args.limits.max_size; + let available_size = args.limits.layout_max_size; let mut min_cursor = QueryCursor::new(Vec2::ZERO, axis, cross_axis, self.contain_margins); let mut cursor = QueryCursor::new(Vec2::ZERO, axis, cross_axis, self.contain_margins); @@ -597,11 +609,12 @@ impl FlowLayout { let cross_size = row .preferred .size() - .max(args.limits.min_size) + .max(args.limits.layout_min_size) .max(preferred_size) .dot(cross_axis); let mut hints = SizingHints::default(); + let mut maximized = Vec2::ZERO; // Distribute the size to the widgets and apply their layout let blocks = row .blocks @@ -618,7 +631,7 @@ impl FlowLayout { %entity, ?block_min_size, block_preferred_size, - "min is larger than preferred", + "min is larger than preferred (query)", ); // return; @@ -668,15 +681,15 @@ impl FlowLayout { let child_limits = if self.stretch { let cross_size = cross_size - child_margin.size().dot(cross_axis); LayoutLimits { - min_size: cross_size*cross_axis, - max_size: axis_sizing + cross_size * cross_axis, + layout_min_size: cross_size*cross_axis, + layout_max_size: axis_sizing + cross_size * cross_axis, } } else { let cross_size = available_size - child_margin.size(); LayoutLimits { - min_size: Vec2::ZERO, - max_size: axis_sizing + cross_size * cross_axis, + layout_min_size: Vec2::ZERO, + layout_max_size: axis_sizing + cross_size * cross_axis, } }; @@ -689,13 +702,14 @@ impl FlowLayout { direction: args.direction, }); + maximized = (maximized + clamp_maximize(sizing.maximize, args.immediate_max_size)).min(Vec2::ONE); hints = hints.combine(sizing.hints); tracing::debug!(min=%sizing.min.size(), preferred=%sizing.desired.size(), ?child_limits, "query"); - min_cursor.put(&LayoutBlock::new(sizing.min, sizing.margin, sizing.hints.can_grow)); - cursor.put(&LayoutBlock::new(sizing.desired, sizing.margin, sizing.hints.can_grow)); + min_cursor.put(&LayoutBlock::new(sizing.min, sizing.margin, sizing.hints.can_grow, maximized)); + cursor.put(&LayoutBlock::new(sizing.desired, sizing.margin, sizing.hints.can_grow, maximized)); sizing }).collect_vec(); @@ -720,6 +734,7 @@ impl FlowLayout { block.desired, block.margin, block.hints.can_grow, + block.maximize, )); } @@ -729,8 +744,8 @@ impl FlowLayout { .to_edges(cursor.main_margin, cursor.cross_margin(), self.reverse); Sizing { - min: min_rect.max_size(args.limits.min_size), - desired: rect.max_size(args.limits.min_size), + min: min_rect.max_size(args.limits.layout_min_size), + desired: rect.max_size(args.limits.layout_min_size), margin, hints, maximize: row.maximize_sum, @@ -767,7 +782,7 @@ impl FlowLayout { let mut hints = SizingHints::default(); - let mut maximize = Vec2::ZERO; + let mut maximize_sum = Vec2::ZERO; // In queries, always pass the actual available max_size to children so they can adapt if possible (e.g., text can wrap). // However, do not forcibly clamp the query result; children can report their true minimum/preferred size, even if it exceeds max_size. @@ -776,50 +791,56 @@ impl FlowLayout { .map(|&child| { let child_entity = world.entity(child).expect("Invalid child"); - let child_margin = if self.contain_margins { - query_layout_size( - world, - &child_entity, - QueryArgs { - limits: LayoutLimits { - min_size: Vec2::ZERO, - max_size: args.limits.max_size, - }, - content_area: args.content_area, - direction: self.direction, - }, - ) - .margin - } else { - Edges::ZERO - }; + // let child_margin = if self.contain_margins { + // query_layout_size( + // world, + // &child_entity, + // QueryArgs { + // limits: LayoutLimits { + // layout_min_size: Vec2::ZERO, + // layout_max_size: args.limits.layout_max_size, + // }, + // content_area: args.content_area, + // direction: self.direction, + // }, + // ) + // .margin + // } else { + // Edges::ZERO + // }; let child_sizing = query_layout_size( world, &child_entity, QueryArgs { limits: LayoutLimits { - min_size: Vec2::ZERO, - max_size: args.limits.max_size, + layout_min_size: Vec2::ZERO, + layout_max_size: args.limits.layout_max_size, }, content_area: args.content_area, direction: self.direction, }, ); - maximize += child_sizing.maximize; + if child_sizing.maximize.x > 1.0 || child_sizing.maximize.y > 1.0 { + tracing::error!(%child_entity, ?child_sizing, "Too large maximize sum") + } + + maximize_sum += child_sizing.maximize; hints = hints.combine(child_sizing.hints); min_cursor.put(&LayoutBlock::new( child_sizing.min, child_sizing.margin, child_sizing.hints.can_grow, + child_sizing.maximize, )); preferred_cursor.put(&LayoutBlock::new( child_sizing.desired, child_sizing.margin, child_sizing.hints.can_grow, + child_sizing.maximize, )); // NOTE: cross size is guaranteed to be fulfilled by the parent @@ -837,7 +858,7 @@ impl FlowLayout { preferred, blocks: Arc::new(blocks), hints, - maximize_sum: maximize, + maximize_sum, }; cache.insert_query_row(CachedValue::new( @@ -853,7 +874,7 @@ impl FlowLayout { world: &World, cache: &mut LayoutCache, children: &[Entity], - args: QueryArgs, + args: ContainerQueryArgs, preferred_size: Vec2, ) -> Sizing { puffin::profile_function!(format!("{args:?}")); @@ -895,7 +916,8 @@ impl FlowLayout { children, QueryArgs { direction: self.direction, - ..args + limits: args.limits, + content_area: args.content_area, }, ); @@ -910,7 +932,8 @@ impl FlowLayout { let preferred_size = row.preferred.size().dot(axis); let to_distribute = (preferred_size - minimum_inner_size).max(0.0); - let can_grow = to_distribute > (args.limits.max_size.dot(axis) - minimum_inner_size); + let can_grow = + to_distribute > (args.limits.layout_max_size.dot(axis) - minimum_inner_size); let can_grow = if self.direction.is_horizontal() { BVec2::new(can_grow, false) @@ -919,14 +942,14 @@ impl FlowLayout { }; let to_distribute = to_distribute - .min(args.limits.max_size.dot(axis) - minimum_inner_size) + .min(args.limits.layout_max_size.dot(axis) - minimum_inner_size) .max(0.0); let preferred = (minimum_inner_size + to_distribute) * axis + row.preferred.size() * cross; - let min = row.min.max_size(args.limits.min_size); - let preferred = preferred.max(preferred).max(args.limits.min_size); + let min = row.min.max_size(args.limits.layout_min_size); + let preferred = preferred.max(preferred).max(args.limits.layout_min_size); let (axis, cross_axis) = self.direction.as_main_and_cross(self.reverse); // Do layout one last time for alignment @@ -944,6 +967,7 @@ impl FlowLayout { block.desired, block.margin, block.hints.can_grow, + block.maximize, )); } diff --git a/violet-core/src/layout/mod.rs b/violet-core/src/layout/mod.rs index 51229a7..5444f4a 100644 --- a/violet-core/src/layout/mod.rs +++ b/violet-core/src/layout/mod.rs @@ -81,6 +81,17 @@ pub(crate) struct ApplyLayoutArgs<'a> { offset: Vec2, } +pub(crate) struct ContainerLayoutArgs<'a> { + cache: &'a mut LayoutCache, + children: &'a [Entity], + content_area: Vec2, + limits: LayoutLimits, + desired_size: Vec2, + /// The widget's immediate max size (not the max size enforced by layout) + immediate_max_size: Vec2, + offset: Vec2, +} + #[derive(Debug, Clone)] pub enum Layout { Stack(StackLayout), @@ -93,7 +104,7 @@ impl Layout { &self, world: &World, entity: &EntityRef, - ctx: ApplyLayoutArgs, + ctx: ContainerLayoutArgs, ) -> LayoutBlock { match self { Layout::Stack(v) => v.apply(world, entity, ctx), @@ -107,7 +118,7 @@ impl Layout { world: &World, cache: &mut LayoutCache, children: &[Entity], - args: QueryArgs, + args: ContainerQueryArgs, preferred_size: Vec2, ) -> Sizing { match self { @@ -130,6 +141,21 @@ pub struct QueryArgs { pub direction: Direction, } +/// Arguments for querying the possible layout and size of a widget +#[derive(Debug, Clone, Copy)] +pub struct ContainerQueryArgs { + /// Enforce limits on the layout + pub limits: LayoutLimits, + + /// The widget's immediate max size (not the max size enforced by layout) + immediate_max_size: Vec2, + + /// The size of the potentially available space for the subtree + pub content_area: Vec2, + /// The direction in which the layout is being queried + pub direction: Direction, +} + #[derive(Debug, Clone, Copy)] pub struct Sizing { min: Rect, @@ -162,22 +188,26 @@ impl Display for Sizing { /// Allows for the parent to control the size of the children, such as stretching #[derive(Debug, Clone, Copy, PartialEq)] pub struct LayoutLimits { - pub min_size: Vec2, - pub max_size: Vec2, + pub layout_min_size: Vec2, + pub layout_max_size: Vec2, } impl Default for LayoutLimits { fn default() -> Self { Self { - min_size: Vec2::ZERO, - max_size: Vec2::MAX, + layout_min_size: Vec2::ZERO, + layout_max_size: Vec2::MAX, } } } impl Display for LayoutLimits { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "min: {}, max: {}", self.min_size, self.max_size) + write!( + f, + "min: {}, max: {}", + self.layout_min_size, self.layout_max_size + ) } } @@ -188,14 +218,16 @@ pub struct LayoutBlock { pub(crate) margin: Edges, /// See: [`SizingHints::can_grow`] pub can_grow: BVec2, + pub maximize: Vec2, } impl LayoutBlock { - pub(crate) fn new(rect: Rect, margin: Edges, can_grow: BVec2) -> Self { + pub(crate) fn new(rect: Rect, margin: Edges, can_grow: BVec2, maximize: Vec2) -> Self { Self { rect, margin, can_grow, + maximize, } } } @@ -279,12 +311,15 @@ pub(crate) fn query_layout_size(world: &World, entity: &EntityRef, args: QueryAr let fixed_boundary_size = query.min_size.is_relative() | query.max_size.is_relative(); let min_size_px = query.min_size.resolve(args.content_area); - let max_size_px = query.max_size.resolve(args.content_area); + let immediate_max_size_px = query.max_size.resolve(args.content_area); let mut limits = LayoutLimits { // Minimum size is *always* respected, even if that entails overflowing - min_size: args.limits.min_size.max(min_size_px), - max_size: args.limits.max_size.clamp(min_size_px, max_size_px), + layout_min_size: args.limits.layout_min_size.max(min_size_px), + layout_max_size: args + .limits + .layout_max_size + .clamp(min_size_px, immediate_max_size_px), }; // Check if cache is valid @@ -299,39 +334,40 @@ pub(crate) fn query_layout_size(world: &World, entity: &EntityRef, args: QueryAr let desired_size_px = query.desired_size.resolve(args.content_area); let maximized = entity.get_copy(maximize()).unwrap_or_default(); - let mut hints = SizingHints { + let hints = SizingHints { relative_size: fixed_boundary_size | query.desired_size.is_relative(), can_grow: BVec2::new( - desired_size_px.x > args.limits.max_size.x, - desired_size_px.y > args.limits.max_size.y, + desired_size_px.x > args.limits.layout_max_size.x, + desired_size_px.y > args.limits.layout_max_size.y, ) | maximized.cmpgt(Vec2::ZERO), coupled_size: false, }; // if hints != Default::default() { - // tracing::info!(%entity, ?resolved_size, ?external_max_size, "can grow"); + // tracing::debug!(%entity, ?resolved_size, ?external_max_size, "can grow"); // } // Clamp max size here since we ensure it is > min_size - let clamped_size_px = desired_size_px.clamp(limits.min_size, limits.max_size); + let clamped_size_px = desired_size_px.clamp(limits.layout_min_size, limits.layout_max_size); // Flow let mut sizing = if let Some(layout) = query.layout { // Account for padding eating into the max size - let padded_min_size = (limits.min_size - query.padding.size()).max(Vec2::ZERO); - let padded_max_size = (limits.max_size - query.padding.size()).max(Vec2::ZERO); + let padded_min_size = (limits.layout_min_size - query.padding.size()).max(Vec2::ZERO); + let padded_max_size = (limits.layout_max_size - query.padding.size()).max(Vec2::ZERO); let sizing = layout.query_size( world, query.layout_cache, children, - QueryArgs { + ContainerQueryArgs { limits: LayoutLimits { - min_size: padded_min_size, - max_size: padded_max_size, + layout_min_size: padded_min_size, + layout_max_size: padded_max_size, }, content_area: args.content_area - query.padding.size(), direction: args.direction, + immediate_max_size: immediate_max_size_px, }, // Content area clamped_size_px - query.padding.size(), @@ -345,7 +381,8 @@ pub(crate) fn query_layout_size(world: &World, entity: &EntityRef, args: QueryAr min: sizing.min.pad(*query.padding), desired: sizing.desired.pad(*query.padding), hints: sizing.hints.combine(hints), - maximize: sizing.maximize + entity.get_copy(maximize()).unwrap_or_default(), + maximize: (sizing.maximize + entity.get_copy(maximize()).unwrap_or_default()) + .min(Vec2::ONE), } } // Allow single children to pass through directly, without need for layout @@ -359,7 +396,7 @@ pub(crate) fn query_layout_size(world: &World, entity: &EntityRef, args: QueryAr let (min_size, intrinsic_size, intrinsic_hints) = size_resolver.query_size(entity, args); // If intrinsic_min_size > max_size we overflow, but respect the minimum size nonetheless - limits.min_size = limits.min_size.max(min_size); + limits.layout_min_size = limits.layout_min_size.max(min_size); let intrinsic_size = intrinsic_size.max(clamped_size_px); @@ -421,7 +458,10 @@ fn constrain_sizing( sizing.min = sizing.min.translate(min_offset); sizing.desired = sizing.desired.translate(offset); - if IGNORE_ZERO_SIZE_MARGINS && sizing.desired.size() == Vec2::ZERO { + if IGNORE_ZERO_SIZE_MARGINS + && sizing.desired.size() == Vec2::ZERO + && sizing.maximize == Vec2::ZERO + { sizing.margin = Edges::ZERO } @@ -462,12 +502,15 @@ pub(crate) fn apply_layout(world: &World, entity: &EntityRef, args: LayoutArgs) let query = query.get().expect("Missing items on widget for layout"); let min_size_px = query.min_size.resolve(args.content_area); - let max_size_px = query.max_size.resolve(args.content_area); + let immediate_max_size_px = query.max_size.resolve(args.content_area); let limits = LayoutLimits { // Minimum size is *always* respected, even if that entails overflowing - min_size: args.limits.min_size.max(min_size_px), - max_size: args.limits.max_size.clamp(min_size_px, max_size_px), + layout_min_size: args.limits.layout_min_size.max(min_size_px), + layout_max_size: args + .limits + .layout_max_size + .clamp(min_size_px, immediate_max_size_px), }; // Check if cache is still valid @@ -493,40 +536,41 @@ pub(crate) fn apply_layout(world: &World, entity: &EntityRef, args: LayoutArgs) // Use all the size we can if maximized.x > 0.0 { - resolved_size.x = limits.max_size.x; + resolved_size.x = limits.layout_max_size.x; } if maximized.y > 0.0 { - resolved_size.y = limits.max_size.y; + resolved_size.y = limits.layout_max_size.y; } let can_maximize = maximized.cmpgt(Vec2::ZERO); let can_grow = BVec2::new( - resolved_size.x > args.limits.max_size.x, - resolved_size.y > args.limits.max_size.y, + resolved_size.x > args.limits.layout_max_size.x, + resolved_size.y > args.limits.layout_max_size.y, ) | can_maximize; - let clamped_size_px = resolved_size.clamp(limits.min_size, limits.max_size); + let clamped_size_px = resolved_size.clamp(limits.layout_min_size, limits.layout_max_size); let mut block = if let Some(layout) = query.layout { - let padded_min_size = (limits.min_size - query.padding.size()).max(Vec2::ZERO); - let padded_max_size = (limits.max_size - query.padding.size()).max(Vec2::ZERO); + let padded_min_size = (limits.layout_min_size - query.padding.size()).max(Vec2::ZERO); + let padded_max_size = (limits.layout_max_size - query.padding.size()).max(Vec2::ZERO); let block = layout.apply( world, entity, - ApplyLayoutArgs { + ContainerLayoutArgs { cache: query.layout_cache, children, content_area: args.content_area, limits: LayoutLimits { - min_size: padded_min_size, - max_size: padded_max_size, + layout_min_size: padded_min_size, + layout_max_size: padded_max_size, }, desired_size: clamped_size_px - query.padding.size(), // start of inner content offset: vec2(query.padding.left, query.padding.top), + immediate_max_size: immediate_max_size_px, }, ); @@ -534,6 +578,7 @@ pub(crate) fn apply_layout(world: &World, entity: &EntityRef, args: LayoutArgs) rect: block.rect.pad(*query.padding), margin: (block.margin - *query.padding).max(*query.margin), can_grow: block.can_grow | can_grow, + maximize: (block.maximize + maximized).min(Vec2::ONE), } } else if let [child] = children { let child = world.entity(*child).unwrap(); @@ -546,10 +591,13 @@ pub(crate) fn apply_layout(world: &World, entity: &EntityRef, args: LayoutArgs) // Handle leaf nodes with dynamic size resolution let (intrinsic_size, instrinsic_can_grow) = size_resolver.apply_layout(entity, args); + let intrinsic_size = intrinsic_size.max(clamped_size_px); + LayoutBlock { - rect: Rect::from_size(intrinsic_size.clamp(limits.min_size, limits.max_size)), + rect: Rect::from_size(intrinsic_size), margin: *query.margin, can_grow: instrinsic_can_grow | can_grow, + maximize: maximized, } } else { assert_eq!(children, [], "Widget with children must have a layout"); @@ -558,6 +606,7 @@ pub(crate) fn apply_layout(world: &World, entity: &EntityRef, args: LayoutArgs) rect: Rect::from_size(clamped_size_px), margin: *query.margin, can_grow: can_grow, + maximize: maximized, } }; @@ -567,7 +616,7 @@ pub(crate) fn apply_layout(world: &World, entity: &EntityRef, args: LayoutArgs) let offset = resolve_pos(entity, args.content_area, block.rect.size()); block.rect = block.rect.translate(offset); - if IGNORE_ZERO_SIZE_MARGINS && block.rect.size() == Vec2::ZERO { + if IGNORE_ZERO_SIZE_MARGINS && block.rect.size() == Vec2::ZERO && block.maximize == Vec2::ZERO { block.margin = Edges::ZERO } @@ -580,8 +629,6 @@ pub(crate) fn apply_layout(world: &World, entity: &EntityRef, args: LayoutArgs) .layout_cache .insert_layout(CachedValue::new(limits, args.content_area, block)); - tracing::info!(%entity, size=%block.rect.size(), %args.limits, "applied layout"); - block } @@ -637,3 +684,16 @@ fn resolve_pos(entity: &EntityRef, parent_size: Vec2, self_size: Vec2) -> Vec2 { offset - anchor.resolve(self_size) } + +/// If a child with *maximize* is within a widget with explicit max size it can not expand outside +/// the widget, and thus *maximize* only applies within that subtree, and should not affect +/// layouting outside of it. +/// +/// Returns the new maximize value +fn clamp_maximize(child_maximize: Vec2, max_size: Vec2) -> Vec2 { + return child_maximize + * vec2( + (max_size.x == f32::MAX) as u32 as f32, + (max_size.y == f32::MAX) as u32 as f32, + ); +} diff --git a/violet-core/src/layout/stack.rs b/violet-core/src/layout/stack.rs index 18f4e75..9b8e9d6 100644 --- a/violet-core/src/layout/stack.rs +++ b/violet-core/src/layout/stack.rs @@ -2,12 +2,13 @@ use flax::{Entity, EntityRef, World}; use glam::{vec2, BVec2, Vec2}; use itertools::Itertools; -use super::{ - apply_layout, resolve_pos, ApplyLayoutArgs, LayoutBlock, LayoutLimits, QueryArgs, Sizing, -}; +use super::{apply_layout, resolve_pos, LayoutBlock, LayoutLimits, QueryArgs, Sizing}; use crate::{ components::{self, item_align, LayoutAlignment}, - layout::{query_layout_size, LayoutArgs, SizingHints}, + layout::{ + clamp_maximize, query_layout_size, ContainerLayoutArgs, ContainerQueryArgs, LayoutArgs, + SizingHints, + }, Edges, Rect, }; @@ -33,7 +34,7 @@ use crate::{ pub struct StackLayout { pub alignment: LayoutAlignment, pub clip: BVec2, - pub grow: BVec2, + pub preserve_size: BVec2, } impl Default for StackLayout { @@ -41,7 +42,7 @@ impl Default for StackLayout { Self { alignment: Default::default(), clip: Default::default(), - grow: BVec2::TRUE, + preserve_size: BVec2::FALSE, } } } @@ -51,7 +52,7 @@ impl StackLayout { &self, world: &World, entity: &EntityRef, - args: ApplyLayoutArgs, + args: ContainerLayoutArgs, ) -> LayoutBlock { puffin::profile_function!(); let _span = tracing::debug_span!("StackLayout::apply", %self.clip, %entity).entered(); @@ -62,14 +63,13 @@ impl StackLayout { }; let clip = vec2(self.clip.x as u32 as f32, self.clip.y as u32 as f32); - // let grow = vec2(self.grow.x as u32 as f32, self.grow.y as u32 as f32); let child_limits = LayoutLimits { // Wrapping a widget in a stack layout does not change its minimum size, such as for "stretch" widths // min_size: args.limits.min_size, - min_size: args.limits.min_size, + layout_min_size: args.limits.layout_min_size, // If clip is enabled, the inner content can have any size - max_size: clip * Vec2::MAX + (1.0 - clip) * args.limits.max_size, + layout_max_size: clip * Vec2::MAX + (1.0 - clip) * args.limits.layout_max_size, }; let blocks = args @@ -97,7 +97,7 @@ impl StackLayout { let total_size = child_bounds .size() .max(args.desired_size) - .max(args.limits.min_size); + .max(args.limits.layout_min_size); let mut aligned_bounds = StackableBounds::from_rect(Rect::from_size_pos(args.desired_size, args.offset)); @@ -107,6 +107,7 @@ impl StackLayout { let offset = args.offset; let start_position = resolve_pos(entity, args.content_area, total_size); + let mut maximize = Vec2::ZERO; // Position the entities, with the correct alignment and offset for (child, block) in blocks { let block_size = block.rect.size(); @@ -117,14 +118,17 @@ impl StackLayout { .unwrap_or(self.alignment) .align(total_size, block_size); - let clip_mask = Rect::from_size(clip * args.limits.max_size + Vec2::MAX * (1.0 - clip)) - .translate(clip * offset); + let clip_mask = + Rect::from_size(clip * args.limits.layout_max_size + Vec2::MAX * (1.0 - clip)) + .translate(clip * offset); aligned_bounds = aligned_bounds.merge(&StackableBounds::new( block.rect.translate(local_offset), block.margin, )); + maximize = + (maximize + clamp_maximize(block.maximize, args.immediate_max_size)).min(Vec2::ONE); can_grow |= block.can_grow; child.update_dedup(components::rect(), block.rect).unwrap(); @@ -137,26 +141,27 @@ impl StackLayout { .unwrap(); } - let rect = aligned_bounds.inner; + let child_rect = aligned_bounds.inner; - let rect = rect - .max_size(args.limits.min_size) - .min_size(args.limits.max_size * clip + Vec2::MAX * (1.0 - clip)); + let rect = child_rect + .max_size(args.limits.layout_min_size) + .min_size(args.limits.layout_max_size * clip + Vec2::MAX * (1.0 - clip)); + // tracing::info!(child_rect=?child_rect.size(), rect=?rect.size(), ?args.limits.max_size); let margin = aligned_bounds.margin(); - LayoutBlock::new(rect, margin, can_grow) + LayoutBlock::new(rect, margin, can_grow, maximize) } pub(crate) fn query_size( &self, world: &World, children: &[Entity], - args: QueryArgs, + args: ContainerQueryArgs, preferred_size: Vec2, ) -> Sizing { puffin::profile_function!(); - let min_rect = Rect::from_size(args.limits.min_size); + let min_rect = Rect::from_size(args.limits.layout_min_size); let mut min_bounds = StackableBounds::from_rect(min_rect); let mut preferred_bounds = StackableBounds::from_rect(min_rect); @@ -165,11 +170,14 @@ impl StackLayout { let mut maximize = Vec2::ZERO; let clip = vec2(self.clip.x as u32 as f32, self.clip.y as u32 as f32); - let grow = vec2(self.grow.x as u32 as f32, self.grow.y as u32 as f32); + let preserve_size = vec2( + (self.preserve_size.x || !self.clip.x) as u32 as f32, + (self.preserve_size.y || !self.clip.y) as u32 as f32, + ); let child_limits = LayoutLimits { - min_size: args.limits.min_size, - max_size: clip * Vec2::MAX + (1.0 - clip) * args.limits.max_size, + layout_min_size: args.limits.layout_min_size, + layout_max_size: clip * Vec2::MAX + (1.0 - clip) * args.limits.layout_max_size, }; for &child in children.iter() { @@ -185,7 +193,8 @@ impl StackLayout { }, ); - maximize = (maximize + sizing.maximize).min(Vec2::ONE); + maximize = (maximize + clamp_maximize(sizing.maximize, args.immediate_max_size)) + .min(Vec2::ONE); hints = hints.combine(sizing.hints); @@ -202,15 +211,17 @@ impl StackLayout { let preferred_margin = preferred_bounds.margin(); // ensure size is not smaller than min - let min = min_rect.max_size(args.limits.min_size); - let preferred = preferred_rect.max_size(preferred_size); + let min = min_rect.max_size(args.limits.layout_min_size); + let desired = preferred_rect.max_size(preferred_size); // if clip, clamp to limited max size, otherwise, clip to max - let scissor_size = args.limits.max_size * grow + Vec2::MAX * (1.0 - grow); + let scissor_size = args.limits.layout_max_size * clip + Vec2::MAX * (1.0 - clip); + + let min = min.min_size((preserve_size) * min.size()); Sizing { - min: min.min_size((1.0 - grow) * min.size()), - desired: preferred.clamp_size(min.size(), scissor_size), + min: min, + desired: desired.clamp_size(min.size(), scissor_size), margin: min_margin.max(preferred_margin), hints, maximize, diff --git a/violet-core/src/style/mod.rs b/violet-core/src/style/mod.rs index 8e8525c..219414f 100644 --- a/violet-core/src/style/mod.rs +++ b/violet-core/src/style/mod.rs @@ -390,7 +390,20 @@ impl StylesheetOptions { .set(spacing_small(), Edges::even(self.base_spacing)) .set(spacing_medium(), Edges::even(self.base_spacing * 2.0)) .set(spacing_large(), Edges::even(self.base_spacing * 4.0)) + .set(default_separation(), Edges::even(self.base_spacing)) .set(scrollbar_size(), self.base_spacing * 2.0) + .set( + slider_track_size(), + Unit::px2(self.base_spacing * 64.0, self.base_spacing), + ) + .set( + slider_thumb_size(), + Unit::px2(self.base_spacing * 3.0, self.base_spacing * 3.0), + ) + .set( + dropdown_size(), + Unit::px2(self.base_spacing * 32.0, self.base_spacing * 8.0), + ) .set(default_corner_radius(), Unit::px(4.0)) // text size .set(text_small(), self.base_text_size) @@ -434,6 +447,11 @@ flax::component! { pub spacing_large: Edges, pub default_corner_radius: Unit, + pub default_separation: Edges, + + pub slider_track_size: Unit, + pub slider_thumb_size: Unit, + pub dropdown_size: Unit, pub icon_chevron: String, pub icon_spinner: String, @@ -447,4 +465,6 @@ flax::component! { pub text_small: f32, pub text_medium: f32, pub text_large: f32, + + } diff --git a/violet-core/src/systems.rs b/violet-core/src/systems.rs index 8a241a9..11a8803 100644 --- a/violet-core/src/systems.rs +++ b/violet-core/src/systems.rs @@ -193,8 +193,8 @@ pub fn layout_system(root: Entity, update_canvas_size: bool) -> BoxedSystem { QueryArgs { content_area: canvas_rect.size(), limits: LayoutLimits { - min_size: Vec2::ZERO, - max_size: Vec2::MAX, + layout_min_size: Vec2::ZERO, + layout_max_size: Vec2::MAX, }, direction: Direction::Horizontal, }, @@ -209,8 +209,8 @@ pub fn layout_system(root: Entity, update_canvas_size: bool) -> BoxedSystem { LayoutArgs { content_area: canvas_rect.size(), limits: LayoutLimits { - min_size: Vec2::ZERO, - max_size: canvas_rect.size(), + layout_min_size: Vec2::ZERO, + layout_max_size: canvas_rect.size(), }, }, ); diff --git a/violet-core/src/utils.rs b/violet-core/src/utils.rs index 1f3bd20..a6a63e7 100644 --- a/violet-core/src/utils.rs +++ b/violet-core/src/utils.rs @@ -153,15 +153,12 @@ where if *p.skip { let mut item = None; - loop { - match p.stream.as_mut().poll_next(cx) { - Poll::Ready(Some(v)) => { - item = Some(v); - break; - } - Poll::Ready(None) => return Poll::Ready(None), - Poll::Pending => break, + match p.stream.as_mut().poll_next(cx) { + Poll::Ready(Some(v)) => { + item = Some(v); } + Poll::Ready(None) => return Poll::Ready(None), + Poll::Pending => {} } p.pending.set(Some((p.future)())); diff --git a/violet-core/src/widget/container.rs b/violet-core/src/widget/container.rs index 857f385..7759823 100644 --- a/violet-core/src/widget/container.rs +++ b/violet-core/src/widget/container.rs @@ -8,8 +8,8 @@ use crate::{ layout::{Align, Direction, FloatLayout, FlowLayout, Layout, StackLayout}, scope::ScopeRef, style::{ - default_corner_radius, spacing_medium, spacing_small, surface_secondary, surface_tertiary, - Background, SizeExt, StyleExt, WidgetSizeProps, + default_corner_radius, default_separation, spacing_small, surface_secondary, + surface_tertiary, Background, SizeExt, StyleExt, WidgetSizeProps, }, unit::Unit, Scope, Widget, WidgetCollection, @@ -104,8 +104,9 @@ impl Stack { self } - pub fn with_grow(mut self, grow: impl Into) -> Self { - self.layout.grow = grow.into(); + // Preserved minimum size in the given axis, even if clipping is enabled + pub fn with_preserve_size(mut self, preserve_size: impl Into) -> Self { + self.layout.preserve_size = preserve_size.into(); self } } @@ -221,8 +222,8 @@ impl Widget for List { } } -type OnMove = Box Vec2>; -type OnDrop = Box; +type OnMove = Box, Vec2) -> Vec2>; +type OnDrop = Box, Vec2)>; /// Allows a widget to be dragged around using the mouse. /// @@ -246,13 +247,16 @@ impl Movable { pub fn on_move( mut self, - on_move: impl 'static + Send + Sync + FnMut(&ScopeRef, Vec2) -> Vec2, + on_move: impl 'static + Send + Sync + FnMut(&ScopeRef<'_>, Vec2) -> Vec2, ) -> Self { self.on_move = Box::new(on_move); self } - pub fn on_drop(mut self, on_drop: impl 'static + Send + Sync + FnMut(&ScopeRef, Vec2)) -> Self { + pub fn on_drop( + mut self, + on_drop: impl 'static + Send + Sync + FnMut(&ScopeRef<'_>, Vec2), + ) -> Self { self.on_drop = Box::new(on_drop); self } @@ -270,7 +274,7 @@ impl Widget for Movable { move |scope, input| { if input.state == ElementState::Pressed { let cursor_pos = input.cursor.local_pos; - tracing::info!(?cursor_pos, "grab"); + tracing::debug!(?cursor_pos, "grab"); *start_offset.lock_mut() = cursor_pos; } else { (self.on_drop)(scope, input.cursor.absolute_pos); @@ -352,8 +356,8 @@ pub fn centered_horizontal(widget: W) -> Stack { pub fn card(widget: W) -> Stack { Stack::new(widget) .with_background(Background::new(surface_secondary())) - .with_padding(spacing_medium()) - .with_margin(spacing_medium()) + .with_padding(default_separation()) + .with_margin(default_separation()) .with_corner_radius(default_corner_radius()) } @@ -364,7 +368,7 @@ pub fn raised_card(widget: W) -> Stack { /// Inset content area pub fn panel(widget: W) -> Stack { Stack::new(widget) - .with_padding(spacing_medium()) + .with_padding(default_separation()) .with_background(Background::new(surface_secondary())) } diff --git a/violet-core/src/widget/interactive/base.rs b/violet-core/src/widget/interactive/base.rs index 4ee9f06..f74f8eb 100644 --- a/violet-core/src/widget/interactive/base.rs +++ b/violet-core/src/widget/interactive/base.rs @@ -233,7 +233,7 @@ impl Widget for InteractiveWidget { if last_click.is_some_and(|v| now.duration_since(v) < double_click_timeout) { // abort normal click click_action.take().map(|v| v.abort()); - tracing::info!("double click"); + tracing::debug!("double click"); if let Some(double_click) = &mut self.double_click { double_click(scope); } diff --git a/violet-core/src/widget/interactive/button.rs b/violet-core/src/widget/interactive/button.rs index 1b91437..0297edb 100644 --- a/violet-core/src/widget/interactive/button.rs +++ b/violet-core/src/widget/interactive/button.rs @@ -200,7 +200,7 @@ impl Default for ButtonStyle { hover: ColorPair::new(surface_hover(), element_hover()), size: WidgetSizeProps::default() .with_padding(spacing_medium()) - .with_margin(spacing_medium()) + .with_margin(default_separation()) .with_corner_radius(default_corner_radius()), align: LayoutAlignment::new(Align::Center, Align::Center), } diff --git a/violet-core/src/widget/interactive/collapsible.rs b/violet-core/src/widget/interactive/collapsible.rs index 2156f95..d723fc8 100644 --- a/violet-core/src/widget/interactive/collapsible.rs +++ b/violet-core/src/widget/interactive/collapsible.rs @@ -278,7 +278,7 @@ impl W> Widget for CollapsibleCont // scope.set(max_size(), Unit::px2(f32::MAX, new_height)); // scope.set(min_size(), Unit::px2(0.0, new_height)); - // tracing::info!( + // tracing::debug!( // ?collapsed, // ?inner_size, // ?new_height, diff --git a/violet-core/src/widget/interactive/dropdown.rs b/violet-core/src/widget/interactive/dropdown.rs index 8217d16..ac856ca 100644 --- a/violet-core/src/widget/interactive/dropdown.rs +++ b/violet-core/src/widget/interactive/dropdown.rs @@ -3,11 +3,12 @@ use std::{sync::Arc, time::Duration}; use futures::StreamExt; use futures_signals::signal::{Mutable, SignalExt}; use glam::{vec2, Vec2, Vec3, Vec3Swizzles}; +use nucleo::{Config, Matcher, Utf32Str}; use crate::{ components::{offset, opacity, rect, screen_transform}, state::StateDuplex, - style::{icon_ellipsis, icon_search, surface_interactive, SizeExt, StyleExt}, + style::{dropdown_size, icon_ellipsis, icon_search, surface_interactive, SizeExt, StyleExt}, time::sleep, to_owned, unit::Unit, @@ -26,7 +27,7 @@ use crate::{ pub struct Dropdown { items: I, - searcheable: Option bool + Send + Sync>>, + searchable: Option String + Send + Sync>>, selection: Arc>, } @@ -43,15 +44,15 @@ where Self { items, selection: Arc::new(selection), - searcheable: None, + searchable: None, } } - pub fn searcheable( + pub fn searchable( mut self, - search_fn: impl 'static + Fn(&T, &str) -> bool + Send + Sync, + search_fn: impl 'static + Fn(&T) -> String + Send + Sync, ) -> Self { - self.searcheable = Some(Arc::new(search_fn)); + self.searchable = Some(Arc::new(search_fn)); self } } @@ -78,6 +79,7 @@ where .stylesheet() .get_clone(icon_search()) .unwrap_or_else(|_| ">".to_string()); + let screen_pos = Mutable::new(Vec2::ZERO); scope.monitor(screen_transform(), { @@ -108,12 +110,18 @@ where items: items.clone(), width: rect.size().x, selection: self.selection.clone(), - filter_fn: self.searcheable.clone(), + search_fn: self.searchable.clone(), search_icon: search_icon.clone(), }); current_dropdown.set(Some(CloseOnDropHandle::new(token))); }) + .with_min_size( + scope + .stylesheet() + .get_copy(dropdown_size()) + .unwrap_or_default(), + ) .mount(scope) } } @@ -122,7 +130,7 @@ struct DropdownListOverlay { position: Vec2, width: f32, items: Arc>, - filter_fn: Option bool + Send + Sync>>, + search_fn: Option String + Send + Sync>>, selection: Arc>, search_icon: String, } @@ -137,16 +145,25 @@ impl Overlay for DropdownListOverlay< .set(offset(), Unit::px(self.position)) .set(opacity(), 0.9); - if let Some(filter_fn) = self.filter_fn { + if let Some(search_fn) = self.search_fn { let filter_term = Mutable::new(String::new()); let items = throttle_skip( filter_term .signal_ref(move |filter| { + let mut matcher = Matcher::new(Config::DEFAULT); + let mut haystack_buf = Vec::new(); + let mut needle_buf = Vec::new(); + let needle = Utf32Str::new(filter, &mut needle_buf); self.items .iter() .enumerate() - .filter(|(_, item)| filter_fn(item, filter)) + .filter(|(_, item)| { + haystack_buf.clear(); + let text = search_fn(item); + let haystack = Utf32Str::new(&text, &mut haystack_buf); + matcher.fuzzy_match(haystack, needle).is_some() + }) .map(|(i, item)| { to_owned!(items = self.items); Button::new(item.clone()) @@ -166,7 +183,7 @@ impl Overlay for DropdownListOverlay< col(( row(( label(self.search_icon), - TextInput::new(filter_term).with_maximize(Vec2::X), + TextInput::new(filter_term).with_maximize(Vec2::X).request_focus(), )), ScrollArea::vertical(StreamWidget::new(items)) .with_max_size(Unit::px2(self.width, 100.0)), diff --git a/violet-core/src/widget/interactive/input.rs b/violet-core/src/widget/interactive/input.rs index 35cef40..1b4a5e0 100644 --- a/violet-core/src/widget/interactive/input.rs +++ b/violet-core/src/widget/interactive/input.rs @@ -83,6 +83,7 @@ pub struct TextInput { content: Arc>, options: TextOptions, on_focus_lost: Option)>>, + request_focus: bool, } impl TextInputStyle { @@ -116,6 +117,7 @@ impl TextInput { style: Default::default(), options: Default::default(), on_focus_lost: None, + request_focus: false, } } @@ -147,6 +149,11 @@ impl TextInput { self.on_focus_lost = Some(Box::new(set)); self } + + pub fn request_focus(mut self) -> Self { + self.request_focus = true; + self + } } impl StyleExt for TextInput { @@ -227,6 +234,12 @@ impl Widget for TextInput { self.options, )); + if self.request_focus { + if let Some(sender) = scope.get_atom(crate::input::request_focus_sender()) { + sender.send(scope.entity().id()).ok(); + } + } + let dragging = Mutable::new(None); let mut last_click = None; diff --git a/violet-core/src/widget/interactive/slider.rs b/violet-core/src/widget/interactive/slider.rs index 7f248cc..90d7318 100644 --- a/violet-core/src/widget/interactive/slider.rs +++ b/violet-core/src/widget/interactive/slider.rs @@ -57,6 +57,13 @@ impl Default for SliderStyle { } } +/// Determines how the slider maps values to positions. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ScaleMode { + Linear, + Logarithmic, +} + pub struct Slider { style: SliderStyle, value: Arc>, @@ -64,6 +71,7 @@ pub struct Slider { max: V, transform: Option V>>, scrub_mode: bool, + scale_mode: ScaleMode, } impl Slider { @@ -78,6 +86,7 @@ impl Slider { style: Default::default(), transform: None, scrub_mode: false, + scale_mode: ScaleMode::Linear, } } @@ -102,6 +111,12 @@ impl Slider { self.scrub_mode = scrub_mode; self } + + /// Enable logarithmic scaling for this slider + pub fn logarithmic(mut self) -> Self { + self.scale_mode = ScaleMode::Logarithmic; + self + } } impl Widget for Slider { @@ -121,69 +136,110 @@ impl Widget for Slider { .with_corner_radius(default_corner_radius()), ); - let min = self.min.to_progress(); - let max = self.max.to_progress(); + #[derive(Clone, Copy)] + struct ScaleParams { + min: f32, + max: f32, + mode: ScaleMode, + } + let raw_min = self.min.to_progress(); + let min = match self.scale_mode { + ScaleMode::Logarithmic => { + if raw_min < 1e-3 { + tracing::warn!("Logarithmic slider min clamped from {} to 1e-3", raw_min); + 1e-3 + } else { + raw_min + } + } + ScaleMode::Linear => raw_min, + }; + let max = self.max.to_progress(); + let mode = self.scale_mode; + let scale_params = ScaleParams { min, max, mode }; fn get_progress_value( entity: &EntityRef, track_pos: f32, - min: f32, - max: f32, + params: ScaleParams, ) -> V { let rect = entity.get_copy(rect()).unwrap(); let padding = entity.get_copy(padding()).unwrap_or_default(); - - let value = ((track_pos - padding.left) / (rect.size().x - padding.size().x)) - .clamp(0.0, 1.0) - * (max - min) - + min; - + let norm = + ((track_pos - padding.left) / (rect.size().x - padding.size().x)).clamp(0.0, 1.0); + + let value = match params.mode { + ScaleMode::Linear => norm * (params.max - params.min) + params.min, + ScaleMode::Logarithmic => { + let ln_min = params.min.ln(); + let ln_max = params.max.ln(); + let ln_value = norm * (ln_max - ln_min) + ln_min; + ln_value.exp() + } + }; V::from_progress(value) } - fn get_slider_position(entity: &EntityRef, progress: f32, min: f32, max: f32) -> f32 { + fn get_slider_position(entity: &EntityRef, current_value: f32, params: ScaleParams) -> f32 { let rect = entity.get_copy(rect()).unwrap(); let padding = entity.get_copy(padding()).unwrap_or_default(); let size = rect.size().x - padding.size().x; - - (progress - min) * size / (max - min) + padding.left + let norm = match params.mode { + ScaleMode::Linear => (current_value - params.min) / (params.max - params.min), + ScaleMode::Logarithmic => { + let ln_min = params.min.ln(); + let ln_max = params.max.ln(); + let ln_value = current_value.max(params.min).ln(); + let norm = (ln_value - ln_min) / (ln_max - ln_min); + norm + } + }; + let pos = norm.clamp(0.0, 1.0) * size + padding.left; + pos } fn update_scrubbed( entity: &EntityRef, - drag_distance: f32, - start_value: f32, - min: f32, - max: f32, + current_pos: f32, + params: ScaleParams, ) -> V { let rect = entity.get_copy(rect()).unwrap(); let padding = entity.get_copy(padding()).unwrap_or_default(); - - let value = (drag_distance / (rect.size().x - padding.size().x)) * (max - min) + min; - - V::from_progress((start_value + value).clamp(min, max)) + let norm = ((current_pos - padding.left) / (rect.size().x - padding.size().x)).clamp(0.0, 1.0); + let value = match params.mode { + ScaleMode::Linear => norm * (params.max - params.min) + params.min, + ScaleMode::Logarithmic => { + let ln_min = params.min.ln(); + let ln_max = params.max.ln(); + let ln_value = norm * (ln_max - ln_min) + ln_min; + ln_value.exp() + } + }; + V::from_progress(value) } if self.style.fill { let fill = SliderFill { value: self.value.stream(), - min, - max, + min: min, // use clamped min + max: max, rect_id: track, height: self.style.track_size.px.y, color: fill_color, + scale_mode: self.scale_mode, }; scope.attach(Float::new(fill)); } let handle = SliderHandle { value: self.value.stream(), - min, - max, + min: min, // use clamped min + max: max, rect_id: track, corner_radius: self.style.handle_corner_radius, handle_color, handle_size, + scale_mode: self.scale_mode, }; scope.attach(Float::new(handle)); @@ -203,12 +259,12 @@ impl Widget for Slider { .set(interactive(), ()) .on_event(on_mouse_input(), { to_owned![value, drag_start]; + let params = scale_params; move |scope, input| { if input.state == ElementState::Pressed { - let progress = - get_progress_value(scope, input.cursor.local_pos.x, min, max); + let progress = get_progress_value(scope, input.cursor.local_pos.x, params); if let Ok(current_value) = scope.get(current_value()) { - let pos = get_slider_position(scope, *current_value, min, max); + let pos = get_slider_position(scope, *current_value, params); if (pos - input.cursor.local_pos.x).abs() < 16.0 { drag_start.set(Some((input.cursor.local_pos.x, *current_value))); @@ -224,12 +280,13 @@ impl Widget for Slider { }) .on_event(on_cursor_move(), { to_owned![value]; + let params = scale_params; move |scope, input| { let drag_start = &mut *drag_start.lock_mut(); - let progress = if let Some((start, start_value)) = drag_start { - update_scrubbed(scope, input.local_pos.x - *start, *start_value, min, max) + let progress = if let Some((_start, _start_value)) = drag_start { + update_scrubbed(scope, input.local_pos.x, params) } else { - get_progress_value(scope, input.local_pos.x, min, max) + get_progress_value(scope, input.local_pos.x, params) }; value.send(progress); @@ -270,6 +327,7 @@ struct SliderHandle { max: f32, rect_id: Entity, corner_radius: Unit, + scale_mode: ScaleMode, } impl Widget for SliderHandle { @@ -282,12 +340,23 @@ impl Widget for SliderHandle { rect_size.set(v.map(|v| v.size())); }); + let scale_mode = self.scale_mode; scope.spawn_effect(StreamEffect::new(update, { move |scope: &mut Scope<'_>, (value, size): (V, Option)| { if let Some(size) = size { - let pos = (value.to_progress().clamp(self.min, self.max) - self.min) * size.x - / (self.max - self.min); - + let value_f32 = value.to_progress().clamp(self.min, self.max); + let pos = match scale_mode { + ScaleMode::Linear => { + (value_f32 - self.min) * size.x / (self.max - self.min) + } + ScaleMode::Logarithmic => { + let ln_min = self.min.ln(); + let ln_max = self.max.ln(); + let ln_value = value_f32.max(self.min).ln(); + let norm = (ln_value - ln_min) / (ln_max - ln_min); + norm.clamp(0.0, 1.0) * size.x + } + }; scope.entity().update_dedup(offset(), Unit::px2(pos, 0.0)); } } @@ -311,6 +380,7 @@ struct SliderFill { min: f32, max: f32, rect_id: Entity, + scale_mode: ScaleMode, } impl Widget for SliderFill { @@ -323,16 +393,25 @@ impl Widget for SliderFill { rect_size.set(v.map(|v| v.size())); }); + let scale_mode = self.scale_mode; scope.spawn_effect(StreamEffect::new(update, { move |scope: &mut Scope<'_>, (value, outer_size): (V, Option)| { if let Some(outer_size) = outer_size { - let pos = (value.to_progress().clamp(self.min, self.max) - self.min) - * outer_size.x - / (self.max - self.min); - + let value_f32 = value.to_progress().clamp(self.min, self.max); + let pos = match scale_mode { + ScaleMode::Linear => { + (value_f32 - self.min) * outer_size.x / (self.max - self.min) + } + ScaleMode::Logarithmic => { + let ln_min = self.min.ln(); + let ln_max = self.max.ln(); + let ln_value = value_f32.max(self.min).ln(); + let norm = (ln_value - ln_min) / (ln_max - ln_min); + norm.clamp(0.0, 1.0) * outer_size.x + } + }; let entity = scope.entity(); entity.update_dedup(min_size(), Unit::px2(pos, self.height)); - // scope.entity().update_dedup(offset, Unit::px2(pos, 0.0)); } } })); @@ -436,6 +515,7 @@ impl LabeledSlider max, transform: None, scrub_mode: false, + scale_mode: ScaleMode::Linear, }, editable: false, rounding: None, @@ -461,6 +541,18 @@ impl LabeledSlider self } + /// Set the scale mode (linear or logarithmic) for the inner slider + pub fn with_scale_mode(mut self, mode: ScaleMode) -> Self { + self.slider.scale_mode = mode; + self + } + + /// Enable logarithmic scaling for this slider + pub fn logarithmic(mut self) -> Self { + self.slider.scale_mode = ScaleMode::Logarithmic; + self + } + pub fn with_fill_color(mut self, color: impl Into>) -> Self { self.slider = self.slider.with_fill_color(color); self diff --git a/violet-core/src/widget/scroll.rs b/violet-core/src/widget/scroll.rs index bee4afc..02b0eaa 100644 --- a/violet-core/src/widget/scroll.rs +++ b/violet-core/src/widget/scroll.rs @@ -8,12 +8,13 @@ use crate::{ input::{interactive, on_scroll}, state::{StateStream, StateWrite}, style::{ - default_corner_radius, scrollbar_size, surface_interactive_accent, Background, ResolvableStyle, SizeExt, + default_corner_radius, scrollbar_size, surface_interactive_accent, Background, SizeExt, WidgetSizeProps, }, to_owned, unit::Unit, - utils::zip_latest, Scope, Widget, + utils::zip_latest, + Scope, Widget, }; /// Wraps a widget in a scroll area. @@ -119,14 +120,11 @@ impl Widget for ScrollArea { content_size, }) .with_clip(self.directions) - .with_grow(BVec2::TRUE) + .with_preserve_size(BVec2::TRUE) .mount(scope) } }; - let stylesheet = scope.stylesheet(); - let padding = self.size.padding.unwrap_or_default().resolve(stylesheet); - self.size.mount(scope); Stack::new(( diff --git a/violet-demo/src/widgets.rs b/violet-demo/src/widgets.rs index c59ec55..1d633ea 100644 --- a/violet-demo/src/widgets.rs +++ b/violet-demo/src/widgets.rs @@ -201,8 +201,8 @@ fn inputs() -> impl Widget { dialog( "Input", col(( - LabeledSlider::new(Mutable::new(50), 0, 100), LabeledSlider::new(Mutable::new(50), 0, 100).editable(true), + LabeledSlider::new(Mutable::new(10.0), 0.1, 100.0).editable(true).logarithmic().precision(2), TextInput::new(Mutable::new("Text Input".to_string())), row(( row((0..10) diff --git a/violet-lucide/src/lib.rs b/violet-lucide/src/lib.rs index 9258f99..db1d2c8 100644 --- a/violet-lucide/src/lib.rs +++ b/violet-lucide/src/lib.rs @@ -21,8 +21,7 @@ use icons::LUCIDE_CHEVRON_RIGHT; use violet_core::{style::IconSet, text::Source}; use crate::icons::{ - LUCIDE_CHECK, LUCIDE_CHEVRON_DOWN, LUCIDE_CIRCLE_X, LUCIDE_LIGHTBULB, LUCIDE_SEARCH, - LUCIDE_TRIANGLE_ALERT, + LUCIDE_CHECK, LUCIDE_CHEVRON_DOWN, LUCIDE_CIRCLE_X, LUCIDE_LIGHTBULB, LUCIDE_TRIANGLE_ALERT, }; /// All icon glyphs. @@ -43,6 +42,6 @@ pub fn icon_set() -> IconSet { check: LUCIDE_CHECK.into(), ellipsis: LUCIDE_CHEVRON_DOWN.into(), spinner: ".".into(), - search: LUCIDE_SEARCH.into(), + search: LUCIDE_CHEVRON_RIGHT.into(), } } diff --git a/violet-wgpu/src/app.rs b/violet-wgpu/src/app.rs index dbc48cc..ede0a26 100644 --- a/violet-wgpu/src/app.rs +++ b/violet-wgpu/src/app.rs @@ -82,7 +82,7 @@ impl AppBuilder { /// Add a font to the text system pub fn with_font(mut self, font: Source) -> Self { - self.fonts.push(font); + self.fonts.insert(0, font); self } @@ -113,9 +113,7 @@ impl AppBuilder { AppInstance::new( root, self.allow_resize, - Arc::new(Mutex::new(TextSystem::new_with_fonts( - self.fonts.into_iter().rev(), - ))), + Arc::new(Mutex::new(TextSystem::new_with_fonts(self.fonts))), self.stylesheet .unwrap_or_else(|| StylesheetOptions::new().build()), ) @@ -625,7 +623,7 @@ fn setup_puffin() -> Option { } }; - tracing::info!("Puffin running at {server_addr}"); + tracing::debug!("Puffin running at {server_addr}"); puffin::set_scopes_on(true); Some(server) } diff --git a/violet-wgpu/src/graphics/gpu.rs b/violet-wgpu/src/graphics/gpu.rs index aa6e181..02324ad 100644 --- a/violet-wgpu/src/graphics/gpu.rs +++ b/violet-wgpu/src/graphics/gpu.rs @@ -27,7 +27,7 @@ impl Surface { } pub fn resize(&mut self, gpu: &Gpu, new_size: PhysicalSize) { - tracing::info_span!("resize", ?new_size); + tracing::debug_span!("resize", ?new_size); if Some(new_size) == self.size { return; } diff --git a/violet-wgpu/src/renderer/window_renderer.rs b/violet-wgpu/src/renderer/window_renderer.rs index 1ae0f42..a66d29d 100644 --- a/violet-wgpu/src/renderer/window_renderer.rs +++ b/violet-wgpu/src/renderer/window_renderer.rs @@ -67,7 +67,7 @@ impl WindowRenderer { pub fn draw(&mut self, frame: &mut Frame) -> anyhow::Result<()> { if !self.surface.has_size() { - tracing::info!("No surface size, skipping draw"); + tracing::debug!("No surface size, skipping draw"); return Ok(()); } let target = match self.surface.get_current_texture() { diff --git a/violet-wgpu/src/text.rs b/violet-wgpu/src/text.rs index 9baad55..d76af3e 100644 --- a/violet-wgpu/src/text.rs +++ b/violet-wgpu/src/text.rs @@ -1,7 +1,8 @@ use std::sync::Arc; use cosmic_text::{ - fontdb::Source, Attrs, Buffer, FontSystem, LayoutGlyph, Metrics, Shaping, SwashCache, + fontdb::{Database, Source}, + Attrs, Buffer, FontSystem, LayoutGlyph, Metrics, Shaping, SwashCache, }; use flax::EntityRef; use glam::{vec2, BVec2, Vec2}; @@ -40,7 +41,12 @@ impl TextSystem { } pub fn new_with_fonts(sources: impl IntoIterator) -> Self { - let font_system = FontSystem::new_with_fonts(sources); + let mut font_db = Database::new(); + for source in sources { + font_db.load_font_source(source); + } + + let font_system = FontSystem::new_with_locale_and_db(String::from("en-US"), font_db); Self { font_system, @@ -92,14 +98,14 @@ impl SizeResolver for TextSizeResolver { state, text_system, font_size, - vec2(1.0, args.limits.max_size.y.max(line_height)), + vec2(1.0, args.limits.layout_max_size.y.max(line_height)), ); let (desired, can_grow, preferred_lines) = Self::resolve_text_size( state, text_system, font_size, - args.limits.max_size.max(vec2(10.0, line_height)), + args.limits.layout_max_size.max(vec2(10.0, line_height)), ); // + vec2(5.0, 5.0); @@ -140,10 +146,10 @@ impl SizeResolver for TextSizeResolver { font_size, // Add a little leeway, because an exact fit from the query may miss the last // word/glyph - args.limits.max_size.max(vec2(0.0, line_height)) + vec2(5.0, 5.0), + args.limits.layout_max_size.max(vec2(0.0, line_height)) + vec2(5.0, 5.0), ); - if size.x > args.limits.max_size.x || size.y > args.limits.max_size.y { + if size.x > args.limits.layout_max_size.x || size.y > args.limits.layout_max_size.y { // tracing::error!(%entity, text=?state.text(), %size, %limits.max_size, "Text overflowed"); } @@ -178,7 +184,19 @@ impl TextSizeResolver { buffer.shape_until_scroll(true); - measure(&state.buffer) + let (size, overflow, total_lines) = measure(&state.buffer); + + // Warn if overflow would have occurred with the original height + let max_lines = (layout_size_limit.y / state.buffer.metrics().line_height).floor() as usize; + if total_lines > max_lines { + tracing::warn!( + total_lines, + max_lines, + "Text buffer overflow: some lines would be skipped. Allowing overflow." + ); + } + + (size, overflow, total_lines) } } @@ -255,8 +273,9 @@ impl TextBufferState { )), ) }), - Attrs::new(), + &Attrs::new(), Shaping::Advanced, + None, ); } @@ -272,7 +291,7 @@ impl TextBufferState { let mut current_offset = 0; let mut glyph_index = 0; - let Some(layout) = line.layout_opt().as_ref() else { + let Some(layout) = line.layout_opt() else { return Vec::new(); };