Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
0dd5200
chore: ignore .superpowers/ brainstorming workspace
claude May 7, 2026
47b5c25
spec: add buiy foundation design
claude May 7, 2026
6f8d161
spec: revise foundation design after parallel review
claude May 7, 2026
a5ceaa5
spec: pass-2 review fixes
claude May 7, 2026
3db803c
spec: split foundation design into multi-file folder
claude May 7, 2026
467a357
plan: add Phase 0 foundations implementation plan
claude May 7, 2026
c11d8b8
chore: bootstrap Buiy workspace and crate scaffolding
claude May 7, 2026
fc494fc
fix(workspace): drop nightly-only rustfmt options + sync plan to actu…
claude May 7, 2026
00e2732
feat(core): scaffold CorePlugin and BuiySet system sets
claude May 7, 2026
94b6bc2
feat(core): add Node, Style, ResolvedLayout components
claude May 7, 2026
6a12af9
feat(core): add Theme resource, default light theme, UserPreferences
claude May 7, 2026
69095c5
feat(core): integrate Taffy for layout in BuiySet::Layout
claude May 7, 2026
b9108c2
feat(core): add Focusable + FocusPlugin with Tab handling
claude May 7, 2026
26375aa
docs(focus): document Phase 0 deferred behavior
claude May 7, 2026
03c2f66
feat(core): add A11yRole / A11yLabel / A11yTreeBuilder
claude May 7, 2026
34e7ecb
feat(core): add minimal AABB picking backend
claude May 7, 2026
6ee94d7
fix(picking): derive Reflect on Hovered for resource consistency
claude May 7, 2026
02dae6a
feat(core): scaffold BuiyRenderPlugin with extract phase
claude May 7, 2026
40f8168
fix(render): drop dead main-world init + warn on missing theme tokens
claude May 7, 2026
02485c2
feat(core): add rounded-rect render pipeline + WGSL shader
claude May 7, 2026
dacb653
fix(render): pipeline cleanups + clip-space TODO for Task 11
claude May 7, 2026
7a3d166
feat(core): add Buiy render-graph node into Core2d after EndMainPass
claude May 7, 2026
9928a6e
docs(render-node): explain pre-tonemap insertion + color-attachment c…
claude May 7, 2026
a0044dd
feat(widgets): add Button widget with OnPress message
claude May 7, 2026
589d3de
docs(button): add TODOs flagging Phase 0 deferrals to widget-catalog-…
claude May 7, 2026
799522d
feat(buiy): top-level BuiyPlugin composes sub-plugins
claude May 7, 2026
c4ac71e
fix(buiy): re-export hygiene + InputPlugin contract docs
claude May 7, 2026
9777113
feat(verify): scaffold buiy_verify with module stubs
claude May 7, 2026
1f30d8b
feat(verify): visual regression diff with tolerance
claude May 7, 2026
0a20f9c
fix(verify): widen u32 dims before multiply + #[must_use] DiffResult
claude May 8, 2026
8ff18e5
feat(verify): AccessKit tree snapshot + JSON diff
claude May 8, 2026
1e3386a
fix(verify): a11y snapshot doc honesty + LINT comments + Some-branch …
claude May 8, 2026
efee992
feat(verify): WCAG 2 contrast linter with default-theme baseline
claude May 8, 2026
3751d60
fix(verify): contrast linter polish + real lint_theme test
claude May 8, 2026
5fd7595
feat(example): add hello_button minimal app
claude May 8, 2026
0a24ccf
test: end-to-end verification fixture for hello_button scene
claude May 8, 2026
03ac756
fix(e2e): drop duplicate contrast test + doc honesty + canonicalizer …
claude May 8, 2026
580132a
ci: lint + test workflow on Linux / macOS / Windows
claude May 8, 2026
9b84e0b
fix: v0.x readiness pass — 3 reviewer-flagged pre-merge cleanups
claude May 8, 2026
d72ea2e
fix: apply 7 review-swarm pre-merge fixes
claude May 8, 2026
a6afb09
style: rustfmt the split focus tests
claude May 8, 2026
75c2ba7
ci: install Bevy's Linux system deps in the lint job
claude May 8, 2026
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
55 changes: 55 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: CI

