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
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ Other useful one-offs:
- `cargo test -p buiy_core` — fast loop on the core crate.
- `cargo run -p hello_button` — visual smoke test of the Phase 0 widget.
- `cargo run -p hello_text` — visual smoke test of the text stack.
- `cargo run -p hello_bsn` — visual smoke test of the `bsn!` authoring path.
- `cargo run -p capture` — regenerate the README screenshots headlessly (offscreen render-to-texture + GPU readback; needs a real wgpu adapter).
- `BUIY_ACCEPT_SHAPING=1 cargo test -p buiy_core --test text_shaping_snapshots`
— regenerate the `.snap` shaping snapshots (curated: review the diff before
Expand All @@ -92,5 +93,6 @@ Other useful one-offs:
- **Docs entry point:** `docs/README.md` is the master index of specs, plans, reports, and prior-art folders, grouped by area. Read it before adding any new doc or before searching for an existing one. The `organizing-buiy-docs` skill mirrors the conventions for on-demand loading. Cemented in `docs/specs/2026-05-07-docs-organization-design.md`.
- **Prior-art workflow:** the `researching-prior-art` skill drives the 7-stage parallel-agent creation of a `docs/prior-art/<system>/` folder; the `using-prior-art` skill is the consumer-side flow that surfaces relevant folders during spec/plan/review work.
- **Visual-bug verification (`buiy_verify`):** before adding/changing any visual, layout, paint-order, color, or render test — or adding a widget fixture, writing a reftest, or blessing a golden — use the `using-buiy-verification` skill (the task-oriented how-to: pick a tier, add a fixture, run the gates, gotchas). It mirrors the design spec `docs/specs/2026-06-15-buiy-verification-design/` and the crate root doc `crates/buiy_verify/src/lib.rs`. Rule of thumb: add a test at the **lowest tier that can observe the bug** (layout snapshot → display-list snapshot → invariant → reftest → golden); goldens are the last resort for the rasterization residue only. The GPU `--ignored` lane (Tiers 4–5) is additive and must pass on a GPU host; the headless gate must stay green without an adapter.
- **BSN authoring (`buiy_bsn`):** the thin `buiy_bsn` crate re-exports Bevy 0.19's `bsn!` / `bsn_list!` + spawn ext traits (no new syntax); it is reached via `buiy::bsn` and folded into `buiy::prelude`, so `use buiy::prelude::*;` brings `bsn!` into scope. Author the **decomposed components directly** (`bsn! { BoxModel { … } Background(…) }`) — `Style` is a `Bundle` builder, not a Component, so it is not `bsn!`-authorable. Widgets carry `#[require(...)]` contracts; **style them via the parameterized scene-fns in `buiy_widgets`** (`button("…")`, `text_input_*`, re-exported through `buiy::prelude`), never a single-field patch of a `#[require]`'d component (that drops the widget's other defaults — the § 4.1c suppression gotcha). Pin: `docs/specs/2026-06-18-buiy-bsn-integration-design.md`.

_TODO: add language- and project-specific conventions (naming, error handling, testing, serialization, etc.) as they are established._
13 changes: 8 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ buiy_verify = { path = "crates/buiy_verify" }
resolver = "2"
members = [
"crates/buiy",
"crates/buiy_bsn",
"crates/buiy_core",
"crates/buiy_widgets",
"crates/buiy_verify",
"examples/hello_bsn",
"examples/hello_button",
"examples/hello_text",
"examples/capture",
Expand All @@ -44,7 +46,7 @@ rust-version = "1.85"
# accesskit + accesskit_winit (per-window adapter), bevy_picking feature
# on bevy. Still deferred: image-compare (visual harness upgrade is v0.x
# verification work), thiserror (no error-typing pressure yet).
bevy = { version = "0.18", default-features = false, features = ["bevy_render", "bevy_core_pipeline", "bevy_winit", "bevy_window", "bevy_asset", "bevy_log", "bevy_picking", "x11", "wayland"] }
bevy = { version = "0.19.0-rc.3", default-features = false, features = ["bevy_render", "bevy_core_pipeline", "bevy_winit", "bevy_window", "bevy_asset", "bevy_log", "bevy_picking", "bevy_scene", "x11", "wayland"] }
taffy = "0.10"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
Expand All @@ -68,11 +70,12 @@ arboard = { version = "3.6", default-features = false }
tracing = "0.1"
# Phase 0 closeout deps (closeout plan 2026-05-08).
bytemuck = { version = "1", features = ["derive"] }
accesskit = "0.21"
accesskit_winit = "0.29"
accesskit = "0.24"
accesskit_winit = "0.32"
# guillotiere is only a *transitive* dep of bevy_image (not re-exported);
# pin it directly to the version bevy_image 0.18.1 resolves so a bevy patch
# bump cannot drop the atlas allocator. Spec atlas-and-text-seam.md § 2.1.
# pin it directly to the version bevy_image 0.19.0-rc.3 resolves (0.6.2, which
# satisfies bevy_image's "0.6.0" req) so a bevy patch bump cannot drop the atlas
# allocator. Spec atlas-and-text-seam.md § 2.1.
guillotiere = "=0.6.2"
smallvec = "1"
# Tier-5 golden bless ledger (goldens.md § "The bless ledger"): `<slug>.toml`
Expand Down
1 change: 1 addition & 0 deletions crates/buiy/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ description = "A comprehensive UI library for Bevy with web-quality accessibilit

[dependencies]
bevy.workspace = true
buiy_bsn = { path = "../buiy_bsn" }
buiy_core = { path = "../buiy_core" }
buiy_widgets = { path = "../buiy_widgets" }
25 changes: 25 additions & 0 deletions crates/buiy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,31 @@ pub use buiy_core::{
theme::{Theme, UserPreferences, default_light_theme},
};
pub use buiy_widgets::{Button, OnPress, TextInput, WidgetsPlugin};
// Widget BSN scene-fns (the mergeable styled-authoring path): `button(label)`,
// `text_input_single_line(placeholder)`, `text_input_multi_line(placeholder)`.
// They live in `buiy_widgets` (so they reuse the `#[require]` initializer fns —
// one source of truth) and surface here, where the widget + BSN surfaces
// converge, so `use buiy::prelude::*;` brings them in next to `bsn!`. (They are
// NOT re-exported through `buiy_bsn`, which stays widget-agnostic per spec
// § 4.2 — it must not take a `buiy_widgets` dependency.)
pub use buiy_widgets::scene::{button, text_input_multi_line, text_input_single_line};

// BSN authoring (docs/specs/2026-06-18-buiy-bsn-integration-design.md § 4.2).
// `buiy::bsn` is the named path to the authoring crate; the BSN prelude
// (`bsn!`, `bsn_list!`, the spawn extension traits) is folded into the
// `buiy` crate root so the existing `use buiy::*;` convention
// (`hello_button` / `hello_text`) brings `bsn!` into scope — and into the
// `buiy::prelude` module below for the explicit `use buiy::prelude::*;` form.
pub use buiy_bsn as bsn;
pub use buiy_bsn::prelude::*;

/// The Buiy prelude. `use buiy::prelude::*;` brings the common Buiy surface —
/// components, plugins, widgets — and the BSN authoring macros (`bsn!`,
/// `bsn_list!`) + spawn extension traits into scope in one import. Mirrors the
/// flat crate-root re-export the examples use via `use buiy::*;`.
pub mod prelude {
pub use crate::*;
}

// `buiy_core::render::ExtractedDraws` is intentionally NOT re-exported at
// the crate root: it is a render-world resource only, populated during the
Expand Down
74 changes: 74 additions & 0 deletions crates/buiy/tests/plugin.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,74 @@
use bevy::prelude::*;
use buiy::BuiyPlugin;

/// The meta-crate BSN wiring (spec § 4.2): `use buiy::prelude::*;` must bring
/// the `bsn!` macro + the `spawn_scene` extension trait into scope, and a Buiy
/// component re-exported through `buiy` must author through them. Without this
/// wiring, `hello_bsn` (which depends only on `buiy`) could not reach `bsn!`.
#[test]
fn bsn_authoring_is_reachable_through_buiy_prelude() {
use buiy::prelude::*;

let mut app = App::new();
app.add_plugins(MinimalPlugins)
.add_plugins(bevy::asset::AssetPlugin::default())
.add_plugins(bevy::scene::ScenePlugin);

// `Background`, `ColorToken`, `bsn!`, and `spawn_scene` all resolve through
// `buiy::prelude::*` — the single import a downstream user needs.
let id = app
.world_mut()
.spawn_scene(bsn! {
Background { color: { ColorToken::CurrentColor } }
})
.expect("spawn_scene via buiy::prelude")
.id();

assert_eq!(
app.world().get::<Background>(id).expect("Background").color,
ColorToken::CurrentColor,
);
}

/// The widget scene-fns reach `hello_bsn` through `use buiy::prelude::*;` too,
/// and `button(label)` patches MERGE field-wise (the styled-authoring path):
/// patching `width` keeps the canonical padding. This is the form `hello_bsn`
/// uses to author styled widgets.
#[test]
fn widget_scene_fns_merge_through_buiy_prelude() {
use buiy::prelude::*;

let mut app = App::new();
app.add_plugins(MinimalPlugins)
.add_plugins(bevy::asset::AssetPlugin::default())
.add_plugins(bevy::scene::ScenePlugin)
.add_plugins(BuiyPlugin)
.add_plugins(bevy::input::InputPlugin);

// `button` (the scene-fn), `bsn!`, `spawn_scene`, `BoxModel`, `Sizing`,
// `Length` all resolve through `buiy::prelude::*`.
let id = app
.world_mut()
.spawn_scene(bsn! {
button("Save")
BoxModel { width: { Sizing::Length(Length::Px(240.0)) } }
})
.expect("spawn_scene")
.id();

let bm = app.world().get::<BoxModel>(id).expect("BoxModel");
assert_eq!(bm.width, Sizing::Length(Length::Px(240.0)), "width patched");
assert_eq!(
bm.padding,
Edges::all(8.0),
"canonical padding merges through (scene-fn, not the suppression gotcha)"
);
assert_eq!(
app.world().get::<A11yLabel>(id).expect("A11yLabel").0,
"Save"
);
}

#[test]
fn buiy_plugin_loads_in_correct_order() {
let mut app = App::new();
Expand Down Expand Up @@ -64,6 +132,12 @@ fn facade_render_finish_registers_device_resources() {
.add_plugins(bevy::input::InputPlugin)
.add_plugins(BuiyPlugin);
app.init_asset::<Mesh>();
// Bevy 0.19: `CameraPlugin`'s `update_skinned_mesh_bounds` reads
// `Res<Assets<SkinnedMeshInverseBindposes>>` (the second asset `MeshPlugin`
// inits alongside `Mesh`). A missing `Res` silently skipped under 0.18 but
// PANICS under 0.19's param validation. Real apps get it via `DefaultPlugins`
// → `MeshPlugin`; this hand-rolled stack must init it like `Mesh`.
app.init_asset::<bevy::mesh::skinning::SkinnedMeshInverseBindposes>();
app.finish();
app.cleanup();
// The first frame is where the missing-resource param validation fired.
Expand Down
21 changes: 21 additions & 0 deletions crates/buiy_bsn/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "buiy_bsn"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
description = "BSN (Bevy Scene Notation) authoring surface for Buiy: ergonomic re-exports of the bevy_scene `bsn!` macro + spawn extension traits."

[dependencies]
# `bevy.workspace = true` inherits the workspace `bevy_scene` feature
# additively (resolver-2 union) — no per-crate feature edit needed. `bsn!`,
# `bsn_list!`, the `Scene`/`Template` machinery, and the spawn extension traits
# all ship inside the `bevy_scene` crate, reached here via `bevy::scene`.
bevy.workspace = true

[dev-dependencies]
# The round-trip authorability tests author real Buiy components/widgets via
# `bsn!` and spawn them against a `World`, asserting the resulting components.
buiy_core = { path = "../buiy_core" }
buiy_widgets = { path = "../buiy_widgets" }
77 changes: 77 additions & 0 deletions crates/buiy_bsn/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//! `buiy_bsn` — the BSN (Bevy Scene Notation) authoring surface for Buiy.
//!
//! BSN authoring shipped upstream in **Bevy 0.19** (PR #23413) inside the
//! `bevy_scene` crate: the [`bsn!`](bevy::scene::bsn) /
//! [`bsn_list!`](bevy::scene::bsn_list) macros and the
//! `Template` / `Scene` machinery. `bsn!` authoring is **compile-time and
//! reflection-free** — a component is authorable as soon as it is
//! `Component + Clone + Default` (the upstream blanket `Template` impl), which
//! every Buiy component already satisfies by construction. So this crate is
//! intentionally **thin**: it adds *no* new authoring syntax and does *not*
//! wrap or re-skin `bsn!`. The macro vocabulary is Bevy/Buiy component types
//! (Rust identifiers), per the dioxus prior-art lesson — "resist HTML
//! cosmetics; component types are Rust identifiers".
//!
//! Its sole job is **ergonomic re-exports**: it surfaces the BSN macros and
//! the spawn extension traits in a focused [`prelude`] so Buiy users reach
//! BSN authoring without taking a direct `bevy_scene` dependency or learning
//! Bevy's prelude layout. Everything here lives in `bevy::scene` (the
//! `bevy_scene` crate, enabled by the workspace `bevy_scene` feature).
//!
//! See: docs/specs/2026-06-18-buiy-bsn-integration-design.md § 4.
//!
//! # Authoring Buiy components in BSN
//!
//! ```
//! # use bevy::prelude::*;
//! # use bevy::scene::ScenePlugin;
//! use buiy_bsn::prelude::*;
//! use buiy_core::render::components::Background;
//! use buiy_core::layout::BoxModel;
//!
//! # let mut app = App::new();
//! # app.add_plugins((bevy::asset::AssetPlugin::default(), ScenePlugin));
//! // Decomposed style components author directly — no `Style` builder in BSN.
//! let scene = bsn! {
//! Background { color: { buiy_core::render::color::ColorToken::CurrentColor } }
//! BoxModel { }
//! };
//! app.world_mut().spawn_scene(scene).unwrap();
//! ```
//!
//! ## Why `ScenePlugin` is required
//!
//! [`WorldSceneExt::spawn_scene`](bevy::scene::WorldSceneExt::spawn_scene)
//! resolves the scene through the
//! `Assets<ScenePatch>` registry and reads the `AssetServer`, so a `World`
//! that spawns BSN scenes needs both `AssetPlugin` and `bevy::scene::ScenePlugin`
//! added. (Inline `bsn!` does not load any `.bsn` asset file — that loader is
//! deferred upstream — but the spawn path still routes through the asset
//! registry.)

#![forbid(unsafe_code)]

/// The Buiy BSN authoring prelude.
///
/// Glob-import this (`use buiy_bsn::prelude::*;`) to bring the `bsn!` /
/// `bsn_list!` macros and the scene spawn extension traits into scope. These
/// re-export from `bevy::scene` (the `bevy_scene` crate); they are also folded
/// into `buiy::prelude`, so `use buiy::prelude::*;` brings them in too.
pub mod prelude {
/// The BSN authoring macros: [`bsn!`](bevy::scene::bsn) builds a `Scene`,
/// [`bsn_list!`](bevy::scene::bsn_list) builds a `SceneList`.
pub use bevy::scene::{bsn, bsn_list};

/// The scene spawn extension traits. `spawn_scene` / `apply_scene` live on
/// `World`, `Commands`, `EntityWorldMut`, and `EntityCommands` through
/// these traits.
pub use bevy::scene::{
CommandsSceneExt, EntityCommandsSceneExt, EntityWorldMutSceneExt, WorldSceneExt,
};

/// The core scene types + the inline-observer / value helpers used inside
/// `bsn!` literals. `Scene`/`SceneList` are the values the macros expand
/// to; `on` attaches an entity observer; `template_value` inserts a
/// component value from an expression.
pub use bevy::scene::{Scene, SceneList, on, template_value};
}
Loading
Loading