Uncertainties about custom widget implementation

Hello!

I am working on a resizable widget.

You can think of it as essentially a rudimentary PaneGrid with just 2 fixed panes having a bar in the middle for resizing them.

It accepts 2 elements and adds a divider between them which can be dragged to resize the 2 elements, either vertically or horizontally.

I’ve made it work, but I have some questions about the implementation and whether or not it is correct in regard to implementing Widget in iced.

Here is the code for reference:

Structs

pub struct Resizable<'a, Message, Theme, Renderer> {
    // width: Length,
    // height: Length,
    direction: ResizableDirection,
    divider_size: f32,
    a: Element<'a, Message, Theme, Renderer>,
    b: Element<'a, Message, Theme, Renderer>,
}

#[derive(Debug, Clone, Copy)]
pub enum ResizableDirection {
    Horizontal,
    Vertical,
}

#[derive(Debug, Clone, Copy, Default)]
struct ResizableState {
    /// The x/y offset of the divider from the left/top of the widget.
    /// Always Some after resize.
    divider_offset: Option<f32>,
    dragging: bool,
}

Where Resizable is the Widget impl and ResizableState is its state used in the tree state Tree.

layout

The first question I have is about the layout implementation. For brevity, I’ll include only the logic in the horizontal direction.

    fn layout(
        &mut self,
        tree: &mut iced::advanced::widget::Tree,
        renderer: &Renderer,
        limits: &iced::advanced::layout::Limits,
    ) -> iced::advanced::layout::Node {
        let max_size = limits.max();
        let state: &ResizableState = tree.state.downcast_ref();

        let a_node = self.a.as_widget_mut().layout(
            &mut tree.children[0],
            renderer,
            &limits.max_width(
                match state.divider_offset {
                    Some(offset) => offset,
                    None => max_size.width / 2.0,
                } - self.divider_size,
            ),
        );

        let a_bounds = a_node.bounds();

        let a_total_x = a_bounds.x + a_bounds.width;

        let divider_node = Node::new(Size::new(self.divider_size, max_size.height))
            .move_to(Point::new(a_total_x, a_bounds.y));

        let b_node = self
            .b
            .as_widget_mut()
            .layout(
                &mut tree.children[1],
                renderer,
                &limits.max_width(max_size.width - a_total_x - self.divider_size),
            )
            .move_to(Point::new(a_total_x + self.divider_size, a_bounds.y));

        let node = Node::with_children(max_size, vec![a_node, divider_node, b_node]);

        node
}

Question 1: Is it OK if I create the divider_node like that and put it in the returned node’s children?

The reason I ask is I haven’t seen anything like this in PaneGrid’s layout implementation, and that does resizing pretty well.

Is there a better way to do this?

Just for reference, here is the draw function. This one is pretty self explanatory.

draw

    fn draw(
        &self,
        tree: &iced::advanced::widget::Tree,
        renderer: &mut Renderer,
        theme: &Theme,
        style: &iced::advanced::renderer::Style,
        layout: iced::advanced::Layout<'_>,
        cursor: iced::advanced::mouse::Cursor,
        viewport: &iced::Rectangle,
    ) {
        let mut children = layout.children();
        let a_node = children.next().unwrap();
        let divider = children.next().unwrap();
        let b_node = children.next().unwrap();

        self.a.as_widget().draw(
            &tree.children[0],
            renderer,
            theme,
            style,
            a_node,
            cursor,
            viewport,
        );

        renderer.fill_quad(
            renderer::Quad {
                bounds: divider.bounds(),
                border: border::rounded(0.7),
                ..Default::default()
            },
            Color::from_rgba(0.5, 0.2, 0.5, 0.7),
        );

        self.b.as_widget().draw(
            &tree.children[1],
            renderer,
            theme,
            style,
            b_node,
            cursor,
            viewport,
        );
    }

My most important questions are about the update and diff functions, .

