IDs & State

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:

MethodWhat 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.

🔗Next steps

Interactivity