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}