Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,27 @@ tagged release.
system sets), decomposed `BoxModel` / `Display` / `Position` /
`FlexParams` / `FlexItem` components, hybrid `Style` builder that
expands to a `Bundle` on spawn.
- Doc-hidden read-only accessors on `LayoutTree`: `by_entity()` and
`tree_ref()` for integration-test introspection.
- Layout `Overflow` component (per-axis `OverflowMode` + `scrollbar_*`,
`scroll_behavior`, `overscroll_*`). Wired into `taffy::Style.overflow`
and `taffy::Style.scrollbar_width`. Spec:
`docs/specs/2026-05-08-buiy-layout-design/overflow-and-scrolling.md`.
- Layout `Scroll` component (snap-type, snap padding, snap margin) for
scroll-snap container declaration.
- Layout `ScrollOffset` runtime-state component (per-axis scroll
position). Mutation does not invalidate `ResolvedLayout` (asserted by
`tests/layout_scroll_offset_no_invalidate.rs`).
- Layout `ScrollSnapItem` decomposed-only child-side component.
- `Overflow::is_scroll_container()` predicate (spec § 1.2).
- 9 supporting layout enum types: `OverflowMode`, `ScrollbarGutter`,
`ScrollbarWidth`, `ScrollbarColor`, `ScrollBehavior`,
`OverscrollBehavior`, `SnapType`, `SnapAlign`, `SnapStop`.
- `Style` builder: `Overflow` and `Scroll` fields; 12 fluent setters
(`.overflow_x()`, `.overflow_y()`, `.overflow()`, `.overflow_hidden()`,
`.overflow_y_scroll()`, `.overflow_x_scroll()`, `.scrollbar_gutter()`,
`.scrollbar_width()`, `.scroll_behavior()`, `.snap_type()`,
`.snap_padding()`, `.snap_margin()`).

### Changed
- Layout subsystem foundation rewritten. Phase 0's flat `layout.rs` is
Expand All @@ -46,6 +67,9 @@ tagged release.
of `(&Style, &ResolvedLayout)`; entities without `Visual` are skipped
by render. `Button::new` inserts a `Visual` carrying the same theme
tokens Phase 0's `Style` did, so visual appearance is preserved.
- `sync_styles`' change-detection trigger set widens to include
`Changed<Overflow>` and `Changed<Scroll>`; remains exclusive of
`Changed<ScrollOffset>` and `Changed<ScrollSnapItem>`.

### Removed
- `buiy_core::components::Style` (the Phase 0 mega-component) and
Expand Down
4 changes: 3 additions & 1 deletion crates/buiy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ pub use buiy_core::{
layout::{
AlignContent, AlignItems, AspectRatio, BoxModel, BoxSizing, BuiyLayoutStep, Display, Edges,
FlexAxis, FlexGap, FlexItem, FlexParams, FlexWrap, Inset, JustifyContent, LayoutPlugin,
Length, Position, PositionKind, Sizing, Style,
Length, Overflow, OverflowMode, OverscrollBehavior, Position, PositionKind, Scroll,
ScrollBehavior, ScrollOffset, ScrollSnapItem, ScrollbarColor, ScrollbarGutter,
ScrollbarWidth, Sizing, SnapAlign, SnapStop, SnapType, Style,
},
picking::{BuiyPickingBackendPlugin, Hovered},
theme::{Theme, UserPreferences, default_light_theme},
Expand Down
160 changes: 159 additions & 1 deletion crates/buiy_core/src/layout/components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@

use super::types::{
AlignContent, AlignItems, AspectRatio, BoxSizing, Edges, FlexAxis, FlexGap, FlexWrap, Inset,
JustifyContent, PositionKind, Sizing,
JustifyContent, OverflowMode, OverscrollBehavior, PositionKind, ScrollBehavior, ScrollbarColor,
ScrollbarGutter, ScrollbarWidth, Sizing, SnapAlign, SnapStop, SnapType,
};
use bevy::prelude::*;

Expand Down Expand Up @@ -134,6 +135,93 @@ impl Default for FlexItem {
}
}

