WARNING: Cushy is in early alpha. This guide doubly so.

Composing User Interfaces

Designing user interfaces in Cushy can feel different than in other frameworks. Part of what makes Cushy unique is its reactive data model. The other significant architectural design was to focus on composition.

The content area of each window in Cushy is a single widget. A window cannot have more than one widget at its root. So, how is a user interface with multiple widgets built? Through composition.

Cushy has a category of widgets dedicated to composing multiple widgets into a single widget: multi-widget layout widgets. For example, the Stack widget positions its children horizontally as a set of columns or vertically as a set of rows, while the Layers widget positions its children on top of each other in the Z direction.

The power to this approach is that adding new layout strategies is as simple as implementing a new Widget that implements Widget::layout, and that new widget can be used anywhere in Cushy that any other widget can be used.

Creating Composable Widgets

At the core of widget composition are the MakeWidget and MakeWidgetWithTag traits. The goal of both of these traits is simple: transform self into a WidgetInstance.

A WidgetInstance is a type-erased, reference-counted instance of a Widget trait implementation. This type enables widgets to refer to other widgets without knowing their underlying types. For example, the label of a Button can be any widget, not just a string, because it accepts a MakeWidget implementor in its constructor.

Let's see the various ways that Cushy offers to create the same component: a labeled form field.

Example: A FormField widget

Let's design a way to present a widget resembling this reusable structure:

Label
Field

The structure definition for this widget might look like this:

#![allow(unused)]
fn main() {
    use cushy::widget::{MakeWidget, WidgetInstance};

    struct FormField {
        label: Value<String>,
        field: WidgetInstance,
    }

    impl FormField {
        pub fn new(label: impl IntoValue<String>, field: impl MakeWidget) -> Self {
            Self {
                label: label.into_value(),
                field: field.make_widget(),
            }
        }
    }
}

While it would arguably be better to accept label as another WidgetInstance, by focusing on composing a single widget, this example can also include a utility trait: WrapperWidget.

Approach A: Using the MakeWidget trait

The simplest approach to making new widgets is to avoid implementing them at all! In this case, we can reuse the existing Stack and Align widgets to position the label and field. So, instead of creating a Widget implementation, if we implement MakeWidget, we can compose our interface using existing widgets:

#![allow(unused)]
fn main() {
    impl MakeWidget for FormField {
        fn make_widget(self) -> WidgetInstance {
            self.label
                .align_left()
                .and(self.field)
                .into_rows()
                .make_widget()
        }
    }

    FormField::new(
        "Label",
        Dynamic::<String>::default()
            .into_input()
            .placeholder("Field"),
    )
}

The example FormField when rendered looks like this:

MakeWidget Example Output

Approach B: Using the WrapperWidget trait

The WrapperWidget trait is an alternate trait from Widget that makes it less error-prone to implement a widget that wraps a single other child widget. The only required function is WrapperWidget::child_mut, which returns a &mut WidgetRef. Previously, we were using WidgetInstance to store our field.

When a WidgetInstance is mounted inside of another widget in a window, a MountedWidget is returned. A WidgetRef is a type that manages mounting and unmounting a widget automatically through its API usage. It also keeps track of each window's MountedWidget.

Updating the type to use WidgetRef is fairly straightforward, and does not impact the type's public API:

#![allow(unused)]
fn main() {
    use cushy::widget::{MakeWidget, WidgetRef};

    #[derive(Debug)]
    struct FormField {
        label: Value<String>,
        field: WidgetRef,
    }

    impl FormField {
        pub fn new(label: impl IntoValue<String>, field: impl MakeWidget) -> Self {
            Self {
                label: label.into_value(),
                field: WidgetRef::new(field),
            }
        }
    }
}

Instead of calling field.make_widget(), we now use WidgetRef::new(field). Now, let's look at the WrapperWidget implementation:

