use crate::ExtendedUiConfiguration;
use crate::ImageCache;
use crate::html::HtmlStyle;
use crate::services::image_service::get_or_load_image;
use crate::services::state_service::update_widget_states;
use crate::styles::components::UiStyle;
use crate::styles::{
BackdropFilter, BackgroundAttachment, BackgroundPosition, BackgroundPositionValue,
BackgroundSize, BackgroundSizeValue, CalcContext, CalcExpr, FontWeight, GradientStopPosition,
LinearGradient, Style, TextTransform,
};
use crate::widgets::UIWidgetState;
use std::collections::{HashMap, HashSet};
use bevy::asset::RenderAssetUsages;
use bevy::asset::{load_internal_asset, uuid_handle};
use bevy::color::Srgba;
use bevy::core_pipeline::{Core2d, upscaling::upscaling};
use bevy::image::{ImageSampler, TRANSPARENT_IMAGE_HANDLE};
use bevy::prelude::*;
use bevy::reflect::TypePath;
use bevy::render::RenderApp;
use bevy::render::camera::ExtractedCamera;
use bevy::render::extract_resource::{ExtractResource, ExtractResourcePlugin};
use bevy::render::render_asset::RenderAssets;
use bevy::render::render_phase::{
DrawFunctions, PhaseItem, SortedRenderPhase, ViewSortedRenderPhases,
};
use bevy::render::render_resource::{
AsBindGroup, Extent3d, Origin3d, ShaderType, TexelCopyTextureInfo, TextureAspect,
TextureDimension, TextureFormat,
};
use bevy::render::renderer::{RenderContext, ViewQuery};
use bevy::render::texture::GpuImage;
use bevy::render::view::{ExtractedView, RetainedViewEntity, ViewTarget};
use bevy::shader::{Shader, ShaderRef};
use bevy::text::{FontSource, LineHeight};
use bevy::ui::{
ComputedNode, ComputedUiRenderTargetInfo, UiGlobalTransform, UiSystems, UiTransform,
};
use bevy::ui_render::{DrawUiMaterial, TransparentUi, UiCameraView, render_pass::ui_pass};
use bevy::window::{PrimaryWindow, WindowResized};
use once_cell::sync::Lazy;
use std::sync::RwLock;
mod cursor;
mod motion;
mod runtime_flags;
mod transform_cache;
use self::cursor::{CssCursorState, update_css_cursor_icons};
pub use self::motion::{
StyleAnimation, StyleTransition, update_style_animations, update_style_transitions,
};
use self::motion::{
apply_transform_style_if_blocked, resolve_transform_transition, update_style_animation_state,
};
use self::runtime_flags::{StyleRuntimeFlags, style_uses_calc, sync_style_runtime_flags};
pub use self::transform_cache::{LastUiTransform, sync_last_ui_transform};
const BACKDROP_BLUR_SHADER_HANDLE: Handle<Shader> =
uuid_handle!("9d04a8bb-b6cf-4758-bca8-30706480973f");
const MAX_BACKDROP_BLUR_PX: f32 = 80.0;
const SELECTOR_READ_ONLY: u8 = 1 << 0;
const SELECTOR_DISABLED: u8 = 1 << 1;
const SELECTOR_CHECKED: u8 = 1 << 2;
const SELECTOR_FOCUS: u8 = 1 << 3;
const SELECTOR_HOVER: u8 = 1 << 4;
const SELECTOR_INVALID: u8 = 1 << 5;
static SELECTOR_METADATA_CACHE: Lazy<RwLock<HashMap<String, SelectorMetadata>>> =
Lazy::new(|| RwLock::new(HashMap::new()));
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
struct SelectorMetadata {
specificity: u32,
pseudo_flags: u8,
has_pseudo: bool,
skip: bool,
}
pub struct StyleService;
impl Plugin for StyleService {
fn build(&self, app: &mut App) {
load_internal_asset!(
app,
BACKDROP_BLUR_SHADER_HANDLE,
"../../assets/shaders/blur_shader.wgsl",
Shader::from_wgsl
);
app.init_resource::<CssCursorState>();
app.init_resource::<BackdropCaptureState>();
app.add_plugins(ExtractResourcePlugin::<BackdropCaptureState>::default());
app.add_plugins(UiMaterialPlugin::<BackdropBlurMaterial>::default());
#[cfg(not(all(feature = "wasm-default", target_arch = "wasm32")))]
app.add_systems(
PostUpdate,
mark_new_nodes_for_style_refresh.before(update_widget_styles_system),
);
app.add_systems(
PostUpdate,
(
update_widget_styles_system.after(update_widget_states),
update_style_transitions.after(update_widget_styles_system),
update_style_animations.after(update_style_transitions),
apply_calc_styles_system.after(update_style_animations),
apply_background_gradients_system
.after(apply_calc_styles_system)
.after(UiSystems::Layout),
apply_background_images_system
.after(apply_background_gradients_system)
.after(UiSystems::Layout),
ensure_backdrop_capture_texture_system.after(apply_background_images_system),
sync_backdrop_blur_materials_system
.after(ensure_backdrop_capture_texture_system)
.after(apply_background_images_system)
.after(UiSystems::Layout),
propagate_style_inheritance.after(apply_calc_styles_system),
sync_last_ui_transform.after(propagate_style_inheritance),
update_css_cursor_icons.after(update_widget_styles_system),
),
);
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app
.init_resource::<DeferredBackdropUiPhases>()
.add_systems(
bevy::render::Render,
split_backdrop_ui_phase_items_system
.in_set(bevy::render::RenderSystems::PrepareBindGroups)
.after(bevy::ui_render::prepare_uimaterial_nodes::<BackdropBlurMaterial>),
)
.add_systems(
Core2d,
(
backdrop_capture_copy_pass_system
.after(ui_pass)
.before(upscaling),
backdrop_deferred_draw_pass_system
.after(backdrop_capture_copy_pass_system)
.before(upscaling),
),
);
}
}
#[derive(Component)]
pub struct StyleRefreshOnNodeAdded;
#[derive(Component, Debug, Clone, Default)]
pub struct TextTransformState {
source: String,
last_rendered: String,
}
#[derive(Component)]
struct BackgroundGradientApplied;
#[derive(Component)]
struct BackgroundImageApplied;
#[derive(ShaderType, Clone, Copy, Debug, PartialEq)]
struct BackdropBlurUniform {
blur_radius_px: f32,
overlay_alpha: f32,
feedback_compensation: f32,
viewport_size: Vec2,
tint: Vec4,
}
#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
struct BackdropBlurMaterial {
#[uniform(0)]
uniform: BackdropBlurUniform,
#[texture(1)]
#[sampler(2)]
screen_texture: Handle<Image>,
#[texture(3)]
#[sampler(4)]
overlay_texture: Handle<Image>,
}
impl UiMaterial for BackdropBlurMaterial {
fn fragment_shader() -> ShaderRef {
BACKDROP_BLUR_SHADER_HANDLE.into()
}
}
#[derive(Resource, Default, Clone, ExtractResource)]
struct BackdropCaptureState {
screen_texture: Option<Handle<Image>>,
captured_size: UVec2,
captured_format: Option<TextureFormat>,
warmup_frames: u8,
}
#[derive(Resource, Default)]
struct DeferredBackdropUiPhases(HashMap<RetainedViewEntity, SortedRenderPhase<TransparentUi>>);
type UiStyleComponents<'w, 's> = (
Option<Mut<'w, Node>>,
Option<Mut<'w, BackgroundColor>>,
Option<Mut<'w, BorderColor>>,
Option<Mut<'w, BoxShadow>>,
Option<Mut<'w, TextColor>>,
Option<Mut<'w, TextFont>>,
Option<Mut<'w, TextLayout>>,
Option<Mut<'w, ImageNode>>,
Option<Mut<'w, ZIndex>>,
Option<Mut<'w, Pickable>>,
Option<Mut<'w, UiTransform>>,
Option<Mut<'w, LineHeight>>,
Option<Mut<'w, Outline>>,
);
#[cfg(not(all(feature = "wasm-default", target_arch = "wasm32")))]
fn mark_new_nodes_for_style_refresh(
mut commands: Commands,
query: Query<Entity, (Added<Node>, With<UiStyle>)>,
) {
for entity in query.iter() {
commands.entity(entity).insert(StyleRefreshOnNodeAdded);
}
}
pub fn update_widget_styles_system(
mut commands: Commands,
mut query: Query<
(
Entity,
Option<Ref<UIWidgetState>>,
Option<Ref<HtmlStyle>>,
Option<&StyleRefreshOnNodeAdded>,
&mut UiStyle,
),
Or<(
Changed<UiStyle>,
Changed<HtmlStyle>,
Changed<UIWidgetState>,
Added<StyleRefreshOnNodeAdded>,
)>,
>,
mut transition_query: Query<Option<&mut StyleTransition>>,
mut animation_query: Query<Option<&mut StyleAnimation>>,
mut qs: ParamSet<(Query<UiStyleComponents>,)>,
time: Res<Time>,
asset_server: Res<AssetServer>,
mut image_cache: ResMut<ImageCache>,
mut images: ResMut<Assets<Image>>,
) {
for (entity, state_opt, html_style_opt, refresh_on_node_added, mut ui_style) in query.iter_mut()
{
let state_changed = state_opt.as_ref().is_some_and(|state| state.is_changed());
let html_style_changed = html_style_opt
.as_ref()
.is_some_and(|html_style| html_style.is_changed());
let ui_style_changed = ui_style.is_changed();
if state_changed
&& !html_style_changed
&& !ui_style_changed
&& refresh_on_node_added.is_none()
&& ui_style.active_style.is_some()
&& !ui_style_has_stateful_selectors(&ui_style)
{
continue;
}
let state = state_opt.as_deref().cloned().unwrap_or_default();
let node_added = refresh_on_node_added.is_some();
let mut base_styles: Vec<(&String, u32, usize)> = vec![];
let mut pseudo_styles: Vec<(&String, u32, usize)> = vec![];
for (key, style_pair) in &ui_style.styles {
let selector = if style_pair.selector.is_empty() {
key.as_str()
} else {
style_pair.selector.as_str()
};
let metadata = selector_metadata(selector);
if metadata.skip {
continue;
}
if selector_matches_state(metadata, &state) {
if metadata.has_pseudo {
pseudo_styles.push((key, metadata.specificity, style_pair.origin));
} else {
base_styles.push((key, metadata.specificity, style_pair.origin));
}
}
}
sort_style_candidates(&mut base_styles);
sort_style_candidates(&mut pseudo_styles);
let mut final_style = Style::default();
merge_style_candidates(&mut final_style, &ui_style, &base_styles, false);
merge_style_candidates(&mut final_style, &ui_style, &base_styles, true);
if let Some(html_style) = html_style_opt {
final_style.merge(&html_style.0);
}
merge_style_candidates(&mut final_style, &ui_style, &pseudo_styles, false);
merge_style_candidates(&mut final_style, &ui_style, &pseudo_styles, true);
let previous_style = ui_style.active_style.clone();
let has_changed = previous_style.as_ref() != Some(&final_style);
if has_changed {
ui_style.active_style = Some(final_style.clone());
}
sync_style_runtime_flags(&mut commands, entity, &final_style);
if style_requests_outline(&final_style) {
let mut has_outline_component = false;
if let Ok(components) = qs.p0().get_mut(entity) {
has_outline_component = components.12.is_some();
}
if !has_outline_component {
commands
.entity(entity)
.insert(build_outline_from_style(&final_style));
}
}
update_style_animation_state(
&mut commands,
entity,
&final_style,
&ui_style.keyframes,
time.elapsed_secs(),
&mut animation_query,
);
let mut transition = transition_query.get_mut(entity).ok().flatten();
let should_transition =
has_changed && final_style.transition.is_some() && previous_style.is_some();
if should_transition {
let spec = final_style.transition.clone().unwrap_or_default();
let from = previous_style.unwrap_or_default();
let to = final_style.clone();
let copy_spec = spec.clone();
let (from_transform, to_transform) = resolve_transform_transition(&spec, &from, &to);
let transition_state = StyleTransition {
from,
to,
start_time: time.elapsed_secs(),
spec,
from_transform,
to_transform,
current_style: None,
};
if let Some(existing) = transition.as_mut() {
**existing = transition_state;
} else {
commands.entity(entity).insert(transition_state);
}
apply_transform_style_if_blocked(&mut qs, entity, &final_style, ©_spec);
if node_added {
commands.entity(entity).remove::<StyleRefreshOnNodeAdded>();
}
continue;
}
if let Some(transition) = transition.as_mut() {
if !has_changed {
if node_added {
commands.entity(entity).remove::<StyleRefreshOnNodeAdded>();
}
continue;
}
transition.from = previous_style.unwrap_or_default();
transition.to = final_style.clone();
transition.start_time = time.elapsed_secs();
transition.spec = final_style.transition.clone().unwrap_or_default();
let (from_transform, to_transform) =
resolve_transform_transition(&transition.spec, &transition.from, &transition.to);
transition.from_transform = from_transform;
transition.to_transform = to_transform;
apply_transform_style_if_blocked(&mut qs, entity, &final_style, &transition.spec);
if node_added {
commands.entity(entity).remove::<StyleRefreshOnNodeAdded>();
}
continue;
}
if !has_changed && !node_added {
continue;
}
if let Ok(mut components) = qs.p0().get_mut(entity) {
apply_style_components(
&final_style,
&mut components,
&asset_server,
&mut image_cache,
&mut images,
);
}
if node_added {
commands.entity(entity).remove::<StyleRefreshOnNodeAdded>();
}
}
}
fn ui_style_has_stateful_selectors(ui_style: &UiStyle) -> bool {
ui_style.styles.iter().any(|(key, style_pair)| {
let selector = if style_pair.selector.is_empty() {
key.as_str()
} else {
style_pair.selector.as_str()
};
let metadata = selector_metadata(selector);
!metadata.skip && metadata.has_pseudo
})
}
pub fn apply_calc_styles_system(
mut query: Query<
(
Entity,
&UiStyle,
Option<&StyleRuntimeFlags>,
Option<&StyleTransition>,
Option<&StyleAnimation>,
Option<&ChildOf>,
Option<&mut Node>,
),
Or<(
With<StyleRuntimeFlags>,
Changed<UiStyle>,
With<StyleTransition>,
With<StyleAnimation>,
)>,
>,
computed_query: Query<&ComputedNode>,
window_q: Query<&Window, With<PrimaryWindow>>,
) {
let viewport = resolve_layout_viewport(&window_q).unwrap_or_default();
for (_entity, ui_style, flags_opt, transition_opt, animation_opt, parent_opt, node_opt) in
query.iter_mut()
{
let Some(mut node) = node_opt else {
continue;
};
let style = if let Some(transition) = transition_opt {
transition.current_style.as_ref().unwrap_or(&transition.to)
} else if let Some(animation) = animation_opt {
animation.current_style.as_ref().unwrap_or(&animation.base)
} else {
let Some(active) = ui_style.active_style.as_ref() else {
continue;
};
active
};
let needs_calc = flags_opt.is_some_and(|flags| flags.uses_calc) || style_uses_calc(style);
if !needs_calc {
continue;
}
let (content_w, content_h, box_w, box_h) = if let Some(parent) = parent_opt {
if let Ok(parent_node) = computed_query.get(parent.parent()) {
let inv_sf = parent_node.inverse_scale_factor.max(f32::EPSILON);
let size = parent_node.size;
let border = parent_node.border;
let padding = parent_node.padding;
let box_size = Vec2::new(
shrink_axis(size.x, border.min_inset.x, border.max_inset.x),
shrink_axis(size.y, border.min_inset.y, border.max_inset.y),
);
let content = Vec2::new(
shrink_axis(box_size.x, padding.min_inset.x, padding.max_inset.x),
shrink_axis(box_size.y, padding.min_inset.y, padding.max_inset.y),
);
let content = content * inv_sf;
let box_size = box_size * inv_sf;
(content.x, content.y, box_size.x, box_size.y)
} else {
(viewport.x, viewport.y, viewport.x, viewport.y)
}
} else {
(viewport.x, viewport.y, viewport.x, viewport.y)
};
let ctx_content_w = CalcContext {
base: content_w,
viewport,
};
let ctx_content_h = CalcContext {
base: content_h,
viewport,
};
let ctx_box_w = CalcContext {
base: box_w,
viewport,
};
let ctx_box_h = CalcContext {
base: box_h,
viewport,
};
apply_calc_length(style.width_calc.as_ref(), ctx_content_w, &mut node.width);
apply_calc_length(
style.min_width_calc.as_ref(),
ctx_content_w,
&mut node.min_width,
);
apply_calc_length(
style.max_width_calc.as_ref(),
ctx_content_w,
&mut node.max_width,
);
apply_calc_length(style.height_calc.as_ref(), ctx_content_h, &mut node.height);
apply_calc_length(
style.min_height_calc.as_ref(),
ctx_content_h,
&mut node.min_height,
);
apply_calc_length(
style.max_height_calc.as_ref(),
ctx_content_h,
&mut node.max_height,
);
apply_calc_length(style.left_calc.as_ref(), ctx_box_w, &mut node.left);
apply_calc_length(style.right_calc.as_ref(), ctx_box_w, &mut node.right);
apply_calc_length(style.top_calc.as_ref(), ctx_box_h, &mut node.top);
apply_calc_length(style.bottom_calc.as_ref(), ctx_box_h, &mut node.bottom);
if let Some(expr) = style.flex_basis_calc.as_ref() {
let base_main = match node.flex_direction {
FlexDirection::Row | FlexDirection::RowReverse => content_w,
_ => content_h,
};
let ctx_main = CalcContext {
base: base_main,
viewport,
};
if let Some(px) = expr.eval_length(ctx_main) {
node.flex_basis = Val::Px(px);
}
}
let mut row_gap_val = None;
let mut column_gap_val = None;
if let Some(expr) = style.row_gap_calc.as_ref() {
if let Some(px) = expr.eval_length(ctx_content_w) {
row_gap_val = Some(Val::Px(px));
}
}
if let Some(expr) = style.column_gap_calc.as_ref() {
if let Some(px) = expr.eval_length(ctx_content_w) {
column_gap_val = Some(Val::Px(px));
}
}
let needs_row_gap = style.row_gap.is_none() && row_gap_val.is_none();
let needs_column_gap = style.column_gap.is_none() && column_gap_val.is_none();
if (needs_row_gap || needs_column_gap) && style.gap_calc.is_some() {
if let Some(expr) = style.gap_calc.as_ref() {
if let Some(px) = expr.eval_length(ctx_content_w) {
let gap_val = Val::Px(px);
if needs_row_gap {
row_gap_val = Some(gap_val);
}
if needs_column_gap {
column_gap_val = Some(gap_val);
}
}
}
}
if let Some(val) = row_gap_val {
node.row_gap = val;
}
if let Some(val) = column_gap_val {
node.column_gap = val;
}
}
}
#[cfg(all(feature = "wasm-default", target_arch = "wasm32"))]
fn resolve_layout_viewport(window_q: &Query<&Window, With<PrimaryWindow>>) -> Option<Vec2> {
let window = window_q.single().ok()?;
Some(window.resolution.size())
}
#[cfg(all(
feature = "wasm-breakpoints",
not(feature = "wasm-default"),
target_arch = "wasm32"
))]
fn resolve_layout_viewport(_window_q: &Query<&Window, With<PrimaryWindow>>) -> Option<Vec2> {
let window = web_sys::window()?;
let width = window.inner_width().ok()?.as_f64()? as f32;
let height = window.inner_height().ok()?.as_f64()? as f32;
Some(Vec2::new(width, height))
}
#[cfg(all(feature = "wasm-breakpoints", not(target_arch = "wasm32")))]
fn resolve_layout_viewport(window_q: &Query<&Window, With<PrimaryWindow>>) -> Option<Vec2> {
let window = window_q.single().ok()?;
Some(window.resolution.size())
}
#[cfg(all(not(feature = "wasm-breakpoints"), feature = "css-breakpoints"))]
fn resolve_layout_viewport(window_q: &Query<&Window, With<PrimaryWindow>>) -> Option<Vec2> {
let window = window_q.single().ok()?;
Some(window.resolution.size())
}
#[cfg(all(not(feature = "wasm-breakpoints"), not(feature = "css-breakpoints")))]
fn resolve_layout_viewport(_window_q: &Query<&Window, With<PrimaryWindow>>) -> Option<Vec2> {
None
}
#[inline]
fn clear_image_node_texture(img_node: &mut ImageNode) {
if img_node.image.id() != TRANSPARENT_IMAGE_HANDLE.id() {
img_node.image = TRANSPARENT_IMAGE_HANDLE;
}
}
fn apply_background_gradients_system(
mut commands: Commands,
mut query: Query<
(
Entity,
&UiStyle,
Option<&StyleRuntimeFlags>,
Option<&StyleTransition>,
Option<&StyleAnimation>,
&ComputedNode,
Option<&mut ImageNode>,
Option<&BackgroundGradientApplied>,
Option<&BackgroundImageApplied>,
),
Or<(
With<StyleRuntimeFlags>,
Changed<UiStyle>,
With<StyleTransition>,
With<StyleAnimation>,
With<BackgroundGradientApplied>,
With<BackgroundImageApplied>,
)>,
>,
mut resize_events: MessageReader<WindowResized>,
mut image_cache: ResMut<ImageCache>,
mut images: ResMut<Assets<Image>>,
) {
let viewport_is_resizing = resize_events.read().next().is_some();
for (
entity,
ui_style,
flags_opt,
transition_opt,
animation_opt,
computed,
img_node_opt,
gradient_applied,
image_applied,
) in query.iter_mut()
{
let Some(mut img_node) = img_node_opt else {
continue;
};
let style = if let Some(transition) = transition_opt {
transition.current_style.as_ref().unwrap_or(&transition.to)
} else if let Some(animation) = animation_opt {
animation.current_style.as_ref().unwrap_or(&animation.base)
} else {
match ui_style.active_style.as_ref() {
Some(active) => active,
None => {
if gradient_applied.is_some() {
if image_applied.is_none() {
clear_image_node_texture(&mut img_node);
}
commands
.entity(entity)
.remove::<BackgroundGradientApplied>();
}
continue;
}
}
};
let Some(background) = style.background.as_ref() else {
if gradient_applied.is_some() {
if image_applied.is_none() {
clear_image_node_texture(&mut img_node);
}
commands
.entity(entity)
.remove::<BackgroundGradientApplied>();
}
continue;
};
if flags_opt.is_some_and(|flags| !flags.uses_background_gradient)
&& gradient_applied.is_none()
&& transition_opt.is_none()
&& animation_opt.is_none()
{
continue;
}
let Some(gradient) = background.gradient.as_ref() else {
if gradient_applied.is_some() {
if background.image.is_none() {
clear_image_node_texture(&mut img_node);
}
commands
.entity(entity)
.remove::<BackgroundGradientApplied>();
}
continue;
};
if image_applied.is_some() {
commands.entity(entity).remove::<BackgroundImageApplied>();
}
if img_node.color != Color::WHITE {
img_node.color = Color::WHITE;
}
let size = computed.size;
if size.x <= 0.0 || size.y <= 0.0 {
continue;
}
let width = size.x.round().max(1.0) as u32;
let height = size.y.round().max(1.0) as u32;
let size = UVec2::new(width, height);
let inv_sf = computed.inverse_scale_factor.max(f32::EPSILON);
let scale_factor = inv_sf.recip();
let key = gradient_cache_key(gradient, size);
let handle = if let Some(handle) = image_cache.map.get(&key) {
handle.clone()
} else {
if viewport_is_resizing {
continue;
}
let image = render_linear_gradient_image(gradient, size, scale_factor);
let handle = images.add(image);
image_cache.map.insert(key, handle.clone());
handle
};
if img_node.image != handle {
img_node.image = handle;
}
if gradient_applied.is_none() {
commands.entity(entity).insert(BackgroundGradientApplied);
}
}
}
fn apply_background_images_system(
mut commands: Commands,
mut query: Query<
(
Entity,
&UiStyle,
Option<&StyleRuntimeFlags>,
Option<&StyleTransition>,
Option<&StyleAnimation>,
&ComputedNode,
&ComputedUiRenderTargetInfo,
Option<&UiGlobalTransform>,
Option<&mut ImageNode>,
Option<&BackgroundImageApplied>,
Option<&BackgroundGradientApplied>,
),
Or<(
With<StyleRuntimeFlags>,
Changed<UiStyle>,
With<StyleTransition>,
With<StyleAnimation>,
With<BackgroundGradientApplied>,
With<BackgroundImageApplied>,
)>,
>,
mut resize_events: MessageReader<WindowResized>,
asset_server: Res<AssetServer>,
mut image_cache: ResMut<ImageCache>,
mut images: ResMut<Assets<Image>>,
) {
let viewport_is_resizing = resize_events.read().next().is_some();
for (
entity,
ui_style,
flags_opt,
transition_opt,
animation_opt,
computed,
render_target,
global_transform,
img_node_opt,
image_applied,
gradient_applied,
) in query.iter_mut()
{
let Some(mut img_node) = img_node_opt else {
continue;
};
let style = if let Some(transition) = transition_opt {
transition.current_style.as_ref().unwrap_or(&transition.to)
} else if let Some(animation) = animation_opt {
animation.current_style.as_ref().unwrap_or(&animation.base)
} else {
match ui_style.active_style.as_ref() {
Some(active) => active,
None => {
if image_applied.is_some() {
if gradient_applied.is_none() {
clear_image_node_texture(&mut img_node);
}
commands.entity(entity).remove::<BackgroundImageApplied>();
}
continue;
}
}
};
let Some(background) = style.background.as_ref() else {
if image_applied.is_some() {
if gradient_applied.is_none() {
clear_image_node_texture(&mut img_node);
}
commands.entity(entity).remove::<BackgroundImageApplied>();
}
continue;
};
if flags_opt.is_some_and(|flags| !flags.uses_background_image)
&& image_applied.is_none()
&& transition_opt.is_none()
&& animation_opt.is_none()
{
continue;
}
if background.gradient.is_some() {
if image_applied.is_some() {
commands.entity(entity).remove::<BackgroundImageApplied>();
}
continue;
}
let Some(image_path) = background.image.as_ref() else {
if image_applied.is_some() {
clear_image_node_texture(&mut img_node);
commands.entity(entity).remove::<BackgroundImageApplied>();
}
continue;
};
if gradient_applied.is_some() {
commands
.entity(entity)
.remove::<BackgroundGradientApplied>();
}
if img_node.color != Color::WHITE {
img_node.color = Color::WHITE;
}
let size = computed.size;
if size.x <= 0.0 || size.y <= 0.0 {
continue;
}
let container_size = UVec2::new(
size.x.round().max(1.0) as u32,
size.y.round().max(1.0) as u32,
);
let inv_sf = computed.inverse_scale_factor.max(f32::EPSILON);
let scale_factor = inv_sf.recip();
let attachment = style.background_attachment.clone().unwrap_or_default();
let position = style.background_position.clone().unwrap_or_default();
let bg_size = style.background_size.clone().unwrap_or_default();
let source_handle = get_or_load_image(
image_path.as_str(),
&mut image_cache,
&mut images,
&asset_server,
);
let Some(source_image) = images.get(source_handle.id()) else {
continue;
};
let source_size = source_image.size();
if source_size.x == 0 || source_size.y == 0 {
continue;
}
let viewport_size = render_target.physical_size();
let viewport_size = if viewport_size.x == 0 || viewport_size.y == 0 {
container_size
} else {
viewport_size
};
let positioning_size = if matches!(attachment, BackgroundAttachment::Fixed) {
viewport_size
} else {
container_size
};
let draw_size =
resolve_background_draw_size(&bg_size, source_size, positioning_size, scale_factor);
if draw_size.x == 0 || draw_size.y == 0 {
continue;
}
let mut offset =
resolve_background_position(&position, positioning_size, draw_size, scale_factor);
if matches!(attachment, BackgroundAttachment::Fixed) {
if let Some(transform) = global_transform {
let half = size * 0.5;
let top_left = transform.affine().transform_point2(-half);
offset -= top_left;
}
} else if matches!(attachment, BackgroundAttachment::Local) {
offset -= computed.scroll_position;
}
let cache_key = background_image_cache_key(
&source_handle,
container_size,
draw_size,
offset,
attachment,
);
let handle = if let Some(handle) = image_cache.map.get(&cache_key) {
handle.clone()
} else {
if viewport_is_resizing {
continue;
}
let Some(image) =
render_background_image(source_image, container_size, draw_size, offset)
else {
continue;
};
let handle = images.add(image);
image_cache.map.insert(cache_key, handle.clone());
handle
};
if img_node.image != handle {
img_node.image = handle;
}
if image_applied.is_none() {
commands.entity(entity).insert(BackgroundImageApplied);
}
}
}
fn resolved_active_style<'a>(
ui_style: &'a UiStyle,
transition_opt: Option<&'a StyleTransition>,
animation_opt: Option<&'a StyleAnimation>,
) -> Option<&'a Style> {
if let Some(transition) = transition_opt {
return Some(transition.current_style.as_ref().unwrap_or(&transition.to));
}
if let Some(animation) = animation_opt {
return Some(animation.current_style.as_ref().unwrap_or(&animation.base));
}
ui_style.active_style.as_ref()
}
fn sync_backdrop_blur_materials_system(
mut commands: Commands,
window_q: Query<&Window, With<PrimaryWindow>>,
query: Query<
(
Entity,
&UiStyle,
Option<&StyleRuntimeFlags>,
Option<&StyleTransition>,
Option<&StyleAnimation>,
Option<&ImageNode>,
Option<&MaterialNode<BackdropBlurMaterial>>,
),
Or<(
With<StyleRuntimeFlags>,
Changed<UiStyle>,
With<StyleTransition>,
With<StyleAnimation>,
With<MaterialNode<BackdropBlurMaterial>>,
)>,
>,
capture_state: Res<BackdropCaptureState>,
mut materials: ResMut<Assets<BackdropBlurMaterial>>,
) {
let viewport_size = window_q
.single()
.map(|w| w.physical_size().as_vec2())
.unwrap_or(Vec2::ONE)
.max(Vec2::ONE);
for (
entity,
ui_style,
flags_opt,
transition_opt,
animation_opt,
image_node_opt,
material_node_opt,
) in query.iter()
{
if flags_opt.is_some_and(|flags| !flags.uses_backdrop_filter)
&& material_node_opt.is_none()
&& transition_opt.is_none()
&& animation_opt.is_none()
{
continue;
}
let Some(style) = resolved_active_style(ui_style, transition_opt, animation_opt) else {
if material_node_opt.is_some() {
commands
.entity(entity)
.remove::<MaterialNode<BackdropBlurMaterial>>();
}
continue;
};
let blur_radius = match style.backdrop_filter.as_ref() {
Some(BackdropFilter::Blur(radius)) if *radius > 0.0 => radius.min(MAX_BACKDROP_BLUR_PX),
_ => {
if material_node_opt.is_some() {
commands
.entity(entity)
.remove::<MaterialNode<BackdropBlurMaterial>>();
}
continue;
}
};
let tint = style
.background
.as_ref()
.map(|background| background.color.to_linear().to_vec4())
.unwrap_or(Vec4::new(1.0, 1.0, 1.0, 0.0));
let has_overlay = style
.background
.as_ref()
.map(|background| background.gradient.is_some() || background.image.is_some())
.unwrap_or(false);
let uniform = BackdropBlurUniform {
blur_radius_px: blur_radius,
overlay_alpha: if has_overlay { 1.0 } else { 0.0 },
feedback_compensation: 1.0,
viewport_size,
tint,
};
let texture_ready =
capture_state.screen_texture.is_some() && capture_state.warmup_frames == 0;
if !texture_ready && material_node_opt.is_none() {
continue;
}
if let Some(material_node) = material_node_opt {
if let Some(screen_texture) = capture_state.screen_texture.clone() {
let overlay_texture = if has_overlay {
image_node_opt
.map(|img| img.image.clone())
.unwrap_or_else(|| screen_texture.clone())
} else {
screen_texture.clone()
};
let needs_update = materials.get(material_node.id()).is_none_or(|material| {
material.uniform != uniform
|| material.screen_texture != screen_texture
|| material.overlay_texture != overlay_texture
});
if needs_update {
if let Some(mut material) = materials.get_mut(material_node.id()) {
material.uniform = uniform;
material.screen_texture = screen_texture;
material.overlay_texture = overlay_texture;
} else {
let handle = materials.add(BackdropBlurMaterial {
uniform,
screen_texture,
overlay_texture,
});
commands.entity(entity).insert(MaterialNode(handle));
}
}
} else if materials
.get(material_node.id())
.is_some_and(|material| material.uniform != uniform)
{
if let Some(mut material) = materials.get_mut(material_node.id()) {
material.uniform = uniform;
}
}
continue;
}
let Some(screen_texture) = capture_state.screen_texture.clone() else {
continue;
};
let overlay_texture = if has_overlay {
image_node_opt
.map(|img| img.image.clone())
.unwrap_or_else(|| screen_texture.clone())
} else {
screen_texture.clone()
};
let handle = materials.add(BackdropBlurMaterial {
uniform,
screen_texture,
overlay_texture,
});
commands.entity(entity).insert(MaterialNode(handle));
}
}
fn ensure_backdrop_capture_texture_system(
window_q: Query<&Window, With<PrimaryWindow>>,
blur_query: Query<
(
&UiStyle,
Option<&StyleRuntimeFlags>,
Option<&StyleTransition>,
Option<&StyleAnimation>,
Option<&MaterialNode<BackdropBlurMaterial>>,
),
Or<(
With<StyleRuntimeFlags>,
Changed<UiStyle>,
With<StyleTransition>,
With<StyleAnimation>,
With<MaterialNode<BackdropBlurMaterial>>,
)>,
>,
configuration: Res<ExtendedUiConfiguration>,
mut capture_state: ResMut<BackdropCaptureState>,
mut images: ResMut<Assets<Image>>,
) {
let has_backdrop_blur = blur_query.iter().any(
|(ui_style, flags_opt, transition_opt, animation_opt, material_node_opt)| {
if flags_opt.is_some_and(|flags| !flags.uses_backdrop_filter)
&& material_node_opt.is_none()
&& transition_opt.is_none()
&& animation_opt.is_none()
{
return false;
}
resolved_active_style(ui_style, transition_opt, animation_opt).is_some_and(|style| {
matches!(
style.backdrop_filter.as_ref(),
Some(BackdropFilter::Blur(radius)) if *radius > 0.0
)
})
},
);
if !has_backdrop_blur {
capture_state.screen_texture = None;
capture_state.captured_size = UVec2::ZERO;
capture_state.captured_format = None;
capture_state.warmup_frames = 0;
return;
}
let Ok(window) = window_q.single() else {
return;
};
let window_size = window.physical_size();
if window_size == UVec2::ZERO {
return;
}
let texture_format = if configuration.hdr_support {
TextureFormat::Rgba16Float
} else {
TextureFormat::Rgba8UnormSrgb
};
if capture_state.captured_size == window_size
&& capture_state.captured_format == Some(texture_format)
{
capture_state.warmup_frames = capture_state.warmup_frames.saturating_sub(1);
return;
}
let reuse_existing_texture = capture_state.screen_texture.is_some();
let mut image = Image::new_uninit(
Extent3d {
width: window_size.x,
height: window_size.y,
depth_or_array_layers: 1,
},
TextureDimension::D2,
texture_format,
RenderAssetUsages::default(),
);
image.copy_on_resize = reuse_existing_texture;
image.sampler = ImageSampler::linear();
let handle = if let Some(handle) = capture_state.screen_texture.clone() {
if let Some(mut existing) = images.get_mut(handle.id()) {
*existing = image;
handle
} else {
images.add(image)
}
} else {
images.add(image)
};
capture_state.screen_texture = Some(handle);
capture_state.captured_size = window_size;
capture_state.captured_format = Some(texture_format);
capture_state.warmup_frames = 1;
}
fn backdrop_capture_copy_pass_system(
world: &World,
view: ViewQuery<(Entity, &ExtractedCamera, &ViewTarget, Option<&UiCameraView>)>,
mut render_context: RenderContext,
) {
let (view_entity, camera, view_target, ui_camera_view) = view.into_inner();
let ui_view_entity = ui_camera_view.map(|v| v.0).unwrap_or(view_entity);
let Some(ui_view) = world.get::<ExtractedView>(ui_view_entity) else {
return;
};
let deferred_phases = world.resource::<DeferredBackdropUiPhases>();
let Some(phase) = deferred_phases.0.get(&ui_view.retained_view_entity) else {
return;
};
if phase.items.is_empty() {
return;
}
let Some(target_size) = camera.physical_target_size else {
return;
};
if target_size == UVec2::ZERO {
return;
}
let capture_state = world.resource::<BackdropCaptureState>();
let Some(screen_texture) = capture_state.screen_texture.clone() else {
return;
};
let gpu_images = world.resource::<RenderAssets<GpuImage>>();
let Some(gpu_image) = gpu_images.get(screen_texture.id()) else {
return;
};
if view_target.main_texture_format() != gpu_image.texture_descriptor.format {
return;
}
let copy_size = Extent3d {
width: target_size.x.min(gpu_image.texture_descriptor.size.width),
height: target_size.y.min(gpu_image.texture_descriptor.size.height),
depth_or_array_layers: 1,
};
if copy_size.width == 0 || copy_size.height == 0 {
return;
}
render_context.command_encoder().copy_texture_to_texture(
TexelCopyTextureInfo {
texture: view_target.main_texture(),
mip_level: 0,
origin: Origin3d::ZERO,
aspect: TextureAspect::All,
},
TexelCopyTextureInfo {
texture: &gpu_image.texture,
mip_level: 0,
origin: Origin3d::ZERO,
aspect: TextureAspect::All,
},
copy_size,
);
}
fn split_backdrop_ui_phase_items_system(
mut phases: ResMut<ViewSortedRenderPhases<TransparentUi>>,
draw_functions: Res<DrawFunctions<TransparentUi>>,
mut deferred_phases: ResMut<DeferredBackdropUiPhases>,
) {
deferred_phases.0.clear();
let backdrop_draw_function = draw_functions
.read()
.id::<DrawUiMaterial<BackdropBlurMaterial>>();
for (retained_view, phase) in phases.0.iter_mut() {
if phase.items.is_empty() {
continue;
}
let first_backdrop_index = phase
.items
.iter()
.position(|(_, item)| item.draw_function() == backdrop_draw_function);
let Some(first_backdrop_index) = first_backdrop_index else {
continue;
};
let deferred = phase.items.split_off(first_backdrop_index);
if !deferred.is_empty() {
let deferred_keys: HashSet<_> = deferred.keys().copied().collect();
let mut transient_items = Vec::new();
phase.transient_items.retain(|key| {
if deferred_keys.contains(key) {
transient_items.push(*key);
false
} else {
true
}
});
deferred_phases.0.insert(
*retained_view,
SortedRenderPhase {
items: deferred,
transient_items,
},
);
}
}
}
fn backdrop_deferred_draw_pass_system(
world: &World,
view: ViewQuery<(Entity, &ViewTarget, &ExtractedCamera, Option<&UiCameraView>)>,
mut render_context: RenderContext,
) {
let (view_entity, target, camera, ui_camera_view) = view.into_inner();
let ui_view_entity = ui_camera_view.map(|v| v.0).unwrap_or(view_entity);
let Some(ui_view) = world.get::<ExtractedView>(ui_view_entity) else {
return;
};
let deferred_phases = world.resource::<DeferredBackdropUiPhases>();
let Some(phase) = deferred_phases.0.get(&ui_view.retained_view_entity) else {
return;
};
if phase.items.is_empty() {
return;
}
let mut render_pass = render_context.begin_tracked_render_pass(
bevy::render::render_resource::RenderPassDescriptor {
label: Some("backdrop_deferred_ui"),
color_attachments: &[Some(target.get_unsampled_color_attachment())],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
},
);
if let Some(viewport) = camera.viewport.as_ref() {
render_pass.set_camera_viewport(viewport);
}
let _ = phase.render(&mut render_pass, world, ui_view_entity);
}
type StyleCandidate<'a> = (&'a String, u32, usize);
fn sort_style_candidates(candidates: &mut [StyleCandidate<'_>]) {
candidates.sort_by(|a, b| match a.2.cmp(&b.2) {
std::cmp::Ordering::Equal => a.1.cmp(&b.1),
other => other,
});
}
fn merge_style_candidates(
final_style: &mut Style,
ui_style: &UiStyle,
candidates: &[StyleCandidate<'_>],
important: bool,
) {
for (sel, _, _) in candidates {
if let Some(pair) = ui_style.styles.get(*sel) {
if important {
final_style.merge(&pair.important);
} else {
final_style.merge(&pair.normal);
}
}
}
}
fn shrink_axis(size: f32, min_inset: f32, max_inset: f32) -> f32 {
(size - min_inset - max_inset).max(0.0)
}
fn gradient_cache_key(gradient: &LinearGradient, size: UVec2) -> String {
let mut key = format!("__linear-gradient__:{:.4}", gradient.angle);
for stop in &gradient.stops {
let color = stop.color.to_srgba();
let r = (color.red * 255.0).round() as u8;
let g = (color.green * 255.0).round() as u8;
let b = (color.blue * 255.0).round() as u8;
let a = (color.alpha * 255.0).round() as u8;
key.push_str(&format!(":{r:02x}{g:02x}{b:02x}{a:02x}"));
match stop.position {
Some(GradientStopPosition::Percent(value)) => {
key.push_str(&format!("@{value:.4}%"));
}
Some(GradientStopPosition::Px(value)) => {
key.push_str(&format!("@{value:.4}px"));
}
None => key.push_str("@auto"),
}
}
key.push_str(&format!(":{}x{}", size.x, size.y));
key
}
fn background_image_cache_key(
source: &Handle<Image>,
container_size: UVec2,
draw_size: UVec2,
offset: Vec2,
attachment: BackgroundAttachment,
) -> String {
let id = format!("{:?}", source.id());
let offset_x = offset.x.round();
let offset_y = offset.y.round();
format!(
"__background-image__:{id}:{}x{}:{}x{}:{offset_x:.1}:{offset_y:.1}:{attachment:?}",
container_size.x, container_size.y, draw_size.x, draw_size.y
)
}
fn resolve_background_draw_size(
size: &BackgroundSize,
source: UVec2,
area: UVec2,
scale_factor: f32,
) -> UVec2 {
let source_w = source.x.max(1) as f32;
let source_h = source.y.max(1) as f32;
let area_w = area.x.max(1) as f32;
let area_h = area.y.max(1) as f32;
let (target_w, target_h) = match size {
BackgroundSize::Auto => (source_w, source_h),
BackgroundSize::Cover => {
let scale = (area_w / source_w).max(area_h / source_h);
(source_w * scale, source_h * scale)
}
BackgroundSize::Contain => {
let scale = (area_w / source_w).min(area_h / source_h);
(source_w * scale, source_h * scale)
}
BackgroundSize::Explicit(width, height) => {
let mut w = resolve_background_size_value(width, area_w, scale_factor);
let mut h = resolve_background_size_value(height, area_h, scale_factor);
if w.is_none() && h.is_none() {
w = Some(source_w);
h = Some(source_h);
} else if w.is_none() {
let ratio = source_w / source_h;
w = Some(h.unwrap_or(source_h) * ratio);
} else if h.is_none() {
let ratio = source_h / source_w;
h = Some(w.unwrap_or(source_w) * ratio);
}
(w.unwrap_or(source_w), h.unwrap_or(source_h))
}
};
UVec2::new(
target_w.max(1.0).round() as u32,
target_h.max(1.0).round() as u32,
)
}
fn resolve_background_size_value(
value: &BackgroundSizeValue,
area: f32,
scale_factor: f32,
) -> Option<f32> {
match value {
BackgroundSizeValue::Auto => None,
BackgroundSizeValue::Percent(percent) => Some(area * percent / 100.0),
BackgroundSizeValue::Px(px) => Some(px * scale_factor),
}
}
fn resolve_background_position(
position: &BackgroundPosition,
area: UVec2,
image: UVec2,
scale_factor: f32,
) -> Vec2 {
let area_w = area.x as f32;
let area_h = area.y as f32;
let image_w = image.x as f32;
let image_h = image.y as f32;
let x = resolve_background_position_axis(&position.x, area_w, image_w, scale_factor);
let y = resolve_background_position_axis(&position.y, area_h, image_h, scale_factor);
Vec2::new(x, y)
}
fn resolve_background_position_axis(
value: &BackgroundPositionValue,
area: f32,
image: f32,
scale_factor: f32,
) -> f32 {
match value {
BackgroundPositionValue::Percent(percent) => (area - image) * percent / 100.0,
BackgroundPositionValue::Px(px) => px * scale_factor,
}
}
fn render_background_image(
source: &Image,
container_size: UVec2,
draw_size: UVec2,
offset: Vec2,
) -> Option<Image> {
let source_format = source.texture_descriptor.format;
let source_pixel_bytes = match source_format {
TextureFormat::Rgba8Unorm | TextureFormat::Rgba8UnormSrgb => 4usize,
TextureFormat::Rgba16Unorm => 8usize,
_ => return None,
};
let data = source.data.as_ref()?;
let source_size = source.size();
let src_w = source_size.x as usize;
let src_h = source_size.y as usize;
if src_w == 0 || src_h == 0 {
return None;
}
let out_w = container_size.x as usize;
let out_h = container_size.y as usize;
if out_w == 0 || out_h == 0 {
return None;
}
let out_len = out_w.checked_mul(out_h)?.checked_mul(4)?;
let mut out = vec![0u8; out_len];
let draw_w = draw_size.x as i32;
let draw_h = draw_size.y as i32;
if draw_w <= 0 || draw_h <= 0 {
return None;
}
let offset_x = offset.x.round() as i32;
let offset_y = offset.y.round() as i32;
let out_w_i32 = out_w as i32;
let out_h_i32 = out_h as i32;
let start_x = offset_x.max(0).min(out_w_i32);
let start_y = offset_y.max(0).min(out_h_i32);
let end_x = (offset_x + draw_w).max(0).min(out_w_i32);
let end_y = (offset_y + draw_h).max(0).min(out_h_i32);
if start_x < end_x && start_y < end_y {
let visible_w = (end_x - start_x) as usize;
let visible_h = (end_y - start_y) as usize;
let draw_w_usize = draw_w as usize;
let draw_h_usize = draw_h as usize;
let start_draw_x = (start_x - offset_x) as usize;
let start_draw_y = (start_y - offset_y) as usize;
let start_x_usize = start_x as usize;
let start_y_usize = start_y as usize;
let x_map: Vec<usize> = (0..visible_w)
.map(|i| {
let draw_x = start_draw_x + i;
(((draw_x * 2 + 1) * src_w) / (draw_w_usize * 2)).min(src_w - 1)
})
.collect();
let y_map: Vec<usize> = (0..visible_h)
.map(|i| {
let draw_y = start_draw_y + i;
(((draw_y * 2 + 1) * src_h) / (draw_h_usize * 2)).min(src_h - 1)
})
.collect();
for (row_index, src_y) in y_map.iter().copied().enumerate() {
let dest_y = start_y_usize + row_index;
let dst_row_start = (dest_y * out_w + start_x_usize) * 4;
let dst_row_end = dst_row_start + visible_w * 4;
let dst_row = &mut out[dst_row_start..dst_row_end];
for (col_index, src_x) in x_map.iter().copied().enumerate() {
let src_idx = (src_y * src_w + src_x) * source_pixel_bytes;
let dst_idx = col_index * 4;
if source_pixel_bytes == 4 {
dst_row[dst_idx..dst_idx + 4].copy_from_slice(&data[src_idx..src_idx + 4]);
} else {
for channel in 0..4 {
let base = src_idx + channel * 2;
let value = u16::from_le_bytes([data[base], data[base + 1]]);
dst_row[dst_idx + channel] = (value >> 8) as u8;
}
}
}
}
}
let mut image = Image::new(
Extent3d {
width: out_w as u32,
height: out_h as u32,
depth_or_array_layers: 1,
},
TextureDimension::D2,
out,
source_format,
RenderAssetUsages::default(),
);
image.sampler = ImageSampler::linear();
Some(image)
}
fn render_linear_gradient_image(
gradient: &LinearGradient,
size: UVec2,
scale_factor: f32,
) -> Image {
let width = size.x.max(1) as usize;
let height = size.y.max(1) as usize;
let width_f = width as f32;
let height_f = height as f32;
let angle = gradient.angle.to_radians();
let direction = Vec2::new(angle.sin(), -angle.cos());
let line_length = (direction.x.abs() * width_f + direction.y.abs() * height_f).max(1.0);
let stops = resolve_gradient_stops(gradient, line_length, scale_factor);
let mut data = Vec::with_capacity(width * height * 4);
let half_w = width_f / 2.0;
let half_h = height_f / 2.0;
for y in 0..height {
let fy = y as f32 + 0.5 - half_h;
for x in 0..width {
let fx = x as f32 + 0.5 - half_w;
let projection = fx * direction.x + fy * direction.y;
let t = ((projection + line_length / 2.0) / line_length).clamp(0.0, 1.0);
let color = sample_gradient_color(&stops, t);
data.push((color.red * 255.0).round() as u8);
data.push((color.green * 255.0).round() as u8);
data.push((color.blue * 255.0).round() as u8);
data.push((color.alpha * 255.0).round() as u8);
}
}
let mut image = Image::new(
Extent3d {
width: width as u32,
height: height as u32,
depth_or_array_layers: 1,
},
TextureDimension::D2,
data,
TextureFormat::Rgba8UnormSrgb,
RenderAssetUsages::default(),
);
image.sampler = ImageSampler::linear();
image
}
#[derive(Clone, Copy)]
struct ResolvedGradientStop {
color: Srgba,
position: f32,
}
fn resolve_gradient_stops(
gradient: &LinearGradient,
line_length: f32,
scale_factor: f32,
) -> Vec<ResolvedGradientStop> {
let mut stops: Vec<(Srgba, Option<f32>)> = gradient
.stops
.iter()
.map(|stop| {
let position = match stop.position {
Some(GradientStopPosition::Percent(value)) => Some(value / 100.0),
Some(GradientStopPosition::Px(value)) => Some((value * scale_factor) / line_length),
None => None,
};
(stop.color.to_srgba(), position)
})
.collect();
if stops.is_empty() {
return Vec::new();
}
if stops.len() == 1 {
let color = stops[0].0;
return vec![
ResolvedGradientStop {
color,
position: 0.0,
},
ResolvedGradientStop {
color,
position: 1.0,
},
];
}
if stops.first().and_then(|stop| stop.1).is_none() {
stops[0].1 = Some(0.0);
}
if stops.last().and_then(|stop| stop.1).is_none() {
let last = stops.len() - 1;
stops[last].1 = Some(1.0);
}
let mut i = 0usize;
while i < stops.len() {
if stops[i].1.is_some() {
i += 1;
continue;
}
let start = i - 1;
let mut end = i;
while end < stops.len() && stops[end].1.is_none() {
end += 1;
}
let start_pos = stops[start].1.unwrap();
let end_pos = stops[end].1.unwrap_or(start_pos);
let span = (end - start) as f32;
let step = if span > 0.0 {
(end_pos - start_pos) / span
} else {
0.0
};
for idx in i..end {
stops[idx].1 = Some(start_pos + step * (idx - start) as f32);
}
i = end;
}
let mut resolved: Vec<ResolvedGradientStop> = Vec::with_capacity(stops.len());
let mut prev = stops[0].1.unwrap_or(0.0);
resolved.push(ResolvedGradientStop {
color: stops[0].0,
position: prev,
});
for (color, pos_opt) in stops.into_iter().skip(1) {
let mut pos = pos_opt.unwrap_or(prev);
if pos < prev {
pos = prev;
}
resolved.push(ResolvedGradientStop {
color,
position: pos,
});
prev = pos;
}
resolved
}
fn sample_gradient_color(stops: &[ResolvedGradientStop], t: f32) -> Srgba {
if stops.is_empty() {
return Srgba::new(0.0, 0.0, 0.0, 0.0);
}
if t <= stops[0].position {
return stops[0].color;
}
for window in stops.windows(2) {
let left = window[0];
let right = window[1];
if t <= right.position {
let span = (right.position - left.position).max(f32::EPSILON);
let local = ((t - left.position) / span).clamp(0.0, 1.0);
return lerp_srgba(left.color, right.color, local);
}
}
stops.last().map(|stop| stop.color).unwrap_or_default()
}
fn lerp_srgba(a: Srgba, b: Srgba, t: f32) -> Srgba {
Srgba {
red: lerp(a.red, b.red, t),
green: lerp(a.green, b.green, t),
blue: lerp(a.blue, b.blue, t),
alpha: lerp(a.alpha, b.alpha, t),
}
}
fn lerp(from: f32, to: f32, t: f32) -> f32 {
from + (to - from) * t
}
fn apply_calc_length(expr: Option<&CalcExpr>, ctx: CalcContext, target: &mut Val) {
if let Some(expr) = expr {
if let Some(px) = expr.eval_length(ctx) {
*target = Val::Px(px);
}
}
}
fn apply_style_components(
style: &Style,
components: &mut UiStyleComponents,
asset_server: &AssetServer,
_image_cache: &mut ImageCache,
_images: &mut Assets<Image>,
) {
if let Some(node) = components.0.as_mut() {
apply_style_to_node(style, Some(node.as_mut()));
} else {
apply_style_to_node(style, None);
}
if let Some(bg) = components.1.as_mut() {
bg.0 = style
.background
.as_ref()
.map(|b| b.color)
.unwrap_or(Color::NONE);
}
if let Some(bc) = components.2.as_mut() {
bc.set_all(style.border_color.unwrap_or(Color::NONE));
}
if let Some(bs) = components.3.as_mut() {
bs.0 = style.box_shadow.as_ref().cloned().unwrap_or_default().0;
}
if let Some(tc) = components.4.as_mut() {
tc.0 = style.color.unwrap_or(Color::WHITE);
}
if let Some(tf) = components.5.as_mut() {
if let Some(font_size) = style.font_size.clone() {
tf.font_size = font_size;
}
if let Some(font_family) = style.font_family.as_ref() {
let font_path_str = font_family.0.to_string();
if font_path_str.eq_ignore_ascii_case("default") {
tf.font = Default::default();
} else if font_path_str.ends_with(".ttf") {
tf.font = FontSource::Handle(asset_server.load(font_path_str));
} else {
let folder = font_path_str.trim().trim_matches('"').trim_matches('\'');
if folder.is_empty() {
tf.font = Default::default();
} else {
let weight_opt = style.font_weight.clone();
tf.font = FontSource::Handle(load_weighted_font_from_folder(
asset_server,
folder,
weight_opt,
));
}
}
}
}
if let Some(line_height) = components.11.as_mut() {
if let Some(style_line_height) = style.line_height {
**line_height = style_line_height;
}
}
if let Some(outline) = components.12.as_mut() {
**outline = build_outline_from_style(style);
}
if let Some(tl) = components.6.as_mut() {
if let Some(text_align) = style.text_align {
tl.justify = text_align;
}
if let Some(text_wrap) = style.text_wrap {
tl.linebreak = text_wrap;
}
}
if let Some(zi) = components.8.as_mut() {
zi.0 = style.z_index.unwrap_or(0);
}
if let Some(pick) = components.9.as_mut() {
let old_pick = pick.clone();
let new_pick = style.pointer_events.as_ref().cloned().unwrap_or(Pickable {
is_hoverable: old_pick.is_hoverable,
should_block_lower: old_pick.should_block_lower,
});
**pick = new_pick;
}
if let Some(transform) = components.10.as_mut() {
apply_transform_style(style, transform);
}
}
fn apply_transform_style(style: &Style, transform: &mut UiTransform) {
if style.transform.is_empty() {
*transform = UiTransform::default();
return;
}
let mut next = UiTransform::default();
if let Some(translation) = style.transform.translation {
next.translation = translation;
}
if let Some(x) = style.transform.translation_x {
next.translation.x = x;
}
if let Some(y) = style.transform.translation_y {
next.translation.y = y;
}
if let Some(scale) = style.transform.scale {
next.scale = scale;
}
if let Some(scale_x) = style.transform.scale_x {
next.scale.x = scale_x;
}
if let Some(scale_y) = style.transform.scale_y {
next.scale.y = scale_y;
}
if let Some(rotation) = style.transform.rotation {
next.rotation = Rot2::radians(rotation);
}
*transform = next;
}
fn style_requests_outline(style: &Style) -> bool {
style.outline_width.is_some() || style.outline_offset.is_some() || style.outline_color.is_some()
}
fn build_outline_from_style(style: &Style) -> Outline {
let default_outline = Outline::default();
Outline {
width: style.outline_width.unwrap_or(default_outline.width),
offset: style.outline_offset.unwrap_or(default_outline.offset),
color: style.outline_color.unwrap_or(default_outline.color),
}
}
fn selector_metadata(selector: &str) -> SelectorMetadata {
if let Some(cached) = SELECTOR_METADATA_CACHE
.read()
.ok()
.and_then(|cache| cache.get(selector).copied())
{
return cached;
}
let metadata = compute_selector_metadata(selector);
if let Ok(mut cache) = SELECTOR_METADATA_CACHE.write() {
cache.insert(selector.to_string(), metadata);
}
metadata
}
fn compute_selector_metadata(selector: &str) -> SelectorMetadata {
if selector.contains("::") {
return SelectorMetadata {
skip: true,
..Default::default()
};
}
let mut specificity = 0;
let mut pseudo_flags = 0;
let mut has_pseudo = false;
for part in selector.replace('>', " > ").split_whitespace() {
if part == ">" {
continue;
}
let segments: Vec<&str> = part.split(':').collect();
let base = segments[0];
specificity += if base.starts_with('#') {
100
} else if base.starts_with('.') {
10
} else if base == "*" || base.is_empty() {
0
} else {
1
};
if segments.len() > 1 {
has_pseudo = true;
specificity += segments.len().saturating_sub(1) as u32;
}
for pseudo in &segments[1..] {
match *pseudo {
"read-only" => pseudo_flags |= SELECTOR_READ_ONLY,
"disabled" => pseudo_flags |= SELECTOR_DISABLED,
"checked" => pseudo_flags |= SELECTOR_CHECKED,
"focus" => pseudo_flags |= SELECTOR_FOCUS,
"hover" => pseudo_flags |= SELECTOR_HOVER,
"invalid" => pseudo_flags |= SELECTOR_INVALID,
_ => {}
}
}
}
SelectorMetadata {
specificity,
pseudo_flags,
has_pseudo,
skip: false,
}
}
fn selector_matches_state(metadata: SelectorMetadata, state: &UIWidgetState) -> bool {
if metadata.pseudo_flags & SELECTOR_READ_ONLY != 0 && !state.readonly {
return false;
}
if metadata.pseudo_flags & SELECTOR_DISABLED != 0 && !state.disabled {
return false;
}
if metadata.pseudo_flags & SELECTOR_CHECKED != 0 && (state.disabled || !state.checked) {
return false;
}
if metadata.pseudo_flags & SELECTOR_FOCUS != 0 && (state.disabled || !state.focused) {
return false;
}
if metadata.pseudo_flags & SELECTOR_HOVER != 0 && (state.disabled || !state.hovered) {
return false;
}
if metadata.pseudo_flags & SELECTOR_INVALID != 0 && !state.invalid {
return false;
}
true
}
fn apply_style_to_node(style: &Style, node: Option<&mut Node>) {
if let Some(node) = node {
node.width = style.width.unwrap_or_default();
node.min_width = style.min_width.unwrap_or_default();
node.max_width = style.max_width.unwrap_or_default();
node.height = style.height.unwrap_or_default();
node.min_height = style.min_height.unwrap_or_default();
node.max_height = style.max_height.unwrap_or_default();
node.display = style.display.unwrap_or_default();
node.position_type = style.position_type.unwrap_or_default();
node.left = style.left.unwrap_or_default();
node.top = style.top.unwrap_or_default();
node.right = style.right.unwrap_or_default();
node.bottom = style.bottom.unwrap_or_default();
node.padding = style.padding.unwrap_or_default();
node.margin = style.margin.unwrap_or_default();
node.border = style.border.unwrap_or_default();
let mut br = node.border_radius;
if let Some(radius) = style.border_radius.clone() {
br.top_left = radius.top_left;
br.top_right = radius.top_right;
br.bottom_left = radius.bottom_left;
br.bottom_right = radius.bottom_right;
} else {
br.top_left = Val::ZERO;
br.top_right = Val::ZERO;
br.bottom_left = Val::ZERO;
br.bottom_right = Val::ZERO;
}
node.border_radius = br;
node.justify_content = style.justify_content.unwrap_or_default();
node.align_items = style.align_items.unwrap_or_default();
node.overflow = style.overflow.unwrap_or_default();
node.flex_direction = style.flex_direction.unwrap_or(FlexDirection::Row);
let row_gap = style.row_gap.or(style.gap).unwrap_or_default();
let column_gap = style.column_gap.or(style.gap).unwrap_or_default();
node.row_gap = row_gap;
node.column_gap = column_gap;
node.flex_grow = style.flex_grow.unwrap_or_default();
node.flex_basis = style.flex_basis.unwrap_or_default();
node.flex_shrink = style.flex_shrink.unwrap_or_default();
node.flex_wrap = style.flex_wrap.unwrap_or_default();
node.grid_row = style.grid_row.unwrap_or_default();
node.grid_column = style.grid_column.unwrap_or_default();
node.grid_auto_flow = style.grid_auto_flow.unwrap_or_default();
node.grid_template_rows = style.grid_template_rows.clone().unwrap_or_default();
node.grid_template_columns = style.grid_template_columns.clone().unwrap_or_default();
node.grid_auto_columns = style.grid_auto_columns.clone().unwrap_or_default();
node.grid_auto_rows = style.grid_auto_rows.clone().unwrap_or_default();
}
}
fn load_weighted_font_from_folder(
asset_server: &AssetServer,
folder: &str,
weight: Option<FontWeight>,
) -> Handle<Font> {
let folder = folder
.trim()
.trim_matches('"')
.trim_matches('\'')
.trim_end_matches('/');
if folder.is_empty() {
return Default::default();
}
let family = folder_basename(folder);
let w = weight.unwrap_or(FontWeight::Normal);
let token = weight_token_exact(w);
let path_primary = format!("{folder}/{family}-{token}.ttf");
asset_server.load::<Font>(path_primary)
}
fn weight_token_exact(weight: FontWeight) -> &'static str {
match weight {
FontWeight::Thin => "Thin",
FontWeight::ExtraLight => "ExtraLight",
FontWeight::Light => "Light",
FontWeight::Normal => "Regular",
FontWeight::Medium => "Medium",
FontWeight::SemiBold => "SemiBold",
FontWeight::Bold => "Bold",
FontWeight::ExtraBold => "ExtraBold",
FontWeight::Black => "Black",
}
}
fn folder_basename(folder: &str) -> &str {
folder
.trim_end_matches('/')
.rsplit('/')
.next()
.unwrap_or(folder)
}
pub fn propagate_style_inheritance(
mut commands: Commands,
mut qs: ParamSet<(
Query<
Entity,
Or<(
Added<UiStyle>,
Changed<UiStyle>,
Added<StyleTransition>,
Changed<StyleTransition>,
Added<StyleAnimation>,
Changed<StyleAnimation>,
Added<ChildOf>,
Changed<ChildOf>,
Added<Text>,
Changed<Text>,
Added<TextColor>,
Added<TextFont>,
Added<LineHeight>,
Added<ImageNode>,
Added<TextShadow>,
)>,
>,
Query<(
Option<&mut TextColor>,
Option<&mut TextFont>,
Option<&mut LineHeight>,
Option<&mut ImageNode>,
Option<&mut TextShadow>,
Option<&mut Text>,
Option<&mut TextTransformState>,
)>,
)>,
parent_query: Query<&ChildOf>,
children_query: Query<&Children>,
style_query: Query<&UiStyle>,
style_state_query: Query<(Option<&StyleTransition>, Option<&StyleAnimation>)>,
asset_server: Res<AssetServer>,
) {
let dirty_entities: Vec<Entity> = qs.p0().iter().collect();
if dirty_entities.is_empty() {
return;
}
let dirty_set: HashSet<Entity> = dirty_entities.iter().copied().collect();
let mut target_query = qs.p1();
for entity in dirty_entities {
let mut current = entity;
let mut has_dirty_ancestor = false;
while let Ok(parent) = parent_query.get(current) {
let parent_entity = parent.parent();
if dirty_set.contains(&parent_entity) {
has_dirty_ancestor = true;
break;
}
current = parent_entity;
}
if has_dirty_ancestor {
continue;
}
let inherited_style = resolve_inherited_style_for_entity(
entity,
&parent_query,
&style_query,
&style_state_query,
);
propagate_recursive(
entity,
inherited_style.as_ref(),
&mut commands,
&children_query,
&style_query,
&style_state_query,
&mut target_query,
&asset_server,
);
}
}
fn resolve_inherited_style_for_entity(
entity: Entity,
parent_query: &Query<&ChildOf>,
style_query: &Query<&UiStyle>,
style_state_query: &Query<(Option<&StyleTransition>, Option<&StyleAnimation>)>,
) -> Option<Style> {
let mut lineage = Vec::new();
let mut current = entity;
while let Ok(parent) = parent_query.get(current) {
let parent_entity = parent.parent();
lineage.push(parent_entity);
current = parent_entity;
}
if lineage.is_empty() {
return None;
}
let mut inherited_style = Style::default();
let mut has_inherited = false;
for ancestor in lineage.into_iter().rev() {
merge_entity_style_for_propagation(
ancestor,
&mut inherited_style,
style_query,
style_state_query,
);
has_inherited = true;
}
has_inherited.then_some(inherited_style)
}
fn merge_entity_style_for_propagation(
entity: Entity,
style_to_propagate: &mut Style,
style_query: &Query<&UiStyle>,
style_state_query: &Query<(Option<&StyleTransition>, Option<&StyleAnimation>)>,
) {
if let Ok(ui_style) = style_query.get(entity) {
if let Some(active_style) = ui_style.active_style.as_ref() {
style_to_propagate.merge(active_style);
}
}
if let Ok((transition_opt, animation_opt)) = style_state_query.get(entity) {
if let Some(transition) = transition_opt {
if let Some(current) = &transition.current_style {
style_to_propagate.merge(current);
}
}
if let Some(animation) = animation_opt {
if let Some(current) = &animation.current_style {
style_to_propagate.merge(current);
}
}
}
}
fn propagate_recursive(
entity: Entity,
inherited_style: Option<&Style>,
commands: &mut Commands,
children_query: &Query<&Children>,
style_query: &Query<&UiStyle>,
style_state_query: &Query<(Option<&StyleTransition>, Option<&StyleAnimation>)>,
target_query: &mut Query<(
Option<&mut TextColor>,
Option<&mut TextFont>,
Option<&mut LineHeight>,
Option<&mut ImageNode>,
Option<&mut TextShadow>,
Option<&mut Text>,
Option<&mut TextTransformState>,
)>,
asset_server: &Res<AssetServer>,
) {
let my_style_comp = style_query.get(entity).ok();
let my_active_style = my_style_comp.and_then(|s| s.active_style.as_ref());
let mut style_to_propagate = inherited_style.cloned().unwrap_or_default();
merge_entity_style_for_propagation(
entity,
&mut style_to_propagate,
style_query,
style_state_query,
);
if let Ok((
mut text_color_opt,
mut text_font_opt,
mut line_height_opt,
mut image_node_opt,
mut text_shadow_opt,
mut text_opt,
mut text_transform_state_opt,
)) = target_query.get_mut(entity)
{
let has_local_color = my_active_style.map_or(false, |s| s.color.is_some());
if !has_local_color {
if let Some(parent_color) = inherited_style.and_then(|s| s.color) {
if let Some(text_color) = text_color_opt.as_mut() {
if text_color.0 != parent_color {
text_color.0 = parent_color;
}
}
if let Some(image_node) = image_node_opt.as_mut() {
if image_node.color != parent_color {
image_node.color = parent_color;
}
}
}
}
let has_local_size = my_active_style.map_or(false, |s| s.font_size.is_some());
if !has_local_size {
if let Some(parent_size_val) = inherited_style.and_then(|s| s.font_size.as_ref()) {
if let Some(text_font) = text_font_opt.as_mut() {
if text_font.font_size != *parent_size_val {
text_font.font_size = *parent_size_val;
}
}
}
}
let has_local_line_height = my_active_style.map_or(false, |s| s.line_height.is_some());
if !has_local_line_height {
if let (Some(parent_line_height), Some(line_height)) = (
inherited_style.and_then(|s| s.line_height),
line_height_opt.as_mut(),
) {
if **line_height != parent_line_height {
**line_height = parent_line_height;
}
}
}
let has_local_family = my_active_style.map_or(false, |s| s.font_family.is_some());
let has_local_weight = my_active_style.map_or(false, |s| s.font_weight.is_some());
if !has_local_family && !has_local_weight {
if let Some(inherited) = inherited_style {
if let Some(family) = &inherited.font_family {
let weight = inherited.font_weight.unwrap_or(FontWeight::Normal);
let folder = &family.0;
let weight_str = weight_token_exact(weight);
let filename = format!("{}-{}.ttf", folder_basename(folder), weight_str);
let full_path = format!("{}/{}", folder, filename);
let handle = asset_server.load(full_path);
if let Some(text_font) = text_font_opt.as_mut() {
let font = FontSource::Handle(handle);
if text_font.font != font {
text_font.font = font;
}
}
}
}
}
let has_text = text_opt.is_some();
let has_css_context = my_style_comp.is_some() || inherited_style.is_some();
if has_css_context {
if let Some(shadow) = style_to_propagate.text_shadow {
if has_text {
if let Some(current_shadow) = text_shadow_opt.as_mut() {
if **current_shadow != shadow {
**current_shadow = shadow;
}
} else {
commands.entity(entity).insert(shadow);
}
}
} else if text_shadow_opt.is_some() {
commands.entity(entity).remove::<TextShadow>();
}
sync_text_transform_entity(
entity,
style_to_propagate.text_transform,
&mut text_opt,
&mut text_transform_state_opt,
commands,
);
}
}
if let Ok(children) = children_query.get(entity) {
for child_entity in children {
propagate_recursive(
*child_entity,
Some(&style_to_propagate),
commands,
children_query,
style_query,
style_state_query,
target_query,
asset_server,
);
}
}
}
fn sync_text_transform_entity(
entity: Entity,
transform: Option<TextTransform>,
text_opt: &mut Option<Mut<Text>>,
transform_state_opt: &mut Option<Mut<TextTransformState>>,
commands: &mut Commands,
) {
let Some(text) = text_opt.as_mut() else {
if transform_state_opt.is_some() {
commands.entity(entity).remove::<TextTransformState>();
}
return;
};
let active_transform = match transform {
Some(TextTransform::Uppercase) => Some(TextTransform::Uppercase),
Some(TextTransform::Lowercase) => Some(TextTransform::Lowercase),
Some(TextTransform::Capitalize) => Some(TextTransform::Capitalize),
_ => None,
};
match (transform_state_opt.as_mut(), active_transform) {
(None, None) => {}
(None, Some(mode)) => {
let source = text.0.clone();
let rendered = apply_text_transform(mode, source.as_str());
if text.0 != rendered {
text.0 = rendered.clone();
}
commands.entity(entity).insert(TextTransformState {
source,
last_rendered: rendered,
});
}
(Some(state), Some(mode)) => {
if text.0 != state.last_rendered {
state.source = text.0.clone();
}
let rendered = apply_text_transform(mode, state.source.as_str());
if text.0 != rendered {
text.0 = rendered.clone();
}
state.last_rendered = rendered;
}
(Some(state), None) => {
if text.0 != state.last_rendered {
state.source = text.0.clone();
}
if text.0 != state.source {
text.0 = state.source.clone();
}
commands.entity(entity).remove::<TextTransformState>();
}
}
}
fn apply_text_transform(transform: TextTransform, input: &str) -> String {
match transform {
TextTransform::None => input.to_string(),
TextTransform::Uppercase => input.to_uppercase(),
TextTransform::Lowercase => input.to_lowercase(),
TextTransform::Capitalize => capitalize_words(input),
}
}
fn capitalize_words(input: &str) -> String {
let mut out = String::with_capacity(input.len());
let mut start_word = true;
for ch in input.chars() {
if ch.is_alphabetic() {
if start_word {
for upper in ch.to_uppercase() {
out.push(upper);
}
start_word = false;
} else {
for lower in ch.to_lowercase() {
out.push(lower);
}
}
} else {
out.push(ch);
start_word = !ch.is_alphanumeric();
}
}
out
}