Skip to main content

bevy_markup/
lib.rs

1//! # bevy_markup — HTML + CSS + Fluent + Tera as Bevy UI
2//!
3//! Write UI as HTML templates; get Bevy UI nodes.
4//!
5//! ```text
6//! .html (Tera template) ──render(TemplateContext)──▶ HTML ──tl──▶ DOM
7//!   ──Fluent (data-l10n-id, ActiveLocale)──▶ localized DOM
8//!   ──CSS (DefaultStylesheet / HtmlStylesheet, FontFamilies)──▶ Bevy UI nodes
9//! ```
10//!
11//! ## Quick start
12//!
13//! ```no_run
14//! use bevy::prelude::*;
15//! use bevy_markup::prelude::*;
16//!
17//! fn main() {
18//!     App::new()
19//!         .add_plugins((DefaultPlugins, BevyMarkupPlugin))
20//!         .add_systems(Startup, setup)
21//!         .run();
22//! }
23//!
24//! fn setup(
25//!     mut commands: Commands,
26//!     asset_server: Res<AssetServer>,
27//!     mut fonts: ResMut<FontFamilies>,
28//! ) {
29//!     commands.spawn(Camera2d);
30//!
31//!     // CSS `font-family: Spectral` → these files.
32//!     fonts.insert(
33//!         "Spectral",
34//!         FontFaces::new(asset_server.load("fonts/Spectral-Regular.ttf"))
35//!             .with_bold(asset_server.load("fonts/Spectral-Bold.ttf")),
36//!     );
37//!     // Applies to every `HtmlUi` without its own `HtmlStylesheet`.
38//!     commands.insert_resource(DefaultStylesheet::new(asset_server.load("ui/theme.css")));
39//!     // `data-l10n-id` attributes resolve against this Fluent bundle.
40//!     commands.insert_resource(ActiveLocale::new(
41//!         asset_server.load("locales/en-US/main.ftl.ron"),
42//!     ));
43//!
44//!     // One component: the template. `Node`, `TemplateContext`, … are required
45//!     // components; override any of them in the same bundle.
46//!     commands.spawn((
47//!         HtmlUi::new(asset_server.load("ui/hud.html")),
48//!         TemplateContext::new().with("player", "Ada").with("coins", &3),
49//!         Node { flex_direction: FlexDirection::Column, ..default() },
50//!     ));
51//! }
52//! ```
53//!
54//! Mutating [`TemplateContext`](html::TemplateContext) re-renders; swapping
55//! [`ActiveLocale`](l10n::ActiveLocale) re-localizes; swapping
56//! [`DefaultStylesheet`](style::DefaultStylesheet) restyles — all at runtime.
57//! After every (re)build an [`HtmlUiBuilt`](html::HtmlUiBuilt) event fires on
58//! the `HtmlUi` entity; use [`HtmlElements`](html::HtmlElements) to find
59//! elements by `id`/`class` and attach behaviour. Style-only changes
60//! (stylesheets, fonts) restyle the existing children in place and fire
61//! [`HtmlUiRestyled`](html::HtmlUiRestyled) instead, keeping what you attached.
62//!
63//! ## Supported subset
64//!
65//! - **HTML** ([`html`]): blocks `h1`–`h6`, `p`, `li`, `pre`, loose text;
66//!   containers `div`, `section`, `ul`, … as nested column nodes; other
67//!   elements are inline text (inside a block) or walked through. Each block
68//!   becomes a `Text` node with a `TextSpan` per styled run; blocks and
69//!   containers carry an [`HtmlElement`](html::HtmlElement) (tag, id, classes).
70//! - **CSS** ([`style`]): type, `.class`, `#id` and compound selectors with
71//!   specificity; `color`, `font-family`, `font-size`,
72//!   `font-weight`, `font-style` (inherited); `border-image` (9-slice),
73//!   `border-width`, `padding`, `background-color` on blocks, containers and
74//!   the `HtmlUi` node (`html` rule, except background); `gap` on containers;
75//!   flex layout, sizes, margins and `box-sizing` on blocks and containers.
76//! - **Fluent** ([`l10n`]): `data-l10n-id` / `data-l10n-args` /
77//!   `data-l10n-name` (fluent-dom convention) on any element; translations
78//!   may contain inline markup.
79//! - **Tera** ([`template`](mod@template)): full Tera 2 syntax in `.html` files, rendered with
80//!   the entity's [`TemplateContext`](html::TemplateContext).
81//! - **9-slice frames**: in CSS via `border-image` (see [`style`]), or for
82//!   nodes outside HTML via `*.slice.ron` assets and
83//!   [`NineSliceFrame`](nine_slice::NineSliceFrame) ([`nine_slice`]).
84//!
85//! ## Cargo features
86//!
87//! - `system_fonts`: fall back to installed system fonts per script (e.g. CJK)
88//!   when the chosen font lacks glyphs.
89
90use bevy::prelude::*;
91use bevy::ui::UiSystems;
92use bevy_fluent::FluentPlugin;
93
94#[cfg(feature = "fuzzing")]
95#[cfg_attr(docsrs, doc(cfg(feature = "fuzzing")))]
96#[doc(hidden)]
97pub mod fuzz;
98
99mod build;
100mod cascade;
101pub mod fonts;
102pub mod html;
103pub mod l10n;
104pub mod nine_slice;
105mod rebuild;
106pub mod style;
107pub mod template;
108
109/// Dependencies whose types appear in this crate's API.
110pub use {bevy_fluent, lightningcss, tera, tl};
111
112/// Everything needed to build HTML UIs: `use bevy_markup::prelude::*;`.
113pub mod prelude {
114    pub use crate::fonts::{FontFaces, FontFamilies, GenericFamily};
115    pub use crate::html::{
116        HtmlDebugOutline, HtmlElement, HtmlElements, HtmlUi, HtmlUiBuilt, HtmlUiRestyled,
117        RenderedHtml, TemplateContext,
118    };
119    pub use crate::l10n::ActiveLocale;
120    pub use crate::nine_slice::{NineSlice, NineSliceFrame};
121    pub use crate::style::{DefaultStylesheet, HtmlStylesheet, Stylesheet};
122    pub use crate::template::HtmlTemplate;
123    pub use crate::{BevyMarkupPlugin, HtmlUiSystems};
124    pub use bevy_fluent::BundleAsset;
125}
126
127/// Adds the asset loaders (`.html`, `.css`, `*.slice.ron`, Fluent's
128/// `*.ftl.ron`), the [`DefaultStylesheet`](style::DefaultStylesheet),
129/// [`ActiveLocale`](l10n::ActiveLocale) and
130/// [`FontFamilies`](fonts::FontFamilies) resources, and the systems that turn
131/// [`HtmlUi`](html::HtmlUi) entities into Bevy UI.
132///
133/// Adds bevy_fluent's `FluentPlugin` unless the app already did.
134#[derive(Default)]
135pub struct BevyMarkupPlugin;
136
137/// Pipeline stages, in `PostUpdate` before Bevy UI layout (chained in this
138/// order). Order your systems against these to see a stage's output the same
139/// frame.
140#[derive(SystemSet, Debug, Clone, Copy, PartialEq, Eq, Hash)]
141pub enum HtmlUiSystems {
142    /// Tera renders templates and `tl` parses them into [`html::RenderedHtml`].
143    Render,
144    /// Fluent resolves `data-l10n-id` elements against [`l10n::ActiveLocale`].
145    Localize,
146    /// The DOM is styled with CSS and spawned as Bevy UI children (then
147    /// [`html::HtmlUiBuilt`] fires), or existing children are restyled in
148    /// place ([`html::HtmlUiRestyled`]).
149    Build,
150}
151
152impl Plugin for BevyMarkupPlugin {
153    fn build(&self, app: &mut App) {
154        if !app.is_plugin_added::<FluentPlugin>() {
155            app.add_plugins(FluentPlugin);
156        }
157        app.init_asset::<template::HtmlTemplate>()
158            .init_asset_loader::<template::HtmlTemplateLoader>()
159            .init_asset::<style::Stylesheet>()
160            .init_asset_loader::<style::StylesheetLoader>()
161            .init_asset::<nine_slice::NineSlice>()
162            .init_asset_loader::<nine_slice::NineSliceLoader>()
163            .init_resource::<style::DefaultStylesheet>()
164            .init_resource::<l10n::ActiveLocale>()
165            .init_resource::<fonts::FontFamilies>()
166            .configure_sets(
167                PostUpdate,
168                (
169                    HtmlUiSystems::Render,
170                    HtmlUiSystems::Localize,
171                    HtmlUiSystems::Build,
172                )
173                    .chain()
174                    .before(UiSystems::Prepare),
175            )
176            .add_systems(
177                PostUpdate,
178                (
179                    html::render_templates.in_set(HtmlUiSystems::Render),
180                    l10n::localize.in_set(HtmlUiSystems::Localize),
181                    build::build_html_ui.in_set(HtmlUiSystems::Build),
182                    nine_slice::apply_nine_slices.before(UiSystems::Prepare),
183                ),
184            );
185    }
186}