Widget Ref
A WidgetRef is a handle from application code to one widget in the tree. It answers two questions the composed view cannot answer on its own: where that widget ended up, and where the keyboard should go.
Creating a WidgetRef
extern crate guido;
fn main() {
use guido::prelude::*;
let module_ref = create_widget_ref();
;
}
This creates a WidgetRef with an internal Signal<Rect> initialized to Rect::default() (all zeros). The signal is updated automatically on every frame that may have moved the widget.
Attaching to a Container
Attach the ref to a container using the .widget_ref() builder method:
extern crate guido;
use guido::prelude::*;
fn main() {
let module_ref = create_widget_ref();
let module = container()
.widget_ref(module_ref)
.padding(8.0)
.background(Color::rgb(0.2, 0.2, 0.3))
.child(text("System Info"));
;
}
Reading Bounds Reactively
Read the bounds via .rect(), which returns a Signal<Rect>:
extern crate guido;
use guido::prelude::*;
fn main() {
let module_ref = create_widget_ref();
let bounds_text = text(move || {
let r = module_ref.rect().get();
format!("x={:.0} y={:.0} w={:.0} h={:.0}", r.x, r.y, r.width, r.height)
});
;
}
The Rect is where the widget is drawn, relative to the surface origin:
x,y— its top-left cornerwidth,height— its size
That is the box it was laid out in, moved by the scroll offset of every scroll area it is in and by its transforms and its ancestors’. A row in a scrolled list reports the row you see, and a scaled widget its scaled size; a rotated one reports the axis-aligned box around it. A shadow is not part of it. The signal changes when any of those move — after a scroll or an animation frame as well as after layout. A pressed or hovered state that scales the widget moves it too, so a popup anchored to a button that shrinks while pressed follows it.
Do not move a widget by its own rect, or by the rect of something it carries: each frame’s rect would move it again, and it would never settle.
The Box Layout Gave It
.layout_rect() is the same box with every transform taken out — the widget’s
own translate, scale and rotate, its ancestors’, and their animations. It
still moves with the scroll of every scroll area the widget is in. A transform
animating across frames changes rect() on each of them and never writes
layout_rect(); a layout that moves or resizes the widget writes it once.
That is the one to read when a widget’s transform is driven from its own
geometry — a hand-written animation from where an element was to where layout
now puts it — because rect() there would read the transform back:
extern crate guido;
use guido::prelude::*;
fn main() {
let card_ref = create_widget_ref();
let target = move || card_ref.layout_rect().get();
let _ = target;
}
Positioning a Popup
A common use case is positioning a popup centered under a clickable module:
extern crate guido;
use guido::prelude::*;
const BAR_HEIGHT: f32 = 32.0;
const POPUP_WIDTH: f32 = 200.0;
const SCREEN_WIDTH: f32 = 1920.0;
fn popup_content() -> Container { container() }
fn main() {
let show_popup = create_signal(false);
let module_ref = create_widget_ref();
// The module in the status bar
let module = container()
.widget_ref(module_ref)
.on_click(move || show_popup.set(true))
.child(text("Menu"));
// The popup, centered under the module
let popup = container()
.translate(move || {
let r = module_ref.rect().get();
let midpoint = r.x + r.width / 2.0;
let x = (midpoint - POPUP_WIDTH / 2.0).clamp(8.0, SCREEN_WIDTH - POPUP_WIDTH - 8.0);
// Both halves of the pair are the same numeric type — `BAR_HEIGHT`
// here is an `f32`, as `x` is.
(x, BAR_HEIGHT)
})
.child(popup_content());
;
}
Moving the Keyboard
Attach the ref to the widget that takes focus — a text_input, or a container
that declares focusable(true) — and ask:
extern crate guido;
use guido::prelude::*;
fn main() {
let value = create_signal(String::new());
let field = create_widget_ref();
container()
.layout(Flex::column().spacing(8.0))
.child(text_input(value).widget_ref(field))
.child(
container()
.on_click(move || field.focus())
.child(text("Edit")),
)
;
}
focus() lands on the next frame, not inside the call. Focus is resolved against
the tree — that is where a widget’s ancestors are, and when_focused needs them
— and the composing code has no tree. Every toolkit arrives at the same shape:
Compose’s FocusRequester must be called from an effect, iced’s
text_input::focus(id) returns a Task for the runtime to run, and Flutter tells
you to request from a post-frame callback because during initState the widget is
not mounted.
Because of that, asking early is allowed: a request that names a widget which
has not been laid out yet waits for it rather than being dropped, so focus() in
a startup effect does what it looks like. Two requests in one frame are two answers
to the same question, and the later one wins.
A request does not wait forever, though, and the difference matters: one whose
widget has since left the tree is dropped, and so is one whose ref belonged to
a scope that is gone — .focus() from inside a popup that closed before the frame
came round. Both are waiting for something that already happened, and a request
left parked would fire at whatever took that ref next.
The rest of the verb:
extern crate guido;
use guido::prelude::*;
fn main() {
let field = create_widget_ref();
field.blur(); // give the keyboard back, if this widget has it
field.is_focused(); // reactive when read in a tracked scope
field.widget(); // Some(WidgetId) once laid out; None before, and None
// again once the widget leaves or the ref's scope dies
;
}
For a field that should simply be ready when the screen appears, there is no need
for a handle at all — use
autofocus().
When Not to Use It
A WidgetRef rect is written after layout, on the frame’s way to paint, so a
property derived from one lags a frame — and reads zero on the first frame, before any layout has run.
That is fine for positioning a popup, and wrong for sizing. Two common cases
have direct layout support instead:
- A bar proportional to a value (slider fill, gauge, progress): use
fraction(), resolved against the real constraints during layout. - A widget sized to a sibling (background, badge, decoration): stack them
with
ZStackand give the followerfill().
Edge Cases
- Before first layout: The signal returns
Rect::default()(all zeros) - Widget removal: The registry entry is automatically cleaned up
- Cross-surface reads: Works naturally since all surfaces share the main thread. Surface B may read a one-frame-old value if it renders before surface A