#![allow(unused)]
fn main() {
    impl WrapperWidget for FormField {
        fn child_mut(&mut self) -> &mut WidgetRef {
            &mut self.field
        }

}

As mentioned before, this child_mut is the only required function. All other functions provide default behaviors that ensure that the child is mounted, positioned, and rendered for us. Running this example without the rest of the implementation would show only the field without our label.

Before we can start drawing and positioning the field based on the label's size, let's define a couple of helper functions that we can use to implement the WrapperWidget functions needed:

#![allow(unused)]
fn main() {
    impl FormField {
        fn measured_label(
            &self,
            context: &mut GraphicsContext<'_, '_, '_, '_>,
        ) -> MeasuredText<Px> {
            self.label.invalidate_when_changed(context);
            self.label.map(|label| context.gfx.measure_text(label))
        }

        fn label_and_padding_size(
            &self,
            context: &mut GraphicsContext<'_, '_, '_, '_>,
        ) -> Size<UPx> {
            let label_size = self.measured_label(context).size.into_unsigned();
            let padding = context.get(&IntrinsicPadding).into_upx(context.gfx.scale());
            Size::new(label_size.width, label_size.height + padding)
        }
    }
}

The first function uses our graphics context to return Kludgine's MeasuredText<Px> type. The second function looks up the current size to use for padding, and adds it to the height of the measured text.

Advanced Tip: MeasuredText not only contains the dimensions of the text we asked it to measure, it also has all the information necessary to draw all the glyphs necessary in the future. It would be more efficient to cache this data structure in a WindowLocal and only re-cache the text layout when needed.

The Widget::layout() implementation for WrapperWidgets splits the layout process into multiple steps. The goal is to allow wrapper widget authors to be able to customize as little or as much of the process as needed. The first function we are going to use is adjust_child_constraints():

#![allow(unused)]
fn main() {
        fn adjust_child_constraints(
            &mut self,
            available_space: Size<ConstraintLimit>,
            context: &mut LayoutContext<'_, '_, '_, '_>,
        ) -> Size<ConstraintLimit> {
            let label_and_padding = self.label_and_padding_size(context);
            Size::new(
                available_space.width,
                available_space.height - label_and_padding.height,
            )
        }

}

adjust_child_constraints() is responsible for adjusting the incoming available_space constraints by whatever space we need to surround the child. In our case, we need to subtract the label and padding height from the available height. By reducing the available space, we ensure that even if a field is wrapped in an Expand widget, we will have already allocated space for the label to be visible with an appropriate amount of padding.

The next step in the layout process is to position the child:

#![allow(unused)]
fn main() {
        fn position_child(
            &mut self,
            child_size: Size<Px>,
            available_space: Size<ConstraintLimit>,
            context: &mut LayoutContext<'_, '_, '_, '_>,
        ) -> WrappedLayout {
            let label_and_padding = self.label_and_padding_size(context).into_signed();
            let full_size = Size::new(
                available_space
                    .width
                    .min()
                    .into_signed()
                    .max(child_size.width)
                    .max(label_and_padding.width),
                child_size.height + label_and_padding.height,
            );
            WrappedLayout {
                child: Rect::new(
                    Point::new(Px::ZERO, label_and_padding.height),
                    Size::new(full_size.width, child_size.height),
                ),
                size: full_size.into_unsigned(),
            }
        }

}

The above implementation calculates the full size's width by taking the maximum of the minimum available width, the child's width, and the label's width. It calcaultes the full size's height by adding the child's height and the label and padding height.

The result of this function is a structure that contains the child's rectangle and the total size this widget is requesting. The child's rectangle is placed below the label and padding, and its width is set to full_size.width. This is to mimic the behavior our original choice of placing the widgets in a stack. In our example, this is what stretches the text input field to be the full width of the field.

The final step is to draw the label:

#![allow(unused)]
fn main() {
        fn redraw_foreground(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) {
            let label = self.measured_label(context);
            context.gfx.draw_measured_text(&label, TextOrigin::TopLeft);
        }
    }
}

Because the Widget::redraw implementation takes care of drawing the field for us, all this function needed to do was draw the label.

This is the result of using our new implementation:

WrapperWidget Example Output

It looks the exact same as the previous image. That was our goal! Rest assured that this image was generated using the WrapperWidget implementation shown.

Approach C: Using the Widget trait

Implementing Widget is very similar to implementing WrapperWidget, except that we are in full control of layout and rendering. Since our WrapperWidget implementation needed to fully adjust the layout, our layout() function is basically just the combination of the two layout functions from before:

#![allow(unused)]
fn main() {
    impl Widget for FormField {
        fn layout(
            &mut self,
            available_space: Size<ConstraintLimit>,
            context: &mut LayoutContext<'_, '_, '_, '_>,
        ) -> Size<UPx> {
            let label_and_padding = self.label_and_padding_size(context);
            let field_available_space = Size::new(
                available_space.width,
                available_space.height - label_and_padding.height,
            );
            let field = self.field.mounted(context);
            let field_size = context.for_other(&field).layout(field_available_space);

            let full_size = Size::new(
                available_space
                    .width
                    .min()
                    .max(field_size.width)
                    .max(label_and_padding.width),
                field_size.height + label_and_padding.height,
            );

            context.set_child_layout(
                &field,
                Rect::new(
                    Point::new(UPx::ZERO, label_and_padding.height),
                    Size::new(full_size.width, field_size.height),
                )
                .into_signed(),
            );

            full_size
        }

}

The major difference in this function is that we are manually calling layout() on our field widget. This is done by creating a context for our field (context.for_other()), and calling the layout function on that context. After we receive the field's size, we must call set_child_layout() to finish laying out the field.

Finally, the result of the layout() function is the full size that the field needs. With layout done, rendering is the next step:

#![allow(unused)]
fn main() {
        fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) {
            let label = self.measured_label(context);
            context.gfx.draw_measured_text(&label, TextOrigin::TopLeft);

            let field = self.field.mounted(context);
            context.for_other(&field).redraw();
        }

}

This function isn't much more complicated than the WrapperWidget's implementation. The extra two lines again use context.for_other() to create a context for the field widget, but this time we call the field's redraw() function instead.

Finally, we have one last bit of housekeeping that WrapperWidget did automatically: unmounting the field when the form field itself is unmounted. This is important to minimize memory usage by ensuring that if the widget is shared between multiple windows, each window's state is cleaned up indepdent of the widget itself:

#![allow(unused)]
fn main() {
        fn unmounted(&mut self, context: &mut cushy::context::EventContext<'_>) {
            self.field.unmount_in(context);
        }
    }
}

And with this, we can look at an identical picture that shows this implementation works the same as the previous implementations:

Widget Example Output

Conclusion

Cushy tries to provide the tools needed to avoid implementing your own widgets by utilizing composition. It also has many tools for creating reusable, composible widgets. If you find yourself uncertain of what path to use when creating a reusable component, try using a MakeWidget implementation initially.

If using MakeWidget is cumbersome or doesn't expose as much functionality as needed, you may still find that you can utilize MakeWidget along with a simpler custom widget than implementing a more complex, multi-part widget. This approach is how the built-in Checkbox, Radio and Select widgets are all built using a Button.