update

    fn update(
        &mut self,
        state: &mut Tree,
        event: &Event,
        layout: Layout<'_>,
        cursor: iced::advanced::mouse::Cursor,
        renderer: &Renderer,
        clipboard: &mut dyn iced::advanced::Clipboard,
        shell: &mut iced::advanced::Shell<'_, Message>,
        viewport: &Rectangle,
    ) {
        let mut children = layout.children();
        let a_layout = children.next().unwrap();
        let divider = children.next().unwrap();
        let b_layout = children.next().unwrap();

        let widget_state: &mut ResizableState = state.state.downcast_mut();

        match event {
            Event::Mouse(mouse::Event::ButtonPressed(Button::Left)) => {
                if cursor.is_over(divider.bounds()) {
                    widget_state.dragging = true;
                }
            }
            Event::Mouse(mouse::Event::ButtonReleased(Button::Left)) => {
                if widget_state.dragging {
                    widget_state.dragging = false;
                }
            }
            Event::Mouse(mouse::Event::CursorMoved { position: _ }) => {
                if widget_state.dragging {
                    match self.direction {
                        ResizableDirection::Horizontal => {
                            widget_state.divider_offset = cursor
                                .position_in(layout.bounds())
                                .map(|point| point.x)
                                .or(widget_state.divider_offset);
                        }
                        ResizableDirection::Vertical => {
                            widget_state.divider_offset = cursor
                                .position_in(layout.bounds())
                                .map(|point| point.y)
                                .or(widget_state.divider_offset);
                        }
                    }
                    shell.invalidate_layout();
                    shell.request_redraw();
                }
            }
            _ => {}
        }

        self.a.as_widget_mut().update(
            &mut state.children[0],
            event,
            a_layout,
            cursor,
            renderer,
            clipboard,
            shell,
            viewport,
        );

        self.b.as_widget_mut().update(
            &mut state.children[1],
            event,
            b_layout,
            cursor,
            renderer,
            clipboard,
            shell,
            viewport,
        );
    }

As you can see, on any resize that happens I invalidate the layout and request a redraw from the shell. I do this because so far it is the only way I know how to make it look interactive. Without those 2 lines, the state will update, but the view will not update until a mouse is clicked (I assume it is caused by any event in iced that causes the layout to be re-computed).

If I only request the redraw, the same thing happens, i.e. the layout is not updated and
the slider does not move.

Question 2: Does this have something to do with the way the divider node is being added to the children?

I would assume not - and this is where my confusion stems from - because the divider node always uses the state from the tree and that is always being updated correctly.

I assume making iced recompute the layout on every update is not ideal (although it’s only when resizing so maybe not a big deal), and I haven’t seen PaneGrid do this either to recompute the layout, but so far it’s working and I don’t know of a better way to do this.

Question 2a: Is there a better way to do this?

diff

Here are the implementations of state related functions of Widget

    fn tag(&self) -> tree::Tag {
        tree::Tag::of::<ResizableState>()
    }

    fn state(&self) -> tree::State {
        tree::State::new(ResizableState::default())
    }

    fn children(&self) -> Vec<Tree> {
        vec![Tree::new(&self.a), Tree::new(&self.b)]
    }

    fn diff(&self, tree: &mut Tree) {
        tree.diff_children(&[&self.a, &self.b]);
    }

Question 3: Is the way I am diffing this causing a problem with above?

I am fairly certain this is where the problem lies.

There is nothing in the diff to tell iced that the state has changed so I am assuming the problem of layout invalidation has to do with this, but I am not quite sure where I would track the state.

And if I would track it somewhere in state, diff, or wherever, which one of those causes the layout to be invalidated?

operation

My last question is about this implementation.

    fn operate(
        &mut self,
        state: &mut Tree,
        layout: Layout<'_>,
        renderer: &Renderer,
        operation: &mut dyn iced::advanced::widget::Operation,
    ) {
        let mut children = layout.children();
        operation.container(None, layout.bounds());
        operation.traverse(&mut |operation| {
            self.a.as_widget_mut().operate(
                &mut state.children[0],
                children.next().unwrap(),
                renderer,
                operation,
            );
            children.next().unwrap();
            self.b.as_widget_mut().operate(
                &mut state.children[1],
                children.next().unwrap(),
                renderer,
                operation,
            );
        });
    }

