Coder Social home page Coder Social logo

kayak_ui's People

Contributors

adamwhitehurst avatar ahegahoe avatar asaaki avatar atornity avatar bayou-brogrammer avatar caiolc avatar christopherbiscardi avatar commander-lol avatar geerzo avatar heavyrain266 avatar icesentry avatar ickk avatar johanhelsing avatar kjolnyr avatar laocoon7 avatar maccesch avatar mrgvsv avatar mwbryant avatar nisevoid avatar noahshomette avatar piturnah avatar querat avatar raminkav avatar spider-killer556 avatar stararawn avatar therawmeatball avatar thousandthstar avatar ygg01 avatar yoshierahuang3456 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

kayak_ui's Issues

Improve Event Processing Efficiency

The Problem

Currently, when processing events we iterate through every node in the tree using down_iter:

for index in self.widget_manager.node_tree.down_iter() {

And even with #43, we still iterate through every node:

let mut stack: Vec<TreeNode> = vec![(root, 0)];
while stack.len() > 0 {
let (current, depth) = stack.pop().unwrap();

This could be much more efficient if we could smartly recurse down the tree, skipping nodes we don't need to process. Additionally, the only events that actually need to process individual nodes (as of now) are Cursor events. Keyboard and Focus events either happen on a single, known node (the focused node) or are invoked from a Cursor event.

The Issue with Smart Recursion

The main issue with performing a smart recursion down the tree for a Cursor event is that we don't always know if a parent's layout contains a descendant's. For example, a child node may overflow the parent's actual bounding box. For self-directed nodes, the parent could even have no width or height since self-directed nodes don't contribute to a parent's layout.

This is an issue because we can't just recursively select the child node that contains the cursor: we don't know if its descendants will contain it or if its actually within another part of the tree entirely.

A Possible Solution

One possible solution would be to manage a spatial hash of some type, such as a quad-tree, for the nodes. This would cache the layout of every node, allowing fast read-access when determining which nodes contain a specified point. However, it comes at the cost of additional memory overhead. It might also be an issue with animations where a layout could change very quickly in a short period of time, requiring lots of updates to the tree.

Other solutions/suggestions are welcome!

Possible Widget Restructuring

Background

So I'm working on improving the focus management and I came across an idea. However, not one that I felt comfortable just dropping a PR for without opening it up to discussion first (since it could just be a horrendous idea). So here's the problem I found and a possible solution I came up with.

Problem

A lot of internal stuff is exposed to the end user that either doesn't need to be or shouldn't be. At best it can be confusing or misleading to users, and at worst it can cause some users to use code in ways they shouldn't (i.e., unintentional hacks that are unstable and could break between versions).

For example, it seems wrong that a user can do this: self.set_id(Index::default()). And while this might not be that big of an issue ( "users should just not do that" ), what if we want to add other functionality onto widgets in the future? Maybe we add the ability to give widgets their own classes for theming. This might be confusing for the user who thinks that self.add_class("foo") should work on its own (but might not since the context also needs to be made aware of the change).

Additionally, KayakContext, has a lot of functionality that doesn't need to be exposed within the context of a widget, but needs to elsewhere (such as the KayakContext::render and KayakContext::process_events methods). Methods like these need to be public, but they're dangerous to use within a widget's render method.

So in summary the two main issues are:

  1. Internal widget data is exposed to the user in the render function in a confusing manner
  2. Error-prone KayakContext methods are also exposed to the user in the render function when they shouldn't be

Solution

I think the widget system could be restructured in a way that creates a layer of abstraction between the widgets and their context.

Let's assume we have this widget:

#[widget]
fn MyWidget(name: String, fill: Option<Color>) {
  // ...
}

Props Extraction

To get around the first issue of the widget having too much mutable access to its required widget data, we could separate the render function from the widget itself by placing the actual logic inside an associated method.

This would remove the access to a mutable self. But we still need access to the actual props. To solve this issue we can generate a new struct, MyWidgetProps, to contain the prop data. We can then pass this into the render function.

And now we have something that looks a bit more like this:

struct MyWidget {
  pub id: Index,
  pub children: Children,
  pub props: MyWidgetProps,
  // ...
}

struct MyWidgetProps {
  pub name: String,
  pub fill: Option<Color>,
}

impl Widget for MyWidget {
  fn render(&mut self, context: &mut KayakContext) {
    Self::render(self.props, context);
  }
}

impl MyWidget {
  fn render(props: MyWidgetProps, context: &mut KayakContext) {
    // Generated logic
  }
}

Widget Context

Now comes the second issue of direct access to KayakContext. I think a good way of solving this would be to generate an interface struct. This struct would act as a middleman between the render function and KayakContext, forwarding only select methods to the context.

There are two ways of going about this.

  1. Create a local struct:
struct MyWidgetContext<'a> {
  context: &'a mut KayakContext,
  widget: &'a mut MyWidget
}
  1. Create a one-size-fits all struct
struct MyWidgetContext<'a> {
  context: &'a mut KayakContext,
  widget: &'a mut dyn Widget
}

Though option 1 results in more code being generated, I actually like it better since it allows for greater customization in cases where the widget is written out manually instead of via cookie-cutter macro.

Using this interface struct, we can then replace the direct KayakContext access:

impl MyWidget { 
  fn render(mut this: MyWidgetContext) {
    // Generated logic
  }
}

We can of course call the parameter context or something. I'm just being cheeky by calling it this here ;)

And of course we'll need some code for it to be useable:

