use bevy::platform::collections::HashMap;
use bevy::prelude::*;
use bevy::sprite::{BorderRect, SliceScaleMode, TextureSlicer};
use bevy::text::{FontSize as BevyFontSize, LetterSpacing, LineHeight};
use bevy::ui::FocusPolicy;
use bevy::ui::widget::NodeImageMode;
use crate::element::Attrs;
use crate::plugin::Fonts;
use crate::protocol::{
animatable::Animatable, animatable::AnimatableField, background_image::AtlasSpec,
background_image::ImageMode, background_image::ImageModeSpec, background_image::SliceBorder,
background_image::SliceScale, background_image::SliceSpec, units::Angle, units::FontSize,
units::Length, units::Rect, visual::AngularStop, visual::BoxShadowList, visual::BoxShadowSpec,
visual::ConicGradientSpec, visual::GradientList, visual::GradientSpec, visual::GradientStop,
visual::LetterSpacingSpec, visual::LineHeightSpec, visual::LinearGradientSpec,
visual::RadialGradientSpec, visual::RadialShapeSpec,
};
use crate::scrollbar::ScrollbarPosition;
use crate::style::Style;
use crate::style::props::{
ALIGN_CONTENT, ALIGN_ITEMS, ALIGN_SELF, ASPECT_RATIO, BACKGROUND_COLOR, BORDER, BORDER_COLOR,
BORDER_RADIUS, BOTTOM, BOX_SIZING, COLOR, COLUMN_GAP, DISPLAY, FLEX_BASIS, FLEX_DIRECTION,
FLEX_GROW, FLEX_SHRINK, FLEX_WRAP, FOCUS_POLICY, FONT_FAMILY, FONT_SIZE, FONT_WEIGHT, GAP,
GRID_AUTO_COLUMNS, GRID_AUTO_FLOW, GRID_AUTO_ROWS, GRID_COLUMN, GRID_ROW,
GRID_TEMPLATE_COLUMNS, GRID_TEMPLATE_ROWS, HEIGHT, JUSTIFY_CONTENT, JUSTIFY_ITEMS,
JUSTIFY_SELF, LEFT, LETTER_SPACING, LINE_BREAK, LINE_HEIGHT, MARGIN, MAX_HEIGHT, MAX_WIDTH,
MIN_HEIGHT, MIN_WIDTH, OPACITY, OVERFLOW_X, OVERFLOW_Y, PADDING, POSITION_TYPE, RIGHT, ROW_GAP,
SCROLLBAR, SCROLLBAR_WIDTH, TEXT_ALIGN, TEXT_SHADOW, TOP, WIDTH, Z_INDEX,
};
use crate::style::{Invalidation, WriterCtx, WriterMask};
pub fn parse_color(input: &str) -> Color {
match crate::raster::parse_css_color(input) {
Some(c) => Color::from(c),
None => {
let msg = format!("unrecognized color {input:?}");
crate::diag::report("color", input, &msg);
Color::srgb(1.0, 0.0, 1.0)
}
}
}
pub fn apply_opacity(color: Color, opacity: Option<f32>) -> Color {
match opacity {
Some(o) => color.with_alpha(color.alpha() * o),
None => color,
}
}
pub fn length_to_val(length: Length) -> Val {
match length {
Length::Auto => Val::Auto,
Length::Px(v) => Val::Px(v),
Length::Percent(v) => Val::Percent(v),
Length::Vw(v) => Val::Vw(v),
Length::Vh(v) => Val::Vh(v),
Length::VMin(v) => Val::VMin(v),
Length::VMax(v) => Val::VMax(v),
}
}
pub fn rect_to_uirect(rect: Rect) -> UiRect {
UiRect {
left: length_to_val(rect.left),
right: length_to_val(rect.right),
top: length_to_val(rect.top),
bottom: length_to_val(rect.bottom),
}
}
pub fn rect_to_border_radius(rect: Rect) -> BorderRadius {
BorderRadius {
top_left: length_to_val(rect.top),
top_right: length_to_val(rect.right),
bottom_right: length_to_val(rect.bottom),
bottom_left: length_to_val(rect.left),
}
}
fn shadow_style(b: &BoxShadowSpec) -> ShadowStyle {
ShadowStyle {
color: b.color.as_deref().map(parse_color).unwrap_or(Color::BLACK),
x_offset: b.x_offset.map(length_to_val).unwrap_or(Val::Px(0.0)),
y_offset: b.y_offset.map(length_to_val).unwrap_or(Val::Px(0.0)),
spread_radius: b.spread_radius.map(length_to_val).unwrap_or(Val::Px(0.0)),
blur_radius: b.blur_radius.map(length_to_val).unwrap_or(Val::Px(0.0)),
}
}
pub fn build_box_shadows(list: &BoxShadowList) -> Vec<ShadowStyle> {
match list {
BoxShadowList::One(b) => vec![shadow_style(b)],
BoxShadowList::Many(bs) => bs.iter().map(shadow_style).collect(),
}
}
pub(crate) fn node_from(style: Option<&Style>) -> Node {
let Some(s) = style else {
return Node::default();
};
let mut node = Node::default();
if let Some(v) = s.get(&DISPLAY).copied() {
node.display = v;
}
if let Some(v) = s.get(&BOX_SIZING).copied() {
node.box_sizing = v;
}
if let Some(v) = s.get(&POSITION_TYPE).copied() {
node.position_type = v;
}
if let Some(v) = s.get(&OVERFLOW_X).copied() {
node.overflow.x = v;
}
if let Some(v) = s.get(&OVERFLOW_Y).copied() {
node.overflow.y = v;
}
if let Some(v) = s.get(&SCROLLBAR_WIDTH).copied() {
node.scrollbar_width = v;
} else if let Some(spec) = s.get(&SCROLLBAR) {
if spec.is_visible() && spec.position() == ScrollbarPosition::Gutter {
node.scrollbar_width = spec.thickness();
}
}
if let Some(v) = s.get(&LEFT).static_val() {
node.left = length_to_val(v);
}
if let Some(v) = s.get(&RIGHT).static_val() {
node.right = length_to_val(v);
}
if let Some(v) = s.get(&TOP).static_val() {
node.top = length_to_val(v);
}
if let Some(v) = s.get(&BOTTOM).static_val() {
node.bottom = length_to_val(v);
}
if let Some(v) = s.get(&WIDTH).static_val() {
node.width = length_to_val(v);
}
if let Some(v) = s.get(&HEIGHT).static_val() {
node.height = length_to_val(v);
}
if let Some(v) = s.get(&MIN_WIDTH).static_val() {
node.min_width = length_to_val(v);
}
if let Some(v) = s.get(&MIN_HEIGHT).static_val() {
node.min_height = length_to_val(v);
}
if let Some(v) = s.get(&MAX_WIDTH).static_val() {
node.max_width = length_to_val(v);
}
if let Some(v) = s.get(&MAX_HEIGHT).static_val() {
node.max_height = length_to_val(v);
}
if let Some(v) = s.get(&ASPECT_RATIO).static_val() {
node.aspect_ratio = Some(v);
}
if let Some(v) = s.get(&ALIGN_ITEMS).copied() {
node.align_items = v;
}
if let Some(v) = s.get(&JUSTIFY_ITEMS).copied() {
node.justify_items = v;
}
if let Some(v) = s.get(&ALIGN_SELF).copied() {
node.align_self = v;
}
if let Some(v) = s.get(&JUSTIFY_SELF).copied() {
node.justify_self = v;
}
if let Some(v) = s.get(&ALIGN_CONTENT).copied() {
node.align_content = v;
}
if let Some(v) = s.get(&JUSTIFY_CONTENT).copied() {
node.justify_content = v;
}
if let Some(r) = s.get(&MARGIN).copied() {
node.margin = rect_to_uirect(r);
}
if let Some(r) = s.get(&PADDING).copied() {
node.padding = rect_to_uirect(r);
}
if let Some(r) = s.get(&BORDER).copied() {
node.border = rect_to_uirect(r);
}
if let Some(v) = s.get(&FLEX_DIRECTION).copied() {
node.flex_direction = v;
}
if let Some(v) = s.get(&FLEX_WRAP).copied() {
node.flex_wrap = v;
}
if let Some(v) = s.get(&FLEX_GROW).copied() {
node.flex_grow = v;
}
if let Some(v) = s.get(&FLEX_SHRINK).copied() {
node.flex_shrink = v;
}
if let Some(v) = s.get(&FLEX_BASIS).static_val() {
node.flex_basis = length_to_val(v);
}
if let Some(v) = s.get(&GAP).static_val() {
node.row_gap = length_to_val(v);
node.column_gap = length_to_val(v);
}
if let Some(v) = s.get(&ROW_GAP).static_val() {
node.row_gap = length_to_val(v);
}
if let Some(v) = s.get(&COLUMN_GAP).static_val() {
node.column_gap = length_to_val(v);
}
if let Some(v) = s.get(&GRID_AUTO_FLOW).copied() {
node.grid_auto_flow = v;
}
if let Some(v) = s.get(&GRID_TEMPLATE_ROWS) {
node.grid_template_rows = v.clone();
}
if let Some(v) = s.get(&GRID_TEMPLATE_COLUMNS) {
node.grid_template_columns = v.clone();
}
if let Some(v) = s.get(&GRID_AUTO_ROWS) {
node.grid_auto_rows = v.clone();
}
if let Some(v) = s.get(&GRID_AUTO_COLUMNS) {
node.grid_auto_columns = v.clone();
}
if let Some(v) = s.get(&GRID_ROW).copied() {
node.grid_row = v;
}
if let Some(v) = s.get(&GRID_COLUMN).copied() {
node.grid_column = v;
}
if let Some(r) = s.get(&BORDER_RADIUS).static_val() {
node.border_radius = rect_to_border_radius(r);
}
node
}
pub(crate) fn set_if_neq_or_insert<
T: Component<Mutability = bevy::ecs::component::Mutable> + PartialEq,
>(
value: T,
) -> impl EntityCommand {
move |mut entity: EntityWorldMut| match entity.get_mut::<T>() {
Some(mut current) => {
current.set_if_neq(value);
}
None => {
entity.insert(value);
}
}
}
pub fn fresh_style_bundle(style: &Option<Style>, flags: crate::ext::ElementFlags) -> impl Bundle {
let s = style.as_ref();
let opacity = s.and_then(|s| s.get(&OPACITY).static_val());
let focus_policy = s
.and_then(|s| s.get(&FOCUS_POLICY).copied())
.unwrap_or(FocusPolicy::Pass);
(
node_from(s),
background_color(s, opacity),
border_color(s),
ZIndex(s.and_then(|s| s.get(&Z_INDEX).copied()).unwrap_or_default()),
focus_policy,
crate::style::writers::focus_pickable(flags, focus_policy),
)
}
pub(crate) fn background_color(s: Option<&Style>, opacity: Option<f32>) -> BackgroundColor {
match s.and_then(|s| s.get(&BACKGROUND_COLOR)).static_ref() {
Some(hex) => BackgroundColor(apply_opacity(parse_color(hex), opacity)),
None => BackgroundColor::DEFAULT,
}
}
pub(crate) fn border_color(s: Option<&Style>) -> BorderColor {
match s.and_then(|s| s.get(&BORDER_COLOR)).static_ref() {
Some(spec) => {
let side = |c: &Option<String>| c.as_deref().map(parse_color).unwrap_or(Color::NONE);
BorderColor {
top: side(&spec.top),
right: side(&spec.right),
bottom: side(&spec.bottom),
left: side(&spec.left),
}
}
None => BorderColor::DEFAULT,
}
}
pub fn apply_style(ec: &mut EntityCommands, style: &Option<Style>, ctx: &WriterCtx) {
apply_style_masked(ec, style, WriterMask::ALL, u64::MAX, ctx, Invalidation::ALL);
}
pub fn apply_style_fresh(ec: &mut EntityCommands, style: &Option<Style>, ctx: &WriterCtx) {
debug_assert!(ctx.fresh, "apply_style_fresh with a non-fresh ctx");
let bundled = ctx.styles.masks.fresh_bundled;
let keys = style
.as_ref()
.map_or(crate::style::StyleDirty::NONE, |s| s.keys());
let present = ctx.styles.writers_for(&keys);
let attrs = ctx.attrs.keys().union(ctx.events.keys());
let own = ctx.element.writers_for(&keys, attrs);
apply_style_masked(
ec,
style,
present.without(bundled),
own,
ctx,
Invalidation::PAINT,
);
}
pub(crate) fn remove_unless_fresh<B: Bundle>(ec: &mut EntityCommands, fresh: bool) {
if !fresh {
ec.remove::<B>();
}
}
pub fn apply_style_masked(
ec: &mut EntityCommands,
style: &Option<Style>,
writers: WriterMask,
own: u64,
ctx: &WriterCtx,
invalidation: Invalidation,
) {
let empty = Style::empty();
let s = style.as_ref().unwrap_or(empty);
let mut bits = writers
.intersection(ctx.styles.all_writers())
.intersection(ctx.element.global_mask)
.0;
while bits != 0 {
let bit = bits.trailing_zeros() as usize;
bits &= bits - 1;
(ctx.styles.writer_at(bit).apply)(ctx, s, ec);
}
let mut bits = own & ctx.element.all_writers();
while bits != 0 {
let bit = bits.trailing_zeros() as usize;
bits &= bits - 1;
(ctx.element.writer_at(bit).apply)(ctx, s, ec);
}
if invalidation.contains(Invalidation::PAINT) {
crate::layer::mark_content_dirty(ec);
}
}
pub fn overlay_style(base: Option<&Style>, overlay: Option<&Style>) -> Option<Style> {
let Some(overlay) = overlay else {
return base.cloned();
};
let mut merged = base.cloned().unwrap_or_default();
merged.overlay_variant(overlay);
Some(merged)
}
mod gradient;
mod image;
mod text;
pub use gradient::*;
pub use image::*;
pub use text::*;
#[cfg(test)]
mod tests;