Kludgine is now a blend of an app and game engine

Things are coming along nicely for Kludgine. I finished merging a large portion of UI-related functionality.

The journey to get here was a long one. I had only one major stumble along the way. I decided to attempt to wrap stretch into an async interface by communicating via channels. It worked, but unfortunately, stretch was not behaving in a way that I could understand. From my debugging, I seemed to validate that this comment on an issue in github was a legitimate issue. But after thinking about just ignoring it, I realized another major limitation of stretch.

Stretch’s resulting Layout type is essentially a single Rect. There is no way to extract padding or margin information, even though these values are needed under the hood. If you want to support rendering the background color of an element, you cannot include the padded area with the API currently exposed by stretch.

The library itself seems to be not actively maintained by looking at the issues and the fact that as of writing this, the last commit on the main branch was May, and the previous cargo release was just over a year ago. These factors all made it a relatively straightforward decision just to go my own way.

Without further adieu, the current state of the API:

Components vs. Primitives

Components are types that encompass functionality over a region of space on a Window. Components can contain other components, and the Component trait provides various methods that allow you to encapsulate functionality.

For example, I have written two Components – Label and Image. Image renders a Sprite, and it looks like this:

#[async_trait]
impl Component for Image {
    type Message = ();

Components operate on message passing. The concept isn’t flushed out yet; this is just a test of my ability to properly encapsulate the Message type so that each Component could have its own set of messages, just like in yew.

    async fn update(&mut self, context: &mut SceneContext) -> KludgineResult<()> {
        self.current_frame = Some(
            self.sprite
                .get_frame(context.scene().elapsed().await)
                .await?,
        );
        Ok(())
    }

The update function is called once per frame rendered, and you can mutate your state within this method. The Image component uses this moment to grab the current frame of animation.

    async fn render(&self, context: &mut StyledContext, location: &Layout) -> KludgineResult<()> {
        if let Some(frame) = &self.current_frame {
            frame
                .render_at(context.scene(), location.inner_bounds().origin)
                .await
        }
        Ok(())
    }

The render function is where your Component draws its contents, using primitive drawing operations. You receive information about the layout of your components, and you can access the scene through the Context.

    async fn content_size(
        &self,
        _context: &mut StyledContext,
        _constraints: &Size<Option<f32>>,
    ) -> KludgineResult<Size> {
        if let Some(frame) = &self.current_frame {
            Ok(frame.location().await.size.into())
        } else {
            Ok(Size::default())
        }
    }
}

The last thing that needs to be implemented is either content_size or layout, which segues nicely into the next topic.

Pluggable layout systems

In getting frustrated with debugging stretch, I started thinking about how the CSS system is overly complicated. Flexbox is cool, and CSS Grid is cool, but they are insanely complicated. I think this stems from a design decision of having all of these properties that are related to each other but function differently in different contexts. Programming with these complicated tools is very tricky. It’s possible that stretch was behaving correctly, and I just misunderstood how everything was supposed to work!

I realized that there should be a way to abstract away the concept of computing a layout so that you could be expressive when laying out the contents of your component. Here’s the current example from the repository:

    async fn layout(
        &mut self,
        _context: &mut StyledContext,
    ) -> KludgineResult<Box<dyn LayoutSolver>> {
        Layout::absolute()
            .child(
                self.label.unwrap(),
                AbsoluteBounds {
                    left: Dimension::Points(32.),
                    right: Dimension::Points(64.),
                    top: Dimension::Points(32.),
                    bottom: Dimension::Points(64.),
                    ..Default::default()
                },
            )?
            .child(
                self.image.unwrap(),
                AbsoluteBounds {
                    right: Dimension::Points(10.),
                    bottom: Dimension::Points(10.),
                    ..Default::default()
                },
            )?
            .layout()
    }

The above code produces this layout:

What I like about this system is that responsiveness can be done in multiple ways. If you want to have absolute layouts, but change how they are laid out if the device is too narrow, just put an if statement in there.

The way this is powered is by a simple trait, LayoutSolver, and by providing essential tools that enable creating powerful layout helpers. One of the upcoming projects will be to implement a Grid LayoutSolver.

Getting back to the teaser from before, you’ve now seen implementations of both layout and content_size. This is the first set of basic abstractions. By default, a Component implements content_size as directly returning the full measurements of the constraints provided. For components that contain other components, this is likely the best default. Internally, I refer to these components as “leaf” components – they are leaves in the tree structure.

The function layout is also implemented by default, providing no layout information. For components that implement the render method to draw primitives, most likely will not use the layout function, and instead, provide content_size hints – such as what both Label and Image do.

What’s next?

I need to brainstorm a little more about what the next steps are. It took me until a few hours ago to feel good about the last few days of work. I have several areas I want to focus on:

  • Additional Layouts: Grid, specifically, but I’ve also been brainstorming ideas on how to declaratively deal with responsiveness too.

  • Buttons: Implementing a Button is one of the first steps to finishing the message passing design – how components talk to each other. The nice thing about buttons is that they don’t need to have Focus to work, so it seems to make sense to implement Buttons before other types of input mechanisms that require tracking Focus.

    Please note, I do recognize that buttons need to be able to accept focus for accessibility reasons, I’m just stating that focus functionality can be added later without any issues.

  • Clipping: As you see in the video above, if the Label shrinks too small to contain its content anymore, it still draws the text past the bounds it was provided. Clipping should be relatively straightforward, and I want to make it something each Component gets to decide when it’s rendering, and the Label component would clip by default.

  • TileMap: The TileMap type should be promoted to a Component. It should be pretty straightforward. TileMap will also clip to its bounds once clipping is implemented.

  • Focus and Hover: To be able to implement certain controls and accessibility features, we need to be able to track the concepts of focus and hover. The Focused Component is the one the user is expecting to respond when they interact with the keyboard and menu items. The Hovered component is the one that the user has their cursor or assistive device “over” at the moment. Clicking the mouse on a Hovered component will allow it to receive Focus, and mouse-wheel events will go to the Hovered component. These events will then bubble upwards through the Component hierarchy until handled.

  • All the controls: I will want all the basic UI controls, including scrollable areas, text inputs, sliders, tabs, ‘soft’ windows like dialogs.

I plan on spending time tomorrow diving into these topics more and starting to flush out some issues in the Github project.

I’m really excited at how Kludgine is shaping up. If you’re a rust developer following along, I’d love to hear your thoughts on how the examples’ APIs are starting to shape up. As always I still see a lot of room for improvements, but I am really liking the vision I have for this engine.