/// We could also create a trait for common methods, but this is probably fine
/// since we'll likely only ever use it internally within a particular widget
/// (in other words, not as a passable trait object)
impl<'a> MyWidgetContext<'a> {
  pub fn props(&self) -> &MyWidgetProps {
    &self.widget.props
  }
  
  pub fn set_styles(&mut self, styles: Option<Style>) {
    self.widget.styles = styles;
  }
  
  // ...
  
}

impl MyWidget {
  fn render(mut this: MyWidgetContext) {
    let MyWidgetProps {name, fill} = this.props();
    
    // Generated logic
  }
}

And again, the purpose here is to hide internals so the user isn't confused and doesn't code in a hacky way:

impl MyWidget {
  fn render(mut this: MyWidgetContext) {
    // It's now impossible to change a widget's ID internally
    self.set_id(Index::default());
    
    // You can no longer render-ception
    context.render();
    
    // This might seem like a hack to dispatch events but it's an error
    context.process_events(vec![InputEvent::MouseLeftPress]);
    
    // Now we can make this properly notify the context
    this.add_class("foo");
  }
}

TL;DR

  1. Generate MyWidgetProps
  2. Move rendering to associated function
  3. Create MyWidgetContext to interface between render function and MyWidget and KayakContext
  4. ???
  5. Profit

Alternatives

Faith

The biggest alternative, again, is just to trust that the developer isn't going to cause any issues and that we make it clear what code could cause issues. But I think we can still do something to help. And coming up with a solution also improves the overall experience and API.

Data-less Widgets

One big alternative to a portion of this is to just not store data on the widget itself. All relevant data for a widget will be stored in the KayakContext. This forces all code that needs a bit of data about a widget to also need access to the current context.

This could make code outside the functional component much more cumbersome to use.

Additionally, we run into the issue of storing the widget's ID, since that data does need to exist on the widget regardless.

Context Setters

One of the issues I briefly mentioned was about the idea that updating a widget's own data does not directly alert the context. This could be solved by just having users write the notification code:

#[widget]
fn Foo(context: &mut KayakContext) {
  self.add_class("bar");
  context.notify(KayakNotify::ClassAdded); // or however it's implemented
}

From an API perspective, this isn't great. Using the Widget Context idea, we can move that required code into the interface method:

impl<'a> FooContext<'a> {
  pub fn add_class(&mut self, class: &str) {
    self.widget.add_class(class);
    context.notify(KayakNotify::ClassAdded);
  }
}

#[widget]
fn Foo() {
  this.add_class("bar");
}

Also note that we can't do this:

#[widget]
fn Foo(context: &mut KayakContext) {
  context.add_class("bar");
}

Since this requires that Foo have a public method add_class for the context to set. And the alternatives for those issues go back to the ones listed above.

Sample Generated Output

Show/Hide Code
use crate::OnEvent;
use crate::render_command::RenderCommand;
use crate::styles::{Style, StyleProp};

mod my_widget_module {
    use derivative::Derivative;
    use crate::{Color, Index, KayakContext, OnEvent, Widget};
    use crate::styles::Style;

    #[derive(Derivative)]
    #[derivative(Default, Debug, PartialEq)]
    pub struct MyWidget {
        pub id: Index,
        pub props: MyWidgetProps,
        pub styles: Option<Style>,
        #[derivative(Default(value = "None"), Debug = "ignore", PartialEq = "ignore")]
        pub on_event: Option<crate::OnEvent>,
        #[derivative(Default(value = "None"), Debug = "ignore", PartialEq = "ignore")]
        pub children: crate::Children,
    }

    #[derive(Derivative)]
    #[derivative(Default, Debug, PartialEq)]
    pub struct MyWidgetProps {
        pub name: String,
        pub fill: Option<Color>,
    }

    pub struct MyWidgetContext<'a> {
        context: &'a mut KayakContext,
        widget: &'a mut MyWidget,
    }

    impl<'a> MyWidgetContext<'a> {
        pub fn set_styles(&mut self, styles: Option<Style>) {
            self.widget.styles = styles;
        }

        pub fn on_event(&mut self, handler: Option<OnEvent>) {
            self.widget.on_event = handler;
        }

        pub fn props(&self) -> &MyWidgetProps {
            &self.widget.props
        }
    }

    impl MyWidget {
        pub fn generate_context<'a>(&'a mut self, context: &'a mut KayakContext) -> MyWidgetContext<'a> {
            MyWidgetContext {
                widget: self,
                context,
            }
        }
    }

    impl Widget for MyWidget {
        fn get_id(&self) -> Index {
            self.id
        }

        fn focusable(&self) -> bool {
            true
        }

        fn set_id(&mut self, id: Index) {
            self.id = id;
        }

        fn get_styles(&self) -> Option<Style> {
            self.styles.clone()
        }

        fn get_name(&self) -> String {
            String::from("MyWidget")
        }

        fn on_event(&mut self, context: &mut KayakContext, event: &mut crate::Event) {
            let this = self.generate_context(context);
            if let Some(on_event) = this.widget.on_event.as_mut() {
                if let Ok(mut on_event) = on_event.0.write() {
                    on_event(this.context, event);
                }
            }
        }

        fn render(&mut self, context: &mut KayakContext) {
            Self::render(self.generate_context(context));
        }
    }
}

use my_widget_module::{MyWidget, MyWidgetContext, MyWidgetProps};
// Outside the module so it doesn't have access to the internals
impl MyWidget {
    pub fn render(mut this: MyWidgetContext) {
        let MyWidgetProps { fill, name } = this.props();

        this.set_styles(Some(Style {
            render_command: StyleProp::Value(RenderCommand::Empty),
            ..Default::default()
        }));

        this.on_event(Some(OnEvent::new(move |_, evt| {
            println!("Event fired!");
        })));

        // ...
    }
}

