Skip to content
Snippets Groups Projects
Unverified Commit 7649c636 authored by John's avatar John Committed by GitHub
Browse files

Merge pull request #90 from Ygg01/rfc-3-on-layout

Rfc 3 on layout
parents f90c0541 4c79ff76
No related branches found
No related tags found
No related merge requests found
Showing
with 404 additions and 9 deletions
......@@ -1904,6 +1904,7 @@ dependencies = [
"kayak_core",
"kayak_font",
"kayak_render_macros",
"rand",
]
[[package]]
......
......@@ -25,6 +25,7 @@ kayak_render_macros = { path = "kayak_render_macros" }
[dev-dependencies]
bevy = { version = "0.6.0" }
rand = { version = "0.8.4" }
[[example]]
name = "todo"
......
//! This example demonstrates how to use a [on_layout](kayak_core::WidgetProps::get_on_layout)
//! event in widgets.
//!
//! The problem here is strictly contrived for example purposes.
//! We use grow/shrink buttons to set the value of a `width` bound to an [crate::Background] element's width
//! On change of layout we print current width of that element and update the text of Width label.
use bevy::{
prelude::{App as BevyApp, AssetServer, Commands, Res, ResMut},
window::WindowDescriptor,
DefaultPlugins,
};
use kayak_core::{
styles::{LayoutType, Style, StyleProp, Units, Edge},
OnLayout,
};
use kayak_core::{Color, EventType, OnEvent};
use kayak_render_macros::use_state;
use kayak_ui::widgets::{App, Text, Window, Element};
use kayak_ui::{
bevy::{BevyContext, BevyKayakUIPlugin, FontMapping, UICameraBundle},
widgets::Button,
};
use kayak_ui::{
core::{render, rsx, widget, Index},
widgets::Background,
};
/// This widget provides a theme to its children
#[widget]
fn GrowShrink() {
// This is width of background element we update via buttons
let (background_width, set_width, _) = use_state!(150.0);
let panel_style = Style {
layout_type: StyleProp::Value(LayoutType::Row),
width: StyleProp::Value(Units::Auto),
height: StyleProp::Value(Units::Pixels(50.0)),
offset: StyleProp::Value(Edge::all(Units::Pixels(10.0))),
..Default::default()
};
// Grow/Shrink button styles
let button_styles = Style {
width: StyleProp::Value(Units::Pixels(100.0)),
height: StyleProp::Value(Units::Pixels(30.0)),
background_color: StyleProp::Value(Color::new(0.33, 0.33, 0.33, 1.0)),
offset: StyleProp::Value(Edge::all(Units::Pixels(10.0))),
..Default::default()
};
// The background style of element growing/shrink
let fill = Style {
width: StyleProp::Value(Units::Pixels(background_width)),
height: StyleProp::Value(Units::Pixels(28.0)),
layout_type: StyleProp::Value(LayoutType::Column),
background_color: StyleProp::Value(Color::new(1.0, 0.0, 0.0, 1.0)),
..Default::default()
};
// Cloned function for use in closures
let grow_fn = set_width.clone();
let shrink_fn = set_width.clone();
let grow = OnEvent::new(move |_, event| match event.event_type {
EventType::Click(..) => grow_fn(background_width + rand::random::<f32>() * 10.0),
_ => {}
});
let shrink = OnEvent::new(move |_, event| match event.event_type {
EventType::Click(..) => shrink_fn(background_width - rand::random::<f32>() * 10.0),
_ => {}
});
// layout width will be used by width label which we update `on_layout`
let (layout_width, set_layout_width, _) = use_state!(0.0);
let update_text = OnLayout::new(move |_, layout_event| {
println!("Layout changed! New width = {}", layout_event.layout.width);
set_layout_width(layout_event.layout.width);
});
rsx! {
<>
<Window position={(100.0, 100.0)} size={(400.0, 400.0)} title={"Grow/Shrink Example".to_string()}>
<Text size={25.0} content={format!("Width: {:?}", layout_width).to_string()} />
<Element styles={Some(panel_style)}>
<Button styles={Some(button_styles)} on_event={Some(grow)}>
<Text size={20.0} content={"Grow".to_string()}/>
</Button>
<Button styles={Some(button_styles)} on_event={Some(shrink)}>
<Text size={20.0} content={"Shrink".to_string()}/>
</Button>
</Element>
<Background styles={Some(fill)} on_layout={Some(update_text)} />
</Window>
</>
}
}
fn startup(
mut commands: Commands,
mut font_mapping: ResMut<FontMapping>,
asset_server: Res<AssetServer>,
) {
commands.spawn_bundle(UICameraBundle::new());
font_mapping.add("Roboto", asset_server.load("roboto.kayak_font"));
let context = BevyContext::new(|context| {
render! {
<App>
<GrowShrink />
</App>
}
});
commands.insert_resource(context);
}
fn main() {
BevyApp::new()
.insert_resource(WindowDescriptor {
width: 1270.0,
height: 720.0,
title: String::from("UI Example"),
..Default::default()
})
.add_plugins(DefaultPlugins)
.add_plugin(BevyKayakUIPlugin)
.add_startup_system(startup)
.run();
}
use crate::assets::AssetStorage;
use crate::layout_dispatcher::LayoutEventDispatcher;
use crate::{Binding, Changeable, CursorIcon, KayakContextRef};
use std::collections::HashMap;
use std::path::PathBuf;
......@@ -559,6 +560,7 @@ impl KayakContext {
// self.widget_manager.dirty_nodes.clear();
self.widget_manager.render();
self.widget_manager.calculate_layout();
LayoutEventDispatcher::dispatch(self);
self.update_cursor();
}
......
use crate::{
context_ref::KayakContextRef, styles::Style, Children, Index, OnEvent, Widget, WidgetProps,
context_ref::KayakContextRef, styles::Style, Children, Index, OnEvent, OnLayout, Widget,
WidgetProps,
};
/// Props used by the [`Fragment`] widget
......@@ -45,6 +46,10 @@ impl WidgetProps for FragmentProps {
None
}
fn get_on_layout(&self) -> Option<OnLayout> {
None
}
fn get_focusable(&self) -> Option<bool> {
Some(false)
}
......
use crate::layout_cache::Rect;
use crate::Index;
pub use morphorm::GeometryChanged;
/// A layout data sent to widgets on layout.
///
/// Similar and interchangeable with [Rect]
/// ```
/// use kayak_core::layout_cache::Rect;
/// use kayak_core::Layout;
///
/// let layout = Layout::default();
/// let rect : Rect = layout.into();
/// let layout : Layout = rect.into();
/// ```
#[derive(Debug, Default, Clone, Copy, PartialEq)]
pub struct Layout {
/// width of the component
pub width: f32,
/// height of the component
pub height: f32,
/// x-coordinates of the component
pub x: f32,
/// y-coordinates of the component
pub y: f32,
/// z-coordinates of the component
pub z: f32,
}
impl Layout {
/// Returns the position as a Kayak position type
pub fn pos(&self) -> (f32, f32) {
(self.x, self.y)
}
}
impl From<Layout> for Rect {
fn from(layout: Layout) -> Self {
Rect {
posx: layout.x,
posy: layout.y,
width: layout.width,
height: layout.height,
z_index: layout.z,
}
}
}
impl From<Rect> for Layout {
fn from(rect: Rect) -> Self {
Layout {
width: rect.width,
height: rect.height,
x: rect.posx,
y: rect.posy,
z: rect.z_index,
}
}
}
///
/// Struct used for [crate::OnLayout] as layout event data.
///
pub struct LayoutEvent {
/// Layout of target component
pub layout: Layout,
/// Flags denoting the layout change.
pub flags: GeometryChanged,
/// The node ID of the element receiving the layout event.
pub target: Index,
}
impl LayoutEvent {
pub(crate) fn new(rect: Rect, geometry_change: GeometryChanged, index: Index) -> LayoutEvent {
LayoutEvent {
layout: rect.into(),
flags: geometry_change,
target: index,
}
}
}
use indexmap::IndexSet;
use morphorm::{Cache};
use crate::{Index, KayakContext, KayakContextRef, LayoutEvent};
pub(crate) struct LayoutEventDispatcher;
impl LayoutEventDispatcher {
pub fn dispatch(context: &mut KayakContext) {
let dirty_nodes: Vec<Index> = context
.widget_manager
.dirty_render_nodes
.drain(..)
.collect();
// Use IndexSet to prevent duplicates and maintain speed
let mut parents: IndexSet<Index> = IndexSet::default();
for node_index in dirty_nodes {
// If layout is not changed -> skip
if context
.widget_manager
.layout_cache
.geometry_changed(node_index)
.is_empty()
{
continue;
}
// Add parent to set
if let Some(parent_index) = context.widget_manager.tree.get_parent(node_index) {
parents.insert(parent_index);
}
// Process and dispatch
Self::process(node_index, context);
}
// Finally, process all parents
for parent_index in parents {
// Process and dispatch
Self::process(parent_index, context);
}
}
fn process(index: Index, context: &mut KayakContext) {
// We should be able to just get layout from WidgetManager here
// since the layouts will be calculated by this point
let widget = context.widget_manager.take(index);
if let Some(on_layout) = widget.get_props().get_on_layout() {
if let Some(rect) = context.widget_manager.layout_cache.rect.get(&index) {
let layout_event = LayoutEvent::new(
*rect,
context.widget_manager.layout_cache.geometry_changed(index),
index,
);
let mut context_ref = KayakContextRef::new(context, Some(index));
on_layout.try_call(&mut context_ref, &layout_event);
}
}
context.widget_manager.repossess(widget);
}
}
......@@ -15,10 +15,13 @@ pub(crate) mod generational_arena;
mod input_event;
mod keyboard;
mod keys;
mod layout;
pub mod layout_cache;
mod layout_dispatcher;
mod multi_state;
pub mod node;
mod on_event;
mod on_layout;
pub mod render_command;
pub mod render_primitive;
pub mod styles;
......@@ -43,7 +46,9 @@ pub use generational_arena::{Arena, Index};
pub use input_event::*;
pub use keyboard::{KeyboardEvent, KeyboardModifiers};
pub use keys::KeyCode;
pub use layout::*;
pub use on_event::OnEvent;
pub use on_layout::OnLayout;
pub use resources::Resources;
pub use tree::{Tree, WidgetTree};
pub use vec::{VecTracker, VecTrackerProps};
......
use crate::layout::LayoutEvent;
use crate::KayakContextRef;
use std::fmt::{Debug, Formatter};
use std::sync::{Arc, RwLock};
/// A container for a function that handles layout
///
/// This differs from a standard [`Handler`](crate::Handler) in that it's sent directly
/// from the [`KayakContext`](crate::KayakContext) and gives the [`KayakContextRef`]
/// as a parameter.
#[derive(Clone)]
pub struct OnLayout(
Arc<RwLock<dyn FnMut(&mut KayakContextRef, &LayoutEvent) + Send + Sync + 'static>>,
);
impl OnLayout {
/// Create a new layout handler
///
/// The handler should be a closure that takes the following arguments:
/// 1. The current context
/// 2. The LayoutEvent
pub fn new<F: FnMut(&mut KayakContextRef, &LayoutEvent) + Send + Sync + 'static>(
f: F,
) -> OnLayout {
OnLayout(Arc::new(RwLock::new(f)))
}
/// Call the layout handler
///
/// Returns true if the handler was successfully invoked.
pub fn try_call(&self, context: &mut KayakContextRef, event: &LayoutEvent) -> bool {
if let Ok(mut on_layout) = self.0.write() {
on_layout(context, event);
true
} else {
false
}
}
}
impl Debug for OnLayout {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OnLayout").finish()
}
}
impl PartialEq for OnLayout {
fn eq(&self, _: &Self) -> bool {
// Never prevent "==" from being true because of this struct
true
}
}
use crate::{
context_ref::KayakContextRef, styles::Style, Children, Index, OnEvent, Widget, WidgetProps,
};
use crate::{context_ref::KayakContextRef, styles::Style, Children, Index, OnEvent, Widget, WidgetProps, OnLayout};
/// Props used by the [`VecTracker`] widget
#[derive(Default, Debug, PartialEq, Clone)]
......@@ -12,6 +10,7 @@ pub struct VecTrackerProps<T> {
pub styles: Option<Style>,
pub children: Option<Children>,
pub on_event: Option<OnEvent>,
pub on_layout: Option<OnLayout>,
}
/// A widget that renders a `Vec` of widgets
......@@ -40,6 +39,7 @@ impl<T> VecTracker<T> {
styles: None,
children: None,
on_event: None,
on_layout: None,
};
Self {
......@@ -78,6 +78,10 @@ where
self.on_event.clone()
}
fn get_on_layout(&self) -> Option<OnLayout> {
self.on_layout.clone()
}
fn get_focusable(&self) -> Option<bool> {
Some(false)
}
......
use as_any::AsAny;
use std::any::Any;
use crate::on_layout::OnLayout;
use crate::{context_ref::KayakContextRef, styles::Style, Children, Event, Index, OnEvent};
/// An internal trait that has a blanket implementation over all implementors of [`Widget`]
......@@ -86,6 +87,10 @@ pub trait WidgetProps: std::fmt::Debug + AsAny + Send + Sync {
///
/// Returns `None` if this widget doesn't contain a custom event handler
fn get_on_event(&self) -> Option<OnEvent>;
/// Gets the custom layout event handler of this widget
///
/// Returns `None` if this widget doesn't contain a custom layout event handler
fn get_on_layout(&self) -> Option<OnLayout>;
/// Gets the focusability of this widget
///
/// The meanings of the returned values are:
......@@ -164,6 +169,10 @@ impl WidgetProps for () {
None
}
fn get_on_layout(&self) -> Option<OnLayout> {
None
}
fn get_focusable(&self) -> Option<bool> {
None
}
......
......@@ -172,8 +172,8 @@ impl WidgetManager {
pub fn render(&mut self) {
let initial_styles = Style::initial();
let default_styles = Style::new_default();
for dirty_node_index in self.dirty_render_nodes.drain(..) {
let dirty_widget = self.current_widgets[dirty_node_index].as_ref().unwrap();
for dirty_node_index in &self.dirty_render_nodes {
let dirty_widget = self.current_widgets[*dirty_node_index].as_ref().unwrap();
// Get the parent styles. Will be one of the following:
// 1. Already-resolved node styles (best)
// 2. Unresolved widget prop styles
......@@ -232,13 +232,13 @@ impl WidgetManager {
.unwrap_or(vec![]);
let mut node = NodeBuilder::empty()
.with_id(dirty_node_index)
.with_id(*dirty_node_index)
.with_styles(styles, raw_styles)
.with_children(children)
.build();
node.z = current_z;
self.nodes[dirty_node_index] = Some(node);
self.nodes[*dirty_node_index] = Some(node);
}
self.node_tree = self.build_nodes_tree();
......
......@@ -13,6 +13,7 @@ const PROPS_HELPER_IDENT: &str = "prop_field";
const PROP_CHILDREN: &str = "Children";
const PROP_STYLE: &str = "Styles";
const PROP_ON_EVENT: &str = "OnEvent";
const PROP_ON_LAYOUT: &str = "OnLayout";
const PROP_FOCUSABLE: &str = "Focusable";
#[derive(Default)]
......@@ -20,6 +21,7 @@ struct PropsHelpers {
children_ident: Option<Ident>,
styles_ident: Option<Ident>,
on_event_ident: Option<Ident>,
on_layout_ident: Option<Ident>,
focusable_ident: Option<Ident>,
}
......@@ -43,6 +45,7 @@ pub(crate) fn impl_widget_props(input: TokenStream) -> TokenStream {
let children_return = quote_clone_field(helpers.children_ident);
let styles_return = quote_clone_field(helpers.styles_ident);
let on_event_return = quote_clone_field(helpers.on_event_ident);
let on_layout_return = quote_clone_field(helpers.on_layout_ident);
let focusable_return = quote_clone_field(helpers.focusable_ident);
let kayak_core = get_core_crate();
......@@ -65,6 +68,10 @@ pub(crate) fn impl_widget_props(input: TokenStream) -> TokenStream {
#on_event_return
}
fn get_on_layout(&self) -> Option<#kayak_core::OnLayout> {
#on_layout_return
}
fn get_focusable(&self) -> Option<bool> {
#focusable_return
}
......@@ -125,6 +132,7 @@ fn process_field(field: Field, props: &mut PropsHelpers) {
PROP_CHILDREN => props.children_ident = field.ident.clone(),
PROP_STYLE => props.styles_ident = field.ident.clone(),
PROP_ON_EVENT => props.on_event_ident = field.ident.clone(),
PROP_ON_LAYOUT => props.on_layout_ident = field.ident.clone(),
PROP_FOCUSABLE => props.focusable_ident = field.ident.clone(),
err => emit_error!(err.span(), "Invalid attribute: {}", err),
}
......
use kayak_core::OnLayout;
use crate::core::{
render_command::RenderCommand,
rsx,
......@@ -16,6 +18,8 @@ pub struct AppProps {
pub children: Option<Children>,
#[prop_field(OnEvent)]
pub on_event: Option<OnEvent>,
#[prop_field(OnLayout)]
pub on_layout: Option<OnLayout>,
#[prop_field(Focusable)]
pub focusable: Option<bool>,
}
......@@ -32,6 +36,7 @@ pub struct AppProps {
/// | `children` | ✅ |
/// | `styles` | ✅ |
/// | `on_event` | ✅ |
/// | `on_layout` | ✅ |
/// | `focusable` | ✅ |
///
/// # Using the `bevy_renderer` feature
......
use kayak_core::OnLayout;
use crate::core::{
render_command::RenderCommand,
rsx,
......@@ -14,6 +15,8 @@ pub struct BackgroundProps {
pub children: Option<Children>,
#[prop_field(OnEvent)]
pub on_event: Option<OnEvent>,
#[prop_field(OnLayout)]
pub on_layout: Option<OnLayout>,
#[prop_field(Focusable)]
pub focusable: Option<bool>,
}
......@@ -30,6 +33,7 @@ pub struct BackgroundProps {
/// | `children` | ✅ |
/// | `styles` | ✅ |
/// | `on_event` | ✅ |
/// | `on_layout` | ✅ |
/// | `focusable` | ✅ |
///
pub fn Background(props: BackgroundProps) {
......
......@@ -2,7 +2,7 @@ use crate::core::{
render_command::RenderCommand,
rsx,
styles::{Corner, Style, StyleProp, Units},
widget, Children, Color, Fragment, OnEvent, WidgetProps,
widget, Children, Color, Fragment, OnEvent, OnLayout, WidgetProps,
};
use kayak_core::CursorIcon;
......@@ -17,6 +17,7 @@ pub struct ButtonProps {
pub styles: Option<Style>,
pub children: Option<Children>,
pub on_event: Option<OnEvent>,
pub on_layout: Option<OnLayout>,
pub focusable: Option<bool>,
}
......@@ -37,6 +38,10 @@ impl WidgetProps for ButtonProps {
self.on_event.clone()
}
fn get_on_layout(&self) -> Option<OnLayout> {
self.on_layout.clone()
}
fn get_focusable(&self) -> Option<bool> {
Some(!self.disabled)
}
......@@ -54,6 +59,7 @@ impl WidgetProps for ButtonProps {
/// | `children` | ✅ |
/// | `styles` | ✅ |
/// | `on_event` | ✅ |
/// | `on_layout` | ✅ |
/// | `focusable` | ✅ |
///
pub fn Button(props: ButtonProps) {
......
use kayak_core::OnLayout;
use crate::core::{
render_command::RenderCommand,
rsx,
......@@ -14,6 +16,8 @@ pub struct ClipProps {
pub children: Option<Children>,
#[prop_field(OnEvent)]
pub on_event: Option<OnEvent>,
#[prop_field(OnLayout)]
pub on_layout: Option<OnLayout>,
}
#[widget]
......@@ -29,6 +33,7 @@ pub struct ClipProps {
/// | `children` | ✅ |
/// | `styles` | ✅ |
/// | `on_event` | ✅ |
/// | `on_layout` | ✅ |
/// | `focusable` | ❌ |
///
pub fn Clip(props: ClipProps) {
......
use kayak_core::OnLayout;
use crate::core::{
render_command::RenderCommand,
rsx,
......@@ -14,6 +16,8 @@ pub struct ElementProps {
pub children: Option<Children>,
#[prop_field(OnEvent)]
pub on_event: Option<OnEvent>,
#[prop_field(OnLayout)]
pub on_layout: Option<OnLayout>,
#[prop_field(Focusable)]
pub focusable: Option<bool>,
}
......@@ -32,6 +36,7 @@ pub struct ElementProps {
/// | `children` | ✅ |
/// | `styles` | ✅ |
/// | `on_event` | ✅ |
/// | `on_layout` | ✅ |
/// | `focusable` | ✅ |
///
pub fn Element(props: ElementProps) {
......
use kayak_core::OnLayout;
use crate::core::{
render_command::RenderCommand,
rsx,
......@@ -33,6 +35,8 @@ pub struct FoldProps {
pub children: Option<Children>,
#[prop_field(OnEvent)]
pub on_event: Option<OnEvent>,
#[prop_field(OnLayout)]
pub on_layout: Option<OnLayout>,
#[prop_field(Focusable)]
pub focusable: Option<bool>,
}
......@@ -49,6 +53,7 @@ pub struct FoldProps {
/// | `children` | ✅ |
/// | `styles` | ✅ |
/// | `on_event` | ✅ |
/// | `on_layout` | ✅ |
/// | `focusable` | ✅ |
///
/// # Examples
......
......@@ -27,6 +27,7 @@ pub struct IfProps {
/// | `children` | ✅ |
/// | `styles` | ✅ |
/// | `on_event` | ✅ |
/// | `on_layout` | ❌ |
/// | `focusable` | ✅ |
///
pub fn If(props: IfProps) {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment