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. ![]()
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.