diff --git a/assets/bevy_logo_dark.png b/assets/bevy_logo_dark.png new file mode 100644 index 0000000..f144a3a Binary files /dev/null and b/assets/bevy_logo_dark.png differ diff --git a/assets/bevy_logo_light.png b/assets/bevy_logo_light.png new file mode 100644 index 0000000..3e2cbdf Binary files /dev/null and b/assets/bevy_logo_light.png differ diff --git a/assets/icon.png b/assets/icon.png index 3e2cbdf..8c9640b 100644 Binary files a/assets/icon.png and b/assets/icon.png differ diff --git a/bevy_elements_core/src/lib.rs b/bevy_elements_core/src/lib.rs index e3f8734..719fa02 100644 --- a/bevy_elements_core/src/lib.rs +++ b/bevy_elements_core/src/lib.rs @@ -167,8 +167,25 @@ impl Default for TextElementBundle { } } -#[derive(Component, Default)] -pub struct ManualTextProperties; +#[derive(Bundle)] +pub struct ImageElementBundle { + pub element: Element, + #[bundle] + pub image: ImageBundle, +} + +impl Default for ImageElementBundle { + fn default() -> Self { + ImageElementBundle { + element: Element::inline(), + image: ImageBundle { + background_color: BackgroundColor(Color::WHITE), + ..Default::default() + }, + } + } +} + #[derive(Debug)] pub enum ElementsError { /// An unsupported selector was found on a style sheet rule. diff --git a/bevy_elements_core/src/params.rs b/bevy_elements_core/src/params.rs index aafdada..e710453 100644 --- a/bevy_elements_core/src/params.rs +++ b/bevy_elements_core/src/params.rs @@ -1,5 +1,5 @@ use std::{ - any::{Any, TypeId}, + any::{type_name, Any, TypeId}, fmt::Debug, mem, }; @@ -31,6 +31,7 @@ pub enum Variant { Params(Params), BindFrom(BindFromUntyped), BindTo(BindToUntyped), + Any(Box), } impl Debug for Variant { @@ -45,6 +46,7 @@ impl Debug for Variant { Variant::Elements(_) => write!(f, "Variant::Elements"), Variant::BindFrom(_) => write!(f, "Variant::BindFrom"), Variant::BindTo(_) => write!(f, "Variant::BindTo"), + Variant::Any(_) => write!(f, "Variant::Any"), } } } @@ -91,6 +93,7 @@ impl Variant { Variant::Params(_) => TypeId::of::() == TypeId::of::(), Variant::BindFrom(_) => TypeId::of::() == TypeId::of::(), Variant::BindTo(_) => TypeId::of::() == TypeId::of::(), + Variant::Any(v) => v.is::(), } } @@ -105,6 +108,7 @@ impl Variant { Variant::Params(v) => try_cast::(v), Variant::BindFrom(v) => try_cast::(v), Variant::BindTo(v) => try_cast::(v), + Variant::Any(v) => v.downcast_ref::(), } } pub fn get_mut(&mut self) -> Option<&mut T> { @@ -118,6 +122,7 @@ impl Variant { Variant::Params(v) => try_cast_mut::(v), Variant::BindFrom(v) => try_cast_mut::(v), Variant::BindTo(v) => try_cast_mut::(v), + Variant::Any(v) => v.downcast_mut::(), } } @@ -132,6 +137,13 @@ impl Variant { Variant::Params(v) => try_take::(v), Variant::BindFrom(v) => try_take::(v), Variant::BindTo(v) => try_take::(v), + Variant::Any(v) => match v.downcast::() { + Ok(v) => Some(*v), + Err(v) => { + error!("Can't cast {:?} to {}", v, type_name::()); + None + } + }, } } @@ -409,6 +421,15 @@ impl From for Variant { } } +impl TryFrom for String { + type Error = String; + fn try_from(variant: Variant) -> Result { + variant + .take::() + .ok_or("Can't cast variant to String".to_string()) + } +} + impl From<&str> for Variant { fn from(v: &str) -> Self { Variant::String(v.to_string()) @@ -456,8 +477,10 @@ macro_rules! bindattr { match __attr { Some($crate::Variant::BindFrom(__b)) => $ctx.commands().add(__b.to($crate::bind!(=> __elem, $($target)*))), Some($crate::Variant::BindTo(__b)) => $ctx.commands().add(__b.from($crate::bind!(<= __elem, $($target)*))), - Some($crate::Variant::$typ(__v)) => __value = Some(__v), - Some(__attr) => error!("Unsupported value for '{}' param: {:?}", __key, __attr), + Some(__attr) => match $typ::try_from(__attr) { + Ok(__v) => __value = Some(__v), + Err(__err) => error!("Invalid value for '{}' param: {}", __key, __err) + }, _ => () }; __value diff --git a/bevy_elements_core/src/property/impls.rs b/bevy_elements_core/src/property/impls.rs index 640624a..c33d08c 100644 --- a/bevy_elements_core/src/property/impls.rs +++ b/bevy_elements_core/src/property/impls.rs @@ -253,7 +253,7 @@ mod style { /// Impls for `bevy_text` [`Text`] component mod text { use super::*; - use crate::{Defaults, ManualTextProperties}; + use crate::Defaults; #[derive(Default, Clone)] pub enum FontPath { @@ -272,7 +272,7 @@ mod text { impl Property for FontColorProperty { type Cache = Color; type Components = &'static mut Text; - type Filters = (With, Without); + type Filters = With; fn name() -> Tag { tag!("color") @@ -313,7 +313,7 @@ mod text { impl Property for FontProperty { type Cache = FontPath; type Components = &'static mut Text; - type Filters = (With, Without); + type Filters = With; fn name() -> Tag { tag!("font") @@ -385,7 +385,7 @@ mod text { impl Property for FontSizeProperty { type Cache = f32; type Components = &'static mut Text; - type Filters = (With, Without); + type Filters = With; fn name() -> Tag { tag!("font-size") diff --git a/bevy_elements_widgets/src/img.rs b/bevy_elements_widgets/src/img.rs new file mode 100644 index 0000000..9512014 --- /dev/null +++ b/bevy_elements_widgets/src/img.rs @@ -0,0 +1,244 @@ +use bevy::{prelude::*, utils::HashMap}; +use bevy_elements_core::*; +use bevy_elements_macro::*; + +pub(crate) struct ImgPlugin; +impl Plugin for ImgPlugin { + fn build(&self, app: &mut App) { + app.register_widget::(); + + app.init_resource::(); + app.add_system(load_img); + app.add_system(update_img_size); + app.add_system(update_img_layout); + } +} + +#[derive(Resource, Deref, DerefMut, Default)] +struct ImageRegistry(HashMap, Entity>); + +#[derive(Default, Clone, Copy, PartialEq, Debug)] +pub enum ImgMode { + #[default] + Fit, + Cover, + Stretch, + Source, +} + +impl TryFrom for ImgMode { + type Error = String; + fn try_from(value: Variant) -> Result { + match value { + Variant::String(s) if &s == "fit" => Ok(ImgMode::Fit), + Variant::String(s) if &s == "cover" => Ok(ImgMode::Cover), + Variant::String(s) if &s == "stretch" => Ok(ImgMode::Stretch), + Variant::String(s) if &s == "source" => Ok(ImgMode::Source), + Variant::String(s) => Err(format!("Can't parse `{}` as ImgMode", s)), + variant => { + if let Some(value) = variant.take::() { + Ok(value) + } else { + Err("Invalid value for ImgMode".to_string()) + } + } + } + } +} + +impl From for Variant { + fn from(mode: ImgMode) -> Self { + Variant::Any(Box::new(mode)) + } +} + +#[derive(Component, Widget)] +#[alias(img)] +/// The `` tag is used to load image and show it content on the UI screen. +/// The `` tag has two properties: +/// - `src`: Specifies the path to the image +/// - `mode`: Specifies how an image should fits the space: +/// - `fit`: resize the image to fit the box keeping it aspect ratio +/// - `cover`: resize the image to cover the box keeping it aspect ratio +/// - `stretch`: resize image to take all the space ignoring the aspect ratio +/// - `source`: do not resize the image +pub struct Img { + #[param] + pub src: String, + #[param] + pub mode: ImgMode, + entity: Entity, + size: Vec2, +} + +impl WidgetBuilder for Img { + fn setup(&mut self, ctx: &mut ElementContext) { + ctx.commands().entity(self.entity).insert(ImageBundle { + style: Style { + display: Display::None, + ..default() + }, + ..default() + }); + ctx.insert(ElementBundle::default()) + .push_children(&[self.entity]); + } +} + +fn load_img( + asset_server: Res, + mut elements: Query<(Entity, &mut Img), Changed>, + mut images: Query<(&mut UiImage, &mut Style)>, + mut registry: ResMut, + assets: Res>, + mut events: EventWriter>, +) { + for (entity, mut img) in elements.iter_mut() { + let handle = asset_server.load(&img.src); + registry.insert(handle.clone_weak(), entity); + let (mut image, mut style) = images.get_mut(img.entity).unwrap(); + image.0 = handle.clone(); + + // force inner image size recalculation if Image asset already loaded + if assets.contains(&handle) { + style.display = Display::Flex; + events.send(AssetEvent::Modified { + handle: handle.clone_weak(), + }); + } else { + if img.size != Vec2::ZERO { + img.size = Vec2::ZERO; + } + style.display = Display::None; + } + } +} + +fn update_img_size( + mut elements: Query<&mut Img>, + assets: Res>, + mut asset_events: EventReader>, + mut registry: ResMut, +) { + for event in asset_events.iter() { + match event { + AssetEvent::Removed { handle } => { + let Some(entity) = registry.remove(&handle) else { continue }; + let Ok(mut element) = elements.get_mut(entity) else { continue }; + element.size = Vec2::ZERO; + } + AssetEvent::Created { handle } | AssetEvent::Modified { handle } => { + let Some(entity) = registry.get(&handle) else { continue }; + let Ok(mut element) = elements.get_mut(*entity) else { continue }; + let Some(asset) = assets.get(handle) else { continue }; + if element.size != asset.size() { + element.size = asset.size(); + } + } + } + } +} + +fn update_img_layout( + elements: Query<(&Img, &Node), Or<(Changed, Changed)>>, + mut styles: Query<&mut Style>, +) { + for (element, node) in elements.iter() { + let Ok(mut style) = styles.get_mut(element.entity) else { continue }; + if element.size.x.abs() < f32::EPSILON + || element.size.y.abs() < f32::EPSILON + || node.size().x.abs() < f32::EPSILON + || node.size().y.abs() < f32::EPSILON + { + style.display = Display::None; + continue; + } else { + style.display = Display::Flex; + } + let aspect = element.size.y / element.size.x; + match element.mode { + ImgMode::Fit => { + let (width, height) = if aspect > 1.0 { + let width = node.size().x; + let height = width * aspect; + if height > node.size().y { + let width = width * (node.size().y / height); + let height = node.size().y; + (width, height) + } else { + (width, height) + } + } else { + let height = node.size().y; + let width = height / aspect; + if width > node.size().x { + let height = height * (node.size().x / width); + let width = node.size().x; + (width, height) + } else { + (width, height) + } + }; + style.min_size.height = Val::Px(height); + style.min_size.width = Val::Px(width); + style.size = style.min_size; + let hmargin = 0.5 * (node.size().x - width); + let vmargin = 0.5 * (node.size().y - height); + + style.margin.top = Val::Px(vmargin.max(0.)); + style.margin.bottom = Val::Px(vmargin.max(0.)); + style.margin.left = Val::Px(hmargin.max(0.)); + style.margin.right = Val::Px(hmargin.max(0.)); + } + ImgMode::Cover => { + let (width, height) = if aspect > 1.0 { + let width = node.size().x; + let height = width * aspect; + if height < node.size().y { + let width = width * (node.size().y / height); + let height = node.size().y; + (width, height) + } else { + (width, height) + } + } else { + let height = node.size().y; + let width = height / aspect; + if width < node.size().x { + let height = height * (node.size().x / width); + let width = node.size().x; + (width, height) + } else { + (width, height) + } + }; + + style.min_size.height = Val::Px(height); + style.min_size.width = Val::Px(width); + style.size = style.min_size; + let hmargin = 0.5 * (node.size().x - width); + let vmargin = 0.5 * (node.size().y - height); + + style.margin.top = Val::Px(vmargin.min(0.)); + style.margin.bottom = Val::Px(vmargin.min(0.)); + style.margin.left = Val::Px(hmargin.min(0.)); + style.margin.right = Val::Px(hmargin.min(0.)); + } + ImgMode::Stretch => { + style.min_size = Size::new(Val::Undefined, Val::Undefined); + style.size = Size::new(Val::Percent(100.), Val::Percent(100.)); + style.margin = UiRect::all(Val::Px(0.)); + } + ImgMode::Source => { + style.size = Size::new(Val::Px(element.size.x), Val::Px(element.size.y)); + style.min_size = style.size; + let hmargin = 0.5 * (node.size().x - element.size.x); + let vmargin = 0.5 * (node.size().y - element.size.y); + style.margin.left = Val::Px(hmargin); + style.margin.right = Val::Px(hmargin); + style.margin.top = Val::Px(vmargin); + style.margin.bottom = Val::Px(vmargin); + } + } + } +} diff --git a/bevy_elements_widgets/src/lib.rs b/bevy_elements_widgets/src/lib.rs index 361a9c9..3efe076 100644 --- a/bevy_elements_widgets/src/lib.rs +++ b/bevy_elements_widgets/src/lib.rs @@ -1,4 +1,5 @@ pub mod common; +pub mod img; pub mod input; use bevy::prelude::Plugin; @@ -7,6 +8,7 @@ pub struct WidgetsPlugin; impl Plugin for WidgetsPlugin { fn build(&self, app: &mut bevy::prelude::App) { + app.add_plugin(img::ImgPlugin); app.add_plugin(input::InputPlugins); app.add_plugin(common::CommonsPlugin); } @@ -16,5 +18,7 @@ pub mod prelude { #[doc(inline)] pub use crate::common::*; #[doc(inline)] + pub use crate::img::*; + #[doc(inline)] pub use crate::input::*; } diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 6f48a3c..d185f81 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -16,4 +16,4 @@ fn setup(mut commands: Commands) { "Hello, ""world""!" }); -} \ No newline at end of file +} diff --git a/examples/image.rs b/examples/image.rs new file mode 100644 index 0000000..110acfd --- /dev/null +++ b/examples/image.rs @@ -0,0 +1,35 @@ +use bevy::prelude::*; +use bevy_elements::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugin(ElementsPlugin) + .add_startup_system(setup) + .run(); +} + +fn setup(mut commands: Commands) { + commands.spawn(Camera2dBundle::default()); + let img = commands.spawn_empty().id(); + commands.add(eml! { + + +
+
+ "Mode:" + + + + +
+
+
+ "Source:" + + + +
+ + }); +} diff --git a/src/lib.rs b/src/lib.rs index ccc493e..e41b7e3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,11 @@ use bevy::prelude::*; use bevy_elements_core::ElementsCorePlugin; use bevy_elements_widgets::WidgetsPlugin; +// bundles +pub use bevy_elements_core::ElementBundle; +pub use bevy_elements_core::ImageElementBundle; +pub use bevy_elements_core::TextElementBundle; + // structs pub use bevy_elements_core::eml::EmlAsset; pub use bevy_elements_core::eml::EmlScene;