on:
push:
branches: [main]
pull_request:

env:
CARGO_TERM_COLOR: always
RUSTFLAGS: -D warnings

jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
# Bevy's wayland/x11/udev features pull crates whose build.rs runs
# pkg-config; clippy still drives those build scripts even though it
# doesn't link, so the lint job needs the same system libs as test.
- name: Install Linux deps for Bevy
run: |
sudo apt-get update
sudo apt-get install -y libasound2-dev libudev-dev libwayland-dev libxkbcommon-dev
- name: rustfmt
run: cargo fmt --all -- --check
- name: clippy
run: cargo clippy --workspace --all-targets -- -D warnings

test:
name: Test (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Install Linux deps for Bevy
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y libasound2-dev libudev-dev libwayland-dev libxkbcommon-dev xvfb at-spi2-core
- name: cargo test (headless via xvfb on Linux)
if: matrix.os == 'ubuntu-latest'
run: xvfb-run -a cargo test --workspace
- name: cargo test (macOS / Windows)
if: matrix.os != 'ubuntu-latest'
run: cargo test --workspace
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@
.claude/worktrees/

.worktrees/

.superpowers/

target/
Cargo.lock
47 changes: 47 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
[package]
name = "buiy_workspace"
version = "0.0.1"
edition = "2024"
license = "MIT OR Apache-2.0"
publish = false
description = "Workspace root package; exists only to host workspace-level integration tests."

[lib]
path = "src/lib.rs"

[dev-dependencies]
bevy = { workspace = true, features = ["bevy_render", "bevy_winit", "x11", "wayland"] }
buiy = { path = "crates/buiy" }
buiy_core = { path = "crates/buiy_core" }
buiy_verify = { path = "crates/buiy_verify" }

[workspace]
resolver = "2"
members = [
"crates/buiy",
"crates/buiy_core",
"crates/buiy_widgets",
"crates/buiy_verify",
"examples/hello_button",
]

[workspace.package]
version = "0.0.1"
edition = "2024"
license = "MIT OR Apache-2.0"
repository = "https://github.com/intendednull/buiy"
rust-version = "1.85"

[workspace.dependencies]
# Phase 0 keeps the workspace dep set minimal: only crates that have a
# `use` in `src/` are listed. Speculative deps (accesskit, accesskit_winit,
# bevy_picking backend, image-compare for SSIM, thiserror for typed errors)
# return when the consuming task lands. See review notes in
# docs/plans/2026-05-07-buiy-phase-0-foundations.md.
bevy = { version = "0.18", default-features = false, features = ["bevy_render", "bevy_core_pipeline", "bevy_winit", "bevy_window", "bevy_asset", "bevy_log", "x11", "wayland"] }
taffy = "0.10"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
image = "0.25"
proptest = "1"
tracing = "0.1"
1 change: 1 addition & 0 deletions clippy.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
disallowed-methods = []
13 changes: 13 additions & 0 deletions crates/buiy/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "buiy"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
description = "A comprehensive UI library for Bevy with web-quality accessibility."

[dependencies]
bevy.workspace = true
buiy_core = { path = "../buiy_core" }
buiy_widgets = { path = "../buiy_widgets" }
83 changes: 83 additions & 0 deletions crates/buiy/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//! Buiy — comprehensive UI library for Bevy.
//!
//! See: docs/specs/2026-05-07-buiy-foundation/README.md.

use bevy::prelude::*;

pub use buiy_core::{
BuiySet, CorePlugin,
a11y::{A11yDescription, A11yLabel, A11yRole, A11yTreeBuilder},
components::{FlexDirection, Node, ResolvedLayout, Style},
focus::{FocusVisible, Focusable, FocusedEntity},
picking::Hovered,
theme::{Theme, UserPreferences, default_light_theme},
};
pub use buiy_widgets::{Button, OnPress, WidgetsPlugin};

// `buiy_core::render::ExtractedDraws` is intentionally NOT re-exported at
// the crate root: it is a render-world resource only, populated during the
// extract phase. Main-world consumers reading it would see an empty Vec.
// Render-world plugin authors who need it can reach `buiy::buiy_core::render`
// (or depend on `buiy_core` directly) without crate-root surface pollution.

/// Top-level Buiy plugin. Composes sub-plugins in the documented order:
/// core → theme → a11y → focus → input → widgets. Render registration
/// happens in `Plugin::finish` so RenderApp exists when we reach it.
///
/// See: docs/specs/2026-05-07-buiy-foundation/architecture.md § 2.8.
///
/// # Required Bevy plugins
///
/// `BuiyPlugin` requires `bevy::input::InputPlugin`. `DefaultPlugins`
/// includes it; if you build your app with `MinimalPlugins`, add it
/// explicitly:
///
/// ```ignore
/// App::new()
/// .add_plugins(MinimalPlugins)
/// .add_plugins(bevy::input::InputPlugin)
/// .add_plugins(BuiyPlugin)
/// .run();
/// ```
///
/// `FocusPlugin::handle_tab` reads `Res<ButtonInput<KeyCode>>` and the
/// `Button` click handler reads `Res<ButtonInput<MouseButton>>`. Bevy
/// 0.18 panics when a `Res<T>` system param is missing, so the plugin
/// must be present.
///
/// # Plugin order
///
/// Add `BuiyPlugin` **after** Bevy's render plugin (i.e., after
/// `DefaultPlugins`). `BuiyPlugin::finish` registers `BuiyRenderPlugin`,
/// whose `build` reads `PipelineCache` — a resource that
/// `RenderPlugin::finish` inserts. Plugin `finish` runs in registration
/// order, so adding `BuiyPlugin` before `DefaultPlugins` flips the order
/// and panics when `BuiyRenderPlugin` reaches for the missing
/// `PipelineCache`.
pub struct BuiyPlugin;

impl Plugin for BuiyPlugin {
fn build(&self, app: &mut App) {
// Sub-plugin order matches architecture.md § 2.8 documented order:
// core → theme → a11y → focus → input → text → widgets → ...
// Phase 0 omits text / animation / forms / devtools; LayoutPlugin
// and PickingPlugin (which aren't enumerated as sub-plugins in § 2.8
// because their work lives in BuiySet::Layout and BuiySet::Picking)
// are slotted between Focus and Widgets so widgets see resolved
// layout + hit-test results when they run in the same frame.
app.add_plugins((
CorePlugin,
buiy_core::theme::ThemePlugin,
buiy_core::a11y::A11yPlugin,
buiy_core::focus::FocusPlugin,
buiy_core::layout::LayoutPlugin,
buiy_core::picking::PickingPlugin,
WidgetsPlugin,
));
}

fn finish(&self, app: &mut App) {
// RenderApp is guaranteed to exist by `finish` time.
app.add_plugins(buiy_core::render::BuiyRenderPlugin);
}
}
20 changes: 20 additions & 0 deletions crates/buiy/tests/plugin.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use bevy::prelude::*;
use buiy::BuiyPlugin;

#[test]
fn buiy_plugin_loads_in_correct_order() {
let mut app = App::new();
app.add_plugins(MinimalPlugins);
// `BuiyPlugin` composes systems that read keyboard / pointer input
// (focus tab handling, button click). `MinimalPlugins` does not include
// `InputPlugin`, so we add it here so `app.update()` doesn't panic on
// missing `ButtonInput<KeyCode>` / `ButtonInput<MouseButton>` resources.
app.add_plugins(bevy::input::InputPlugin);
app.add_plugins(BuiyPlugin);
app.update();

// Sanity: re-exports are accessible.
let _ = std::any::TypeId::of::<buiy::Button>();
let _ = std::any::TypeId::of::<buiy::Focusable>();
let _ = std::any::TypeId::of::<buiy::A11yRole>();
}
10 changes: 10 additions & 0 deletions crates/buiy_core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
name = "buiy_core"
version.workspace = true
edition.workspace = true
license.workspace = true

[dependencies]
bevy.workspace = true
taffy.workspace = true
tracing.workspace = true
101 changes: 101 additions & 0 deletions crates/buiy_core/src/a11y.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//! AccessKit integration. Phase 0 builds an in-memory snapshot and exposes
//! an `A11yTreeBuilder` that can be serialized for snapshot tests; the real
//! `accesskit_winit::Adapter` wiring per-window happens once Bevy windows
//! are introduced (Task 13, BuiyPlugin).
//!
//! See: docs/specs/2026-05-07-buiy-foundation/architecture.md § 2.6 and
//! accessibility.md § 3.11 (decomposed components per #17644).

use crate::{BuiySet, focus::Focusable};
use bevy::prelude::*;

/// Decomposed AccessKit role component.
#[derive(Component, Reflect, Clone, Copy, Debug, PartialEq, Eq, Default)]
#[reflect(Component)]
pub enum A11yRole {
#[default]
Generic,
Button,
Link,
Image,
Text,
Heading,
Dialog,
AlertDialog,
Tooltip,
// Phase 0 stops here; full taxonomy is in the foundation spec accessibility.md.
}

/// Decomposed accessible name. ACCNAME 1.2 computation is in `buiy-accessibility-design`;
/// Phase 0 is the literal-string fast path.
#[derive(Component, Reflect, Clone, Debug, Default)]
#[reflect(Component)]
pub struct A11yLabel(pub String);

/// Decomposed accessible description.
#[derive(Component, Reflect, Clone, Debug, Default)]
#[reflect(Component)]
pub struct A11yDescription(pub String);

/// One node in the tree as Buiy sees it. Will be translated into
/// `accesskit::Node` by the adapter in Task 13. Decoupled here so we can
/// snapshot it without needing a winit window.
#[derive(Clone, Debug, PartialEq)]
pub struct A11yNodeView {
pub entity: Entity,
pub role: A11yRole,
pub name: String,
pub description: String,
pub focusable: bool,
}

/// Tree builder: rebuilt each frame from changed components in BuiySet::A11yUpdate.
#[derive(Resource, Default)]
pub struct A11yTreeBuilder {
nodes: Vec<A11yNodeView>,
}

impl A11yTreeBuilder {
pub fn snapshot(&self) -> &[A11yNodeView] {
&self.nodes
}
}

pub struct A11yPlugin;

impl Plugin for A11yPlugin {
fn build(&self, app: &mut App) {
app.register_type::<A11yRole>()
.register_type::<A11yLabel>()
.register_type::<A11yDescription>()
.init_resource::<A11yTreeBuilder>()
.add_systems(Update, build_tree.in_set(BuiySet::A11yUpdate));
}
}

#[allow(clippy::type_complexity)]
fn build_tree(
mut builder: ResMut<A11yTreeBuilder>,
q: Query<(
Entity,
Option<&A11yRole>,
Option<&A11yLabel>,
Option<&A11yDescription>,
Option<&Focusable>,
)>,
) {
builder.nodes.clear();
for (entity, role, label, desc, focusable) in q.iter() {
// Skip entities that have no a11y content at all.
if role.is_none() && label.is_none() && desc.is_none() && focusable.is_none() {
continue;
}
builder.nodes.push(A11yNodeView {
entity,
role: role.copied().unwrap_or_default(),
name: label.map(|l| l.0.clone()).unwrap_or_default(),
description: desc.map(|d| d.0.clone()).unwrap_or_default(),
focusable: focusable.is_some(),
});
}
}
Loading
Loading