/// Per-axis overflow handling and scroll/scrollbar configuration. CSS
/// `overflow`, `scrollbar-*`, `scroll-behavior`, `overscroll-behavior`.
///
/// Spec: docs/specs/2026-05-08-buiy-layout-design/overflow-and-scrolling.md § 1.
///
/// Phase 2 wires `x` / `y` and `scrollbar_width` to Taffy. The other
/// fields are stored for downstream consumers (render: `scrollbar_color`,
/// animate: `scroll_behavior`, input: `overscroll_*`). `scrollbar_gutter`
/// is stored but `Stable` does not yet reserve space on non-scrolling
/// containers — see plan coverage map.
#[derive(Component, Reflect, Default, Clone, Debug, PartialEq)]
#[reflect(Component, Default)]
pub struct Overflow {
pub x: OverflowMode,
pub y: OverflowMode,
pub scrollbar_gutter: ScrollbarGutter,
pub scrollbar_width: ScrollbarWidth,
pub scrollbar_color: ScrollbarColor,
pub scroll_behavior: ScrollBehavior,
pub overscroll_x: OverscrollBehavior,
pub overscroll_y: OverscrollBehavior,
}

impl Overflow {
/// True iff either axis is `Scroll` or `Auto`. Scroll containers
/// establish a scroll viewport and a containing block for descendants
/// with `Position::Sticky` (consumer: Phase 7 sub-pass 6a).
///
/// Spec: docs/specs/2026-05-08-buiy-layout-design/overflow-and-scrolling.md § 1.2.
// Called by Phase 7 sticky-positioning pass; unused until that phase lands.
#[allow(dead_code)]
pub fn is_scroll_container(&self) -> bool {
matches!(self.x, OverflowMode::Scroll | OverflowMode::Auto)
|| matches!(self.y, OverflowMode::Scroll | OverflowMode::Auto)
}
}

/// Scroll-snap container settings. CSS `scroll-snap-type`,
/// `scroll-padding`, `scroll-margin`.
///
/// Spec: docs/specs/2026-05-08-buiy-layout-design/overflow-and-scrolling.md § 3.
///
/// Phase 2 stores; the snap-point math runs in
/// `buiy-input-events-design`'s scroll handler.
#[derive(Component, Reflect, Default, Clone, Debug, PartialEq)]
#[reflect(Component, Default)]
pub struct Scroll {
pub snap_type: SnapType,
pub snap_padding: Edges,
pub snap_margin: Edges,
}

/// Runtime scroll position of a scroll container. Mutated by the
/// scroll-input handler in `buiy-input-events-design`. Read by render
/// (drawing) and picking (hit-testing) at consume time, and by Phase 7
/// sub-pass 6a (`StickyOffset`).
///
/// Spec: docs/specs/2026-05-08-buiy-layout-design/overflow-and-scrolling.md § 2.
///
/// **Mutating `ScrollOffset` must NOT invalidate `ResolvedLayout`.** The
/// invariant is enforced by excluding `Changed<ScrollOffset>` from
/// `sync_styles`'s trigger filter (see `systems.rs` line ~80) and
/// asserted by `tests/layout_scroll_offset_no_invalidate.rs`.
#[derive(Component, Reflect, Default, Clone, Copy, Debug, PartialEq)]
#[reflect(Component, Default)]
pub struct ScrollOffset {
pub x: f32,
pub y: f32,
}