Feedback

This is all just me thinking of ways to improve on these issues. Are they worth the trouble? And would we stand to gain from implementing a solution like this? I think so, but I could be totally and completely wrong. There could also be other, better solutions than the one I came up with.

Anyways, just wanted to bring this up and see what people thought. So let me know!

Keep Morphorm or swap over to Taffy?

Should we keep the current Morphorm layout integration or should we move over to taffy for layouts? I'm hoping the community can help make a decision here. Morphorm hasn't been updated recently but Taffy is being actively developed.

Impl Add for Style?

in web development you can have many small styling components that are added together when building a component. (e.g.:

Not sure if theres a way to "add" different styles to form a more complex one in kayak_ui but doing so would be amazing (and would greatly improve code reusability imho).

Such implementation would compare two styles (mut self & style_b) and replace certain fields of struct self whenever such parameters are "default" and B provides diferent values. The following code does not compile, but the basic logic would be as follows:

impl Add for Style {
    type Output = Style;

    fn add(self, rhs: Style) -> Style {
        let mut resulting_style = Style::default();
        for i in Style::default().keys.iter() {
            if resulting_style.i == StyleProp::Default {
                resulting_style.i = rhs.i;
            }
        }
        resulting_style
    }
}

pub fn center_align() -> Style {
    Style {
        bottom: StyleProp::Value(Units::Stretch(1.0)),
        left: StyleProp::Value(Units::Stretch(1.0)),
        top: StyleProp::Value(Units::Stretch(1.0)),
        right: StyleProp::Value(Units::Stretch(1.0)),
        ..Default::default()
    }
}

pub fn main_background_color() -> Style {
    Style {
        background_color: StyleProp::Value(Color::BLACK),
        ..Default::default()
    }
}

// this complex style would have bottom, left, top, right & background_color defined. other fields remain default.
pub fn complex_style() -> Style {
    center_align() + main_background_color()
}

Not sure if there's already a way to achieve this in kayak_ui. Cheers!

Note: I know it is not possible to iter through struct fields (as opposed to python dicts, for example) but I saw some comments about using serde to Impl iterable to struct. Maybe this could be an optional feature, since it could add some external dependencies...

Support for bevy 0.7

Is there a specific problem with using this with bevy 0.7? I've successfully been using it with 0.7 in 2 projects but I want to make sure there's not a pitfall awaiting me

Minus symbol doesn't seem to be resolved correctly

I modified the counter example to add a -1 button like this:

    let (count, set_count, ..) = use_state!(0i32);
    let on_add = {
        let set_count = set_count.clone();
        OnEvent::new(move |_, event| {
            if event.event_type == EventType::Click {
                set_count(count + 1)
            }
        })
    };

    let on_sub = OnEvent::new(move |_, event| {
        if event.event_type == EventType::Click {
            set_count(count - 1)
        }
    });

    rsx! {
        <>
            <Window position={(50.0, 50.0)} size={(300.0, 300.0)} title={"Counter Example".to_string()}>
                <Text styles={Some(text_styles)} size={32.0} content={format!("Current Count: {}", count)}>{}</Text>
                <Button on_event={Some(on_add)}>
                    <Text styles={Some(button_text_styles)} size={24.0} content={"+1".to_string()}>{}</Text>
                </Button>
                <Button on_event={Some(on_sub)}>
                    <Text styles={Some(button_text_styles)} size={24.0} content={"-1".to_string()}>{}</Text>
                </Button>
            </Window>
        </>
    }

When rendering this, the - symbol is never rendered, but there's a space to the left of the number.
counter_i4zviGNcGB

Add opacity

Problem

Currently, there's no way to set the opacity of a widget. This is important when creating overlays and other UI effects.

Solution

Add an opacity style property. This property should match the effect of CSS's opacity. It should:

  • Range from 0.0 (inclusive) to 1.0 (inclusive)
  • Default to an initial value of 1.0
  • Apply to not only itself, but all descendants
    • Values stack: a descendant's opacity is compounded by all ancestors' opacities
      • This includes inherited opacities

Additional Considerations

The W3 specs state that opacities less than 1.0 create a new stacking context. This is because you can't position elements between a non-opaque element and its children (see this example).

I'm not sure how we resolve z-indexes and such so this may or may not be something we need to consider as well. We also may not even need to enforce this if we can properly handle the scenario stated above.

Fatal error when rendering to target with different size than window

I have an app which uses postprocess, so renders to an image texture. Though, the image texture is not 1:1 with the window size, and instead is scaled down (for a post processing effect).

Spawning a UICameraBundle with the target set to this image texture causes a fatal panic to occur when the game opens.

2022-08-27T13:33:19.585318Z ERROR wgpu::backend::direct: Handling wgpu errors as fatal by default    
thread 'main' panicked at 'wgpu error: Validation Error

Caused by:
    In a RenderPass
      note: encoder = `<CommandBuffer-(0, 106, Vulkan)>`
    In a set_scissor_rect command
    Scissor Rect { x: 0, y: 0, w: 1280, h: 720 } is not contained in the render target Extent3d { width: 640, height: 360, depth_or_array_layers: 1 }

', /home/ari/.cargo/git/checkouts/wgpu-a5140d1891ddccc5/a703a78/wgpu/src/backend/direct.rs:2391:5
note: run with `RUST_BACKTRACE=1` environment variable to display a **backtrace**

This issue does not occur with the built-in bevy cameras, but does occur with Kayak UI.

Add `rustfmt.toml`

To better maintain style consistency within the codebase, a rustfmt.toml file should be added to allow automatic formatting of code.

Change Cursor

Right now bevy doesn't expose winit's set_cursor_icon function. We need to enable that functionality in bevy to show different cursors. Then we can hook into the events to change the cursor icon.

examples don't work

I am trying to run the examples (unmodified), but every example results in the attached screenshot.
There is no error message unfortunately, the app seems to run despite not showing anything.
I am running windows 10. So far, I tried the examples "counter", "todo" and "text_box".
Any clue on how to make it work?

image

Support Vec widgets.

Per title we should support Vec widgets like:

<SomeWidget>
  {some_vec.iter().map(|number| {
    rsx! {
      <Text size={30.0} content={format!("Hello World {}!", number)} />
    }
  }).collect()}
</SomeWidget>

Clear Data for Removed Widgets

Problem

Currently, when a widget is removed, it leaves behind pretty much all data associated with it. This is because we typically store that data based on its Index, which can continue to exist after the widget is removed.

There should be a way for this data to be cleaned up. Doing so will also have the added benefit of granting the option of responding to the removal (such as for cleanup functions described in #45) via a Drop impl (presumably).

Possible Solutions

So far I can only think of these two suggestions (but there's probably a better way to do it):

  1. Using diffing (not sure if #1 has been resolved or not), we could get a list of all the Change::Deleted nodes. Then we just iterate over them and manually remove the necessary data from KayakContext. This method is probably slower and more cumbersome, but probably much simpler to implement with adequate diffing.
  2. Utilize a crate like weak_table, to create the data HashMap and HashSet fields. Then add a trait method to Widget allowing it to return a reference to its Index (or maybe a custom wrapper struct). This way we could use a Weak<Index> as the key to automatically drop the stored data when the widget is removed. This way is a bit cleaner, but adds a dependency and requires changing a lot more code to integrate it. (Just did some quick testing and I guess widgets are dropped every render? I assume when rebuilding the tree? So maybe this whole idea wouldn't work anyways...)

Not sure how to best approach this ๐Ÿค”

UICameraBundle Issues

Piggybacking on my previous issue, I have a state flow setup to go from asset_load -> menu -> ingame. Each with a different UI layout.

If I do not inject a UICameraBundle at startup, but instead inject this when building says the main_menu UI, it offsets it towards the top left of the screen.

CleanShot 2022-11-14 at 12 50 18@2x

If instead, I inject a UICameraBundle at startup, then manipulate the RootContext via the enter system of main_menu

pub fn setup_main_menu(
    mut commands: Commands,
    textures: Res<TextureAssets>,
    asset_server: Res<AssetServer>,
    mut font_mapping: ResMut<FontMapping>,
    mut ctx: Query<(Entity, &mut KayakRootContext)>,
) {
    if let Ok((root_entity, mut widget_context)) = ctx.get_single_mut() {
        font_mapping.set_default(asset_server.load("fonts/lato/lato-light.kayak_font"));

        widget_context.add_plugin(KayakWidgetsContextPlugin);
        ...

Then the menu is correctly centered within the screen.

CleanShot 2022-11-14 at 12 52 03@2x

Support bevy 0.8

In the readme it says 0.7. Is this ready for the latest version?

Macos scaled display vs Image widget

When using a macos m1 retina screen, or scaled versions of non-retina screens, the origin, xpos, ypos, width, and height of Image widgets are multiplied by 2. More specifically, the dpi value here that was recently added increases from 1.0 to 2.0.

It seems that this dpi value being force-set to 1.0 fixes the issue, but I have no idea if this is a valid fix.

ninepatch widgets are not affected, so I've added one to the image example code to take some screenshots.

image

image

adjusted image.rs example
use bevy::{
    prelude::{App as BevyApp, AssetServer, Commands, Handle, Res, ResMut},
    window::WindowDescriptor,
    DefaultPlugins,
};
use kayak_core::styles::{PositionType, Edge};
use kayak_ui::{bevy::{BevyContext, BevyKayakUIPlugin, ImageManager, UICameraBundle}, widgets::NinePatch};
use kayak_ui::core::{
    render,
    styles::{Corner, Style, StyleProp, Units},
    Index,
};
use kayak_ui::widgets::{App, Image};

fn startup(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    mut image_manager: ResMut<ImageManager>,
) {
    commands.spawn_bundle(UICameraBundle::new());

    let handle: Handle<bevy::render::texture::Image> = asset_server.load("generic-rpg-vendor.png");
    let ui_image_handle = image_manager.get(&handle);

    let panel_handle: Handle<bevy::render::texture::Image> = asset_server.load("panel.png");
    let panel_image_handle = image_manager.get(&panel_handle);

    
    let context = BevyContext::new(|context| {
        let image_styles = Style {
            position_type: StyleProp::Value(PositionType::SelfDirected),
            left: StyleProp::Value(Units::Pixels(100.0)),
            top: StyleProp::Value(Units::Pixels(10.0)),
            border_radius: StyleProp::Value(Corner::all(500.0)),
            width: StyleProp::Value(Units::Pixels(200.0)),
            height: StyleProp::Value(Units::Pixels(182.0)),
            ..Style::default()
        };
        let nine_patch_styles = Style {
            width: StyleProp::Value(Units::Pixels(512.0)),
            height: StyleProp::Value(Units::Pixels(512.0)),
            ..Style::default()
        };

        render! {
            <App>
                <NinePatch
                    styles={Some(nine_patch_styles)}
                    border={Edge::all(15.0)}
                    handle={panel_image_handle}
                >
                </NinePatch>
                <Image styles={Some(image_styles)} handle={ui_image_handle} />
            </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();
}

Grid Layout: Index out of bounds

I try to display 15 Buttons as a grid. The layout works as column or row, but as soon as I change the layout to grid, the app crashes.
My code looks like this:

     let items_ui = Style {
        layout_type: StyleProp::Value(LayoutType::Grid), // LayoutType::Column works fine
        ..ui_props.clone().styles.unwrap_or_default()
    };

    rsx! {
        <Element styles={Some(items_ui)}>
        {VecTracker::from(handles.iter().map(|(_, h)| {
            constructor! {
                <Item event_type=
                {UIEventType::None}
                handle={Some(h.clone())} />
            }
        }))}
        </Element>
    }

The error I receive is this:

thread 'main' panicked at 'index out of bounds: the len is 2 but the index is 2', C:\Users\Admin.cargo\git\checkouts\morphorm-b1c9411b3870fca5\1243152\src\layout.rs:1374:37

the code at the described position in layout.rs is this:

let col_start = 2 * node.col_index(store).unwrap_or_default() + 1;
let col_span = 2 * node.col_span(store).unwrap_or(1) - 1;
let col_end = col_start + col_span;

let new_posx = col_widths[col_start].0;
let new_width = col_widths[col_end].0 - new_posx; // this is where the error occurs

let new_posy = row_heights[row_start].0;
let new_height = row_heights[row_end].0 - new_posy;

This looks like a bug to me, but maybe I don't fully understand the layout mechanism.
Any Idea on how to fix the error?

Pass attributes on functional widgets

Problem

Attempting to add a doc comment on a functional widget doesn't actually pass the doc comment to the generated widget struct. It would make sense if attributes (like doc comments) were also passed to the generated widget.

For example, the following generates a MyWidget struct without passing along the doc comment:

#[widget]
/// Some doc comment about My Widget
fn MyWidget() {
  // ...
}

Sidenote: Passing attributes might also be good for providing additional functionality to a created widget struct, including the usage of custom attributes.

This might be more of a consideration for #56 (if it's even possible, I'm not super proficient with proc macros yet), but it might also be a tangential issue to that RFC, which is why I'm bringing it up here.

Re-enable Diffing

Issue

Currently diffing is turned off. This is because of the following problem:

A user creates Widgets A > B > C where C is a child of B and B is a child of A.

Widget A state changes and passes new props to C but not to B. Widget A gets marked as dirty and is re-rendered.

When re-rendering the diff for A says to re-render my children. So it re-renders B. B does a diff check and nothing has changed so it will not re-render C even though C has changed.

Possible solutions:

Always attempt to re-render all children if parent has been marked dirty.
Pros:

  • Easy to implement

Cons:

  • We should attempt to re-render B in this scenario. Performance will suffer for larger situations.

Diff props to children separately in parent and build a re-render list.
Pros:

  • Faster re-rendering

Cons:

  • Harder to implement.
  • Requires a list of props that are passed to child widgets list. Not easy to get.

Others?

UI getting clipped

Problem

For some reason the UI is getting clipped at 50% width and 50% height of the window:

clipped

I'm not sure if this is another DPI issue or an OS issue.

Details

Machine: MacBook Pro (15-inch, 2019)
OS: macOS 11.6 Big Sur
Display: 15.4-inch (2880 ร— 1800)

nine_patch flips the border vertically

If you replace the nine_patch image example with this image.

metalPanel_blueCorner

you can see that the border is flipped in the y direction (and that the border seems to be stretched.

image

This also happens with the default nine_patch image example, but it is harder to see.

panel

If you focus on the bordering red/black lines you can see the top-left corner in the origin image is the bottom-left corner in the rendering.

image


Additionally, the measurements seem to be off. In both cases the Edge::all needs to be doubled to fit the right size of pixels (from 30 to 60 for the new image, and from 15 to 30 for the old), and even then the borders appear stretched.

ex: new image at Edge::all(30.0) (which should be the right pixel count for the border).

image

and Edge::all(60.0)

image

The panel I was expecting looks more like this (it's a kenney asset from the space ui set)

Sample

Mobile Support - iOS

Hi there,

I've been watching this project for a few weeks and notice that the rendering uses bevy which has done a good job at keeping iOS and Android as supported platforms.

Would you be interested in iOS support. I can tell you that some of the examples to this point work in the iOS simulator via:

cargo dinghy --platform auto-ios-aarch64-sim run --example text_box 
rm -r target/aarch64-apple-ios-sim/debug/dinghy/text_box/Dinghy.app/resources/
xcrun simctl install booted ./target/aarch64-apple-ios-sim/debug/dinghy/text_box/Dinghy.app/
xcrun simctl launch booted Dinghy

You need to have an iOS simulator booted of course.

image

Click/taps don't seem to work as I think those are different winit events. Also, text input doesn't work because it's pretty specific to iOS.

Not rendering anything

I am trying to render an ui when I get in a specific state, but for some reason nothing is getting rendered in the window.

This is my App configurations
image

And this is my system which should render some texts
image

I am using this commit version 135e513e8d7f197d04dacfb1c5e8c48db3cb5621

I'm on windows 11, nvidia graphic card and intel processor

Removing any elements from KayakApp causes a panic

Using the hello_world as an example, if I have the following setup:

fn startup(
    mut commands: Commands,
    mut font_mapping: ResMut<FontMapping>,
    asset_server: Res<AssetServer>,
) {
    font_mapping.set_default(asset_server.load("roboto.kayak_font"));

    let mut widget_context = KayakRootContext::new();
    widget_context.add_plugin(KayakWidgetsContextPlugin);
    let parent_id = None;
    rsx! {
        <KayakAppBundle>
            <TextWidgetBundle
                text={TextProps {
                    content: "Hello World".into(),
                    size: 20.0,
                    ..Default::default()
                }}
            />
        </KayakAppBundle>
    }

    commands.spawn(UICameraBundle::new(widget_context));
}
fn remove_child(
    mut commands: Commands,
    keys: Res<Input<KeyCode>>,
    q: Query<Entity, With<TextProps>>,
) {
    if keys.just_pressed(KeyCode::Space) {
        q.iter().for_each(|e| {
            commands.entity(e).despawn_recursive();
        });
    }
}

And if I say remove the TextWidgetBundle from the KayakApp, it causes a panic. This goes for anything under the RootContext, if i try to remove KayakApp it panics as well

Proper widget hovering

Right now we get an event for hovering, but that's not so useful. Instead we should have two events:
MouseIn and MouseOut

This will allow users to change widgets based on if the mouse is inside of the widget(Hover effects).

Better widget state management.

Currently the widget state management is very simple. It works but it's a little weird at times. Example of a weird scenario:

    let button_id = self.get_id();
    let on_event = OnEvent::new(move |context, event| match event.event_type {
        EventType::Click => {
            dbg!("Clicked!");
            context.set_current_id(button_id);
            context.set_state::<u16>(blue_button_hover_handle);
        }
        _ => {
            context.set_state::<u16>(blue_button_handle);
        }
    });

Because OnEvent is set on a child we have to set the current widget id before calling set state here. This isn't obvious as behind the scenes we call set_current_id This likely will cause issues later on. Instead we should have a better way of handling state associated with a particular widget. One idea might be to have something like:

let widget_state = context.get_widget_state(widget_id);

but we would have to somehow pass this to the closure. We might be able to use an Arc<RwLock<State>>..

Iterating over HashMap in rsx! macro

Hello, I've been trying to figure this out all day and figured it would be worth asking here. I'm working on a game in bevy and would like to output a list of inventory items. I have an Inventory struct that contains a HashMap<Enum(ItemType), f32> that houses the inventory's item type and it's respective weight.

I've been struggling getting the VecTracker to work with an Iterator on the HashMap as it yields this rust compile error when trying to do so:

   --> src\main.rs:141:5
    |
131 |       let inventory_items = inventory.get().items;
    |           --------------- captured outer variable
...
141 | /     rsx! {
142 | |         <Window position={(1080.0, 0.0)} size={(200.0, 300.0)} title={"Inventory".to_string()}>
143 | |             <Element>
144 | |                 {VecTracker::from(inventory_items.iter().map(|item| {
    | |                                   ---------------
    | |                                   |
    | |                                   variable moved due to use in closure
    | |                                   move occurs because `inventory_items` has type `bevy::utils::hashbrown::HashMap<AstroidMaterial, f32>`, which does not implement the `Copy` trait
...   |
150 | |         </Window>
151 | |     }
    | |     ^
    | |     |
    | |_____captured by this `Fn` closure
    |       move out of `inventory_items` occurs here
    |
    = note: this error originates in the macro `rsx` (in Nightly builds, run with -Z macro-backtrace for more info)

Here's the code that results in the above compilation error:

commands.insert_resource(bind(Inventory {items: HashMap::new()}));
let inventory = context.query_world::<Res<Binding<Inventory>>, _, _>(move |inventory| inventory.clone());
context.bind(&inventory);

let inventory_items = inventory.get().items;


rsx! {
    <Window position={(1080.0, 0.0)} size={(200.0, 300.0)} title={"Inventory".to_string()}>
        <Element>
            {VecTracker::from(inventory_items.iter().map(|item| {
                constructor! {
                    <Text content={format!("{:?}", item.clone())} size={16.0} />
                }
            }))}
        </Element>
    </Window>
}

Please help with any guidance! I've been hitting my head all day trying to figure this out and I'm pretty sure it's my incomplete understanding of rust that's causing the headache. I appreciate the work being done on this project and any time you take to provide guidance, thanks!

Limit bevy features for faster builds

Currently, kayak_ui uses full bevy in each crate, which forces you to build all unecessary features like scene loader, audio, default ui and many more which causes really long builds when kayak_ui is added as dependency in your project.

General Cleanup

  1. Cleanup code that is no longer used.
  2. Cleanup examples
  3. Cleanup warning messages.
  4. Figure out if we are causing the vulkan validation errors/warnings or if its a bevy or wgpu issue. Example error:
ERROR wgpu_hal::vulkan::instance: VALIDATION [VUID-VkImageViewCreateInfo-image-04441 (0xb75da543)]
        Validation Error: [ VUID-VkImageViewCreateInfo-image-04441 ] Object 0: handle = 0x96fbe2000000005e, type = VK_OBJECT_TYPE_IMAGE; | MessageID = 0xb75da543 | Invalid usage flag for VkImage 0x96fbe2000000005e[] used by vkCreateImageView(). In this case, VkImage should have VK_IMAGE_USAGE_[SAMPLED|STORAGE|COLOR_ATTACHMENT|DEPTH_STENCIL_ATTACHMENT|INPUT_ATTACHMENT|TRANSIENT_ATTACHMENT|SHADING_RATE_IMAGE|FRAGMENT_DENSITY_MAP]_BIT set during creation. The Vulkan spec states: image must have been created with a usage value containing at least one of the usages defined in the valid image usage list for image views (https://vulkan.lunarg.com/doc/view/1.2.189.2/windows/1.2-extensions/vkspec.html#VUID-VkImageViewCreateInfo-image-04441)

Render UI To Texture & Multiple Apps

Draft: Render a Kayak UI to a Texture

I've got a use case where I'd like to render multiple Kayak UI Applications and Mount them in world space. As an example having computer kiosks in the world

I was on the fence about making these two separate pull requests but mounting an App in worldspace \ render texture would almost definitely be something done in addition to mounting a HUD to the UI camera as well.

Having Multiple Kayak Apps

This would require making BevyContext a component instead of a Resource and providing a system that will call render on each BevyContext. Will also require updating process_events to consume BevyContext components and likely we'll need to create an IndexResource.

Another wrinkle is handling input on WorldSpace UI's. A basic solution could be to use bevy_mod_picking to raycast on click and create a synthetic click event if the clicked object has a BevyContext component.

We can address keyboard inputs to the last clicked UI in worldspace, or if the last click was not on a worldspace UI we can fallback to the UICamera App. We'll probably want to provide the world space UI's some mechanism for automatically yielding an input if camera is out of a certain radius, or possibly a more generic predicate provided by the user

Rendering Multiple Apps

Our initial thought is to have the BevyApp render to a texture and map provide that texture to the User to place within the world as they wish.

asset_server loads font as .png to fontMapping when in another Bevy app

Expected

Run Bevy example(https://github.com/StarArawn/kayak_ui/blob/main/examples/bevy.rs) in own repo.

Tried:

Error

CleanShot 2022-01-03 at 10 34 34

2022-01-03T09:34:19.890068Z  WARN bevy_asset::asset_server: encountered an error while reading an asset: path not found: /Users/robin/code/bevy/galactic/client/assets/roboto.png
thread 'main' panicked at 'attempt to subtract with overflow', /Users/robin/.cargo/git/checkouts/kayak_ui-299a0b6ac8320811/e25baa9/bevy_kayak_ui/src/render/unified/pipeline.rs:595:25
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Re-produce steps:

Version

kayak_ui = { git="https://github.com/StarArawn/kayak_ui", rev="e25baa9d0ae0cbb63c07e1f3153af2da5c69b8f4" } (branch update-bevy)
bevy = {git = "https://github.com/bevyengine/bevy.git", commit = "1d0d8a3397bd6fc2c14d42ffd0668d2443748912"}

Spinbox Widget

What are spinboxes?

Spinbox is a number-only TextBox with a special +/- or </> near it. Some examples:

QT Spinbox
firefox_fGY6qqvzan

Caves of Qud character creation
CoQ_znbstOTZFH

Why use them?

They are ubiquitous, and they are needed in various places like character creation and UI elements that usually want small changes based on increment/decrement.

Design considerations

  • Some way to customize increment decrement chars
  • Perhaps some way to define how buttons can be placed grouped some small mockup I made
    Aseprite_Td5jPYUaG8
  • Ways to validate the values and prevent change; like for example, attributes can't go below ten or over twenty

Clip breaks on wide windows

Problem

Clipping via the Clip widget seems to stop working when the window is resized too wide. On my machine it happens when the window is sized with a width of 1387 or higher:

Width: 1386 Width: 1387
clip-1386 clip-1387

This happens whether the window is sized via WindowDescriptor or manually resized. And from my tests, it occurs regardless of window height or widget size (obviously as long as it overflows the Clip widget).

Specs

OS: macOS Big Sur (11.6)
rustc: 1.58.1
Kayak Version: 4b45882 137c421 (pre-release)


Edit: It seems I put the wrong commit for Kayak Version. That commit is after the bevy_kayak_render move which seems to have broken clipping altogether (at least on my machine).

Shader structs aren't 16-byte aligned

When running the latest version of kayak_ui on the web, the app will throw an error at startup complaining that the flag BUFFER_BINDINGS_NOT_16_BYTE_ALIGNED is not supported. Tracking down the docs for wgpu, this flag is simply not supported by webgl

Basically, the structs used in unified_pipeline shader (shader.wgsl) aren't laid out in a multiple of 16 bytes. One possible solution would be to pad the structs with unused data to make sure they're a multiple of 16 - I was trying to get this working, but ended up hitting a knowledge gap

More information about alignment & sizes in this Learn Wgpu article and in the W3 spec for WGSL

It seems a bit odd that this wouldn't have come up before, but I'm also quite unfamiliar with wgsl and shaders in general - there may be something else that is causing this

The specific error message I get (Chrom on Ubuntu 22.04):

Screenshot from 2022-08-19 00-24-07

Font rendering on wasm/web?

Heya!

Is kayak's font rendering supposed to work on wasm? Everything else seems to work (ninepatch, on_event, styles, etc), but the fonts aren't rendering at all in the wasm port of my game. I also don't see any errors in the console and such.

The buttons here should have some text on them.

image

Improve font support by allowing more than one font.

Currently a lot of the font stuff is hard coded. To fix this we need to:

  1. Make KayakFont an asset that loads in a png/json file.
  2. Get rid of kayak_font crate since it doesn't really work at the moment...
  3. Define some sort of FontManager to link font_id's to bevy KayakFont assets. Similar to the image manager.

Panic When Accessing Resource via Context

I'm not entirely sure if I'm doing something wrong here (still pretty new to rust) but I'm getting a panic when trying to pull an initialized resource out of kayak's BevyContext::query_world. If I get rid of all the kayak_ui stuff, the resource is present as expected.

error

Initial seed: 3485847679
creating ui
getting seed from res
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: NoSuchResource(NoSuchResource)', ~\
.cargo\git\checkouts\kayak_ui-299a0b6ac8320811\f90c054\kayak_core\src\context.rs:677:71
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\debug\examples\kayak_res_panic.exe` (exit code: 101)

I did my best to try and cut it down to a bare bones reproduction.

cargo.toml

bevy = "0.6.1"
kayak_ui = { git="https://github.com/StarArawn/kayak_ui", rev="f90c0541ea1b8016ccf44904c426ce70e1c9a2aa", features = ["bevy_renderer"] }
rand = "0.8.5"
rand_xoshiro = "0.6.0"

panic.rs

use bevy::prelude::{App as BevyApp, Commands, Res, SystemSet};
use bevy::DefaultPlugins;
use kayak_ui::bevy::BevyKayakUIPlugin;
use kayak_ui::widgets::{App, Text};
use kayak_ui::{
    bevy::BevyContext,
    core::{render, Index},
};
use rand::Rng;
use rand_xoshiro::{rand_core::SeedableRng, Xoshiro256PlusPlus};

#[derive(Clone, Eq, PartialEq, Debug, Hash)]
enum GameState {
    Playing,
}

fn create_ui(mut commands: Commands) {
    println!("creating ui");

    let context = BevyContext::new(|context| {
        println!("getting seed from res");

        let seed = context.query_world::<Res<RandomResource>, _, u32>(|c| c.seed);

        println!("got seed: {}", seed);

        render! {
            <App>
                <Text content={format!("Seed: {}", seed)} size={12.0} />
            </App>
        }
    });

    commands.insert_resource(context);
}

struct RandomResource {
    seed: u32,
}

impl Default for RandomResource {
    fn default() -> Self {
        let mut rng = Xoshiro256PlusPlus::seed_from_u64(1);
        let init_seed = rng.gen::<u32>();
        println!("Initial seed: {}", init_seed);

        RandomResource { seed: init_seed }
    }
}

fn main() {
    BevyApp::new()
        .init_resource::<RandomResource>()
        .add_state(GameState::Playing)
        .add_plugins(DefaultPlugins)
        .add_plugin(BevyKayakUIPlugin)
        .add_system_set(SystemSet::on_enter(GameState::Playing).with_system(create_ui))
        .run();
}

I tried to make a nearly identical example without kayak that doesn't panic to make sure I wasn't going crazy.

no_panic.rs

use bevy::prelude::*;
use rand::Rng;
use rand_xoshiro::{rand_core::SeedableRng, Xoshiro256PlusPlus};

#[derive(Clone, Eq, PartialEq, Debug, Hash)]
enum GameState {
    Playing,
}

fn create_ui(res: Res<RandomResource>) {
    println!("getting seed from res");

    let seed = res.seed;

    println!("got seed: {}", seed);
}
struct RandomResource {
    seed: u32,
}

impl Default for RandomResource {
    fn default() -> Self {
        let mut rng = Xoshiro256PlusPlus::seed_from_u64(1);
        let init_seed = rng.gen::<u32>();
        println!("Initial seed: {}", init_seed);

        RandomResource { seed: init_seed }
    }
}

fn main() {
    App::new()
        .init_resource::<RandomResource>()
        .add_state(GameState::Playing)
        .add_plugins(DefaultPlugins)
        .add_system_set(SystemSet::on_enter(GameState::Playing).with_system(create_ui))
        .run();
}

Allow custom cursors.

It should be fairly easy to allow for custom cursors instead of using the default OS's cursors.

Refutable binding for `Option<T>`

Adding an optional prop to a widget via Option<T> sometimes fails (although it works for Strings).

i32

#[widget]
pub fn Button(children: Children, styles: Option<Style>, foo: Option<i32>) { /* ... */ }

And used as:

<Button on_event={Some(on_event)} foo={Some(12)}>

Results in the error:

refutable pattern in local binding: `i32::MIN..=11_i32` and `13_i32..=i32::MAX` not covered

bool

#[widget]
pub fn Button(children: Children, styles: Option<Style>, foo: Option<bool>) { /* ... */ }
<Button on_event={Some(on_event)} foo={Some(false)}>
refutable pattern in local binding: `true` not covered

String

Using an optional String seems to work:

#[widget]
pub fn Button(children: Children, styles: Option<Style>, foo: Option<String>) { /* ... */ }
// Can't do `Some("".to_string())` here
<Button on_event={Some(on_event)} foo={Some(my_string)}>

No error.

Panic when resizing application window

On commit: 81fc2a4

When resizing any window the application panics with the following message:

wgpu::backend::direct: Handling wgpu errors as fatal by default
thread 'main' panicked at 'wgpu error: Validation Error

Caused by:
    In a RenderPass
      note: encoder = `<CommandBuffer-(0, 11974, Vulkan)>`
    In a set_scissor_rect command
    Invalid ScissorRect parameters

Kayak-UI Panics if KayakRootContext doesn't exist

If you have a very basic bevy setup and just add the kayak-ui plugins without having a startup system that injects the KayakRootContext into bevy resources, it causes a panic.

thread 'main' panicked at 'Resource requested by kayak_ui::widgets::add_widget_systems does not exist: kayak_ui::context::KayakRootContext', /Users/elkxj/.cargo/registry/src/github.com-1ecc6299db9ec823/bevy_ecs-0.9.0/src/system/system_param.rs:520:17
note: run with RUST_BACKTRACE=1 environment variable to display a backtrace

let mut app = App::new();
...
app.add_plugins(DefaultPlugins).add_plugin(KayakContextPlugin).add_plugin(KayakWidgets);
...
app.run();

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.