Ply rebuilds your UI every frame. IDs are how the engine knows that this frame's "submit button" is the same one as last frame's — so it can carry over focus, scroll positions, hover state, and text input.
🔗Automatic IDs
Every element gets an ID automatically, derived from its parent and its position among siblings. You don't have to think about it:
ui.element().width(grow!()).height(grow!())
.layout(|l| l.direction(TopToBottom).gap(8).padding(12))
.children(|ui| {
// Auto-ID based on parent id + child index 0
ui.element().width(grow!()).height(fixed!(40.0))
.background_color(0x262220).empty();
// Auto-ID based on parent id + child index 1
ui.element().width(grow!()).height(fixed!(40.0))
.background_color(0x3A3533).empty();
});
Auto IDs are stable as long as children stay in the same order. For most elements this is all you need.
🔗Explicit IDs
Set an ID with .id() when you need to reference an element, or for interactive elements when the parent or child index might change.
let sidebar_id = ui.element()
.id("sidebar")
.width(fixed!(200.0))
.height(grow!())
.background_color(0x181515)
.layout(|l| l.direction(TopToBottom).gap(4).padding(8))
.children(|ui| {
ui.text("Navigation", |t| t.font_size(14).color(0xFFC32C));
});
if let Some(bbox) = ui.bounding_box(sidebar_id) {
println!("Sidebar bounding box: {:?}", bbox);
}
.children() and .empty() return the element's Id. But anywhere you need to give an id you can also put in the label: ui.bounding_box("sidebar") works too.
🔗Indexed IDs
When you render a list, each item needs a unique ID. Pass a (&str, u32) tuple:
The string "nav_item" and the index i are hashed together. Each item gets
a stable ID regardless of how many items are in the list. Whenever you need an indexed ID, just pass a tuple: ply.set_focus(("nav_item", 2)).
🔗Inline state
You can check the state of the element you are currently building inside its .children() closure:
| Method | What it does |
|---|---|
ui.hovered() | Is pointer over this element? |
ui.pressed() | Is pointer held down on this element |
ui.focused() | Does this element have keyboard focus |
ui.scroll_offset() | What's this element's scroll offset |
These check the currently open parent element, the one whose .children()
closure you're inside.
🔗Querying state by ID
When you need to check state from anywhere use Ply methods with an ID:
if ply.is_pressed("card") {
// card is being held down
}
let mut ui = ply.begin();
let card_id = ui.element()
.id("card")
.width(fixed!(200.0))
.height(fixed!(120.0))
.background_color(0x262220)
.corner_radius(8.0)
.children(|ui| {
ui.text("Hello", |t| t.font_size(18).color(0xE8E0DC));
});
if ui.pointer_over(card_id) {
// pointer is over the card
}
Ui is just a Ply that has begun, so that you can start making elements.
You can use these query methods on both ply and ui.
ui.set_focus("search_box");
if let Some(focused) = ply.focused_element() {
// focused is an Id
}
ui.clear_focus();
let value = ui.get_text_value("editor");
ui.set_text_value("editor", "hello world");
let pos = ui.get_cursor_pos("editor");
ui.set_cursor_pos("editor", 5);
if let Some((start, end)) = ui.get_selection_range("editor") {
// there's an active selection start to end
}
ui.set_selection("editor", 0, 10); // select first 10 chars
if let Some(data) = ply.scroll_container_data("my_list") {
// data.scroll_position, data.content_dimensions, etc.
}🔗Constructing IDs directly
You can create IDs without an element builder:
let id = Id::new("my_button");
let id = Id::new_index("item", 3);
Useful when comparing IDs.