/// Per-snap-item child-side configuration. Lives on each child of a
/// scroll container that participates in snap.
///
/// Spec: docs/specs/2026-05-08-buiy-layout-design/overflow-and-scrolling.md § 3.
/// Spec: docs/specs/2026-05-08-buiy-layout-design/architecture.md § 2.4 (decomposed-only convention).
///
/// Decomposed-only — not in `Style`'s Bundle. Following the `FlexItem`
/// pattern: spawn alongside `Style` rather than nested. The snap-point
/// math runs in `buiy-input-events-design`'s scroll handler at consume
/// time; this component stores the per-item declaration.
#[derive(Component, Reflect, Default, Clone, Copy, Debug, PartialEq)]
#[reflect(Component, Default)]
pub struct ScrollSnapItem {
pub align: SnapAlign,
pub stop: SnapStop,
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -185,4 +273,74 @@ mod tests {
assert_eq!(Display::flex_row(), Display::Flex(FlexAxis::Row));
assert_eq!(Display::flex_column(), Display::Flex(FlexAxis::Column));
}

#[test]
fn overflow_default_is_visible_both_axes() {
let o = Overflow::default();
assert_eq!(o.x, OverflowMode::Visible);
assert_eq!(o.y, OverflowMode::Visible);
assert_eq!(o.scrollbar_gutter, ScrollbarGutter::Auto);
assert_eq!(o.scrollbar_width, ScrollbarWidth::Auto);
assert_eq!(o.scrollbar_color, ScrollbarColor::Auto);
assert_eq!(o.scroll_behavior, ScrollBehavior::Auto);
assert_eq!(o.overscroll_x, OverscrollBehavior::Auto);
assert_eq!(o.overscroll_y, OverscrollBehavior::Auto);
}

#[test]
fn overflow_is_scroll_container_only_when_either_axis_scrolls() {
assert!(!Overflow::default().is_scroll_container());
assert!(
!Overflow {
x: OverflowMode::Hidden,
y: OverflowMode::Hidden,
..Default::default()
}
.is_scroll_container()
);
assert!(
Overflow {
x: OverflowMode::Scroll,
..Default::default()
}
.is_scroll_container()
);
assert!(
Overflow {
y: OverflowMode::Auto,
..Default::default()
}
.is_scroll_container()
);
assert!(
Overflow {
x: OverflowMode::Auto,
y: OverflowMode::Scroll,
..Default::default()
}
.is_scroll_container()
);
}

#[test]
fn scroll_default_is_no_snap_zero_padding() {
let s = Scroll::default();
assert_eq!(s.snap_type, SnapType::None);
assert_eq!(s.snap_padding, Edges::ZERO);
assert_eq!(s.snap_margin, Edges::ZERO);
}

#[test]
fn scroll_offset_default_is_origin() {
let s = ScrollOffset::default();
assert_eq!(s.x, 0.0);
assert_eq!(s.y, 0.0);
}

#[test]
fn scroll_snap_item_default_is_none_normal() {
let s = ScrollSnapItem::default();
assert_eq!(s.align, SnapAlign::None);
assert_eq!(s.stop, SnapStop::Normal);
}
}
12 changes: 10 additions & 2 deletions crates/buiy_core/src/layout/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,17 @@ pub(crate) mod translate;
mod tree;
mod types;

pub use components::{BoxModel, Display, FlexItem, FlexParams, Position};
pub use components::{
BoxModel, Display, FlexItem, FlexParams, Overflow, Position, Scroll, ScrollOffset,
ScrollSnapItem,
};
pub use pipeline::BuiyLayoutStep;
pub use style::Style;
pub use tree::LayoutTree;
pub use types::{
AlignContent, AlignItems, AspectRatio, BoxSizing, Edges, FlexAxis, FlexGap, FlexWrap, Inset,
JustifyContent, Length, PositionKind, Sizing,
JustifyContent, Length, OverflowMode, OverscrollBehavior, PositionKind, ScrollBehavior,
ScrollbarColor, ScrollbarGutter, ScrollbarWidth, Sizing, SnapAlign, SnapStop, SnapType,
};

use bevy::prelude::*;
Expand All @@ -33,6 +37,10 @@ impl Plugin for LayoutPlugin {
.register_type::<Position>()
.register_type::<FlexParams>()
.register_type::<FlexItem>()
.register_type::<Overflow>()
.register_type::<Scroll>()
.register_type::<ScrollOffset>()
.register_type::<ScrollSnapItem>()
.register_type::<Edges>()
.register_type::<Sizing>()
.register_type::<Length>()
Expand Down
Loading
Loading