Question 4: The question is just: Is this fine?

It was inspired by container’s implementation and some others where the operation just traverses.

Full disclaimer, I don’t understand how operations work internally, like, at all.

The docs say this,

/// A piece of logic that can traverse the widget tree of an application in
/// order to query or update some widget state.

but I have no idea where these operation outcomes end up in nor when they are triggered.
I can tell they are intended to provide some extra functionality to widgets, but I guess I fail to see why the update implementation is not enough for this; So as a bonus if you feeling extra generous with your time maybe you can do an ELI5 of those for me just as an entry point to point me in the right direction I would appreciate it. :slight_smile:

So far my references have been looking at the widget implementations in iced and the
unnoficial guide.

That sums it up! I know it’s a wordful, so I thank you for making it this far and I hope this post can serve as a reference to others when implementing custom widgets and running into the same doubts and issues as these.

If you have any questions regarding the implementation please ask away!
Full implementation for reference.

Check out GitHub - edwloef/iced_split: resizeable splits for iced

Hey, thanks for the reply!

I’ve checked out the example. It appears to me that the positioning of the divider (the bar used for resizing) is left to the user in the on_drag event handler when instantiating the widget.


        let main = vertical_split(
            iced::widget::column!["hello"],
            iced::widget::column!["world"],
            0.2,
            Message::Split // This arg right here
        );

The essential difference between my widget and this one is that the position of the divider comes from the internal state in mine, whereas in this one it comes from user state.

This forces users to track the last position of the bar and pass that position to
vertical_split,'s third argument where I’ve passed a hard coded 0.2. I’m trying to avoid any state leakage from the widget into the user API, which is why I’ve opted for tracking the divider position internally in my widget implementation. I want it to be a really simple widget that is just plug and play, no state tracking necessary.

I’m still not really sure why your example does not need layout invalidation, nor why I haven’t seen shell.invalidate_layout() in any of the iced widgets. I can see your widget does not store
the divider as a layout node. I’ve adjusted my widget to do the same, so now it computes the divider position based on the the first child’s layout (a_node) in my example, but it still requires shell.invalidate_layout() for it to recompute and redraw properly ¯_(ツ)_/¯.

I don’t know what’s going on, the only difference I see from my widget and the split example you gave is that when computing the layout, my state for the divider comes from the state Tree, while the one in yours is computed from &self.

Hi, iced_split dude here :slight_smile:

The reason I put the split position in app state and not in widget state is simply flexibility. If the split position is widget-internal, you have to modify the widget to change its behavior, instead of just handling whatever behavior you want in fn update. For example, one could clamp the split position between two limits, or snap the split position to zero beyond some threshold, without me having to explicitly add support for these behaviors, or the user having to copy-paste and modify the widget.

The reason I can avoid shell.invalidate_layout() is because I emit a message with the new split position every time it changes, and a re-layout always happens after a message is handled by the app.

That DAW looks beatiful, thank you for the reply!

I didn’t know publishing events from the shell invalidates the layout, that’s exactly what I was missing.

Looking at your examples, your approach for exposing the divider position to the user does make more sense. I am also loving the animation :smiley:

Was this widget ever considered being added to the iced::widgets module? Feels like a pretty common thing people could use (I’ve found a use for it twice now in my project).

1 Like

Big thanks for the kind words!

hecrj has stated in the past that he’s aiming for the built-in widgets to eventually be enough to satisfy most peoples’ needs, so I can only assume something of the sort will eventually land upstream, since it’s definitely one of the more common where-is-this-widget questions I see asked. That being said, I have no idea when that’ll happen, or whether it’ll be some variant of my implementation or something he cooks up on his own.