Element Query API
The Element Query API provides programmatic access to elements in the UI tree, enabling imperative operations like scrolling, focusing, reading bounds, and triggering updates.
Overview
#![allow(unused)]
fn main() {
// Query an element by its string ID
let handle = ctx.query("my-element");
// Check if it exists
if handle.exists() {
// Get computed bounds
if let Some(bounds) = handle.bounds() {
println!("Element at ({}, {}) size {}x{}",
bounds.x, bounds.y, bounds.width, bounds.height);
}
// Scroll into view
handle.scroll_into_view();
// Focus the element
handle.focus();
}
}
Assigning Element IDs
To query an element, it must have a string ID assigned via .id():
#![allow(unused)]
fn main() {
div()
.id("sidebar")
.w(250.0)
.h_full()
.child(
div()
.id("nav-item-home")
.child(text("Home"))
)
.child(
div()
.id("nav-item-settings")
.child(text("Settings"))
)
}
IDs should be unique within your UI. Duplicate IDs will cause the last element to win.
ElementHandle API
Creation & Existence
#![allow(unused)]
fn main() {
// Get a handle - works even if element doesn't exist yet
let handle = ctx.query("my-element");
// Check if element exists in the tree
if handle.exists() {
// Element is rendered
}
// Get the string ID
let id = handle.id(); // "my-element"
}
Bounds & Visibility
#![allow(unused)]
fn main() {
// Get computed bounds after layout
if let Some(bounds) = handle.bounds() {
println!("Position: ({}, {})", bounds.x, bounds.y);
println!("Size: {}x{}", bounds.width, bounds.height);
}
// Check if visible in viewport
if handle.is_visible() {
// Element intersects with window viewport
}
}
Navigation
#![allow(unused)]
fn main() {
// Scroll element into view (smooth scroll)
handle.scroll_into_view();
// Focus the element (for inputs, updates EventRouter)
handle.focus();
// Remove focus
handle.blur();
// Check focus state
if handle.is_focused() {
// Element has keyboard focus
}
}
Tree Traversal
#![allow(unused)]
fn main() {
// Get parent element
if let Some(parent) = handle.parent() {
println!("Parent ID: {}", parent.id());
}
// Iterate over ancestors (parent → grandparent → root)
for ancestor in handle.ancestors() {
println!("Ancestor: {}", ancestor.id());
}
}
Triggering Updates
ElementHandle provides three levels of update granularity:
#![allow(unused)]
fn main() {
// 1. Visual-only update (fastest - skips layout)
// Use for: background color, opacity, shadows, transforms
handle.mark_visual_dirty(
RenderProps::default().with_background(Color::RED.into())
);
// 2. Subtree rebuild with new children
// Use for: structural changes where you know the new content
handle.mark_dirty_subtree(
div().child(text("New content"))
);
// 3. Full rebuild (fallback)
// Triggers complete UI rebuild, diffing determines actual changes
handle.mark_dirty();
}
Signal Integration
#![allow(unused)]
fn main() {
// Emit a signal to trigger reactive updates
// Only rebuilds stateful elements that depend on this signal
handle.emit_signal(my_signal_id);
}
On-Ready Callbacks
Register callbacks that fire once after an element’s first layout:
#![allow(unused)]
fn main() {
ctx.query("progress-bar").on_ready(|bounds| {
// Element has been laid out
println!("Progress bar width: {}", bounds.width);
// Start an animation based on computed size
progress_anim.lock().unwrap().set_target(bounds.width * 0.75);
});
}
On-ready callbacks:
- Fire only once per element ID
- Work even if element doesn’t exist yet (callback queued)
- Survive tree rebuilds (tracked by string ID)
Querying in Event Handlers
Inside event handlers, use the global query() function to get an ElementHandle:
#![allow(unused)]
fn main() {
use blinc_layout::prelude::*;
div()
.on_click(|_| {
// query() returns Option<ElementHandle> - None if element doesn't exist
if let Some(handle) = query("my-element") {
handle.scroll_into_view();
handle.focus();
}
})
}
The query() function uses the global BlincContextState internally, so you don’t need to capture any context or registry in your closures.
For simple operations like scroll and focus without needing the full handle:
#![allow(unused)]
fn main() {
use blinc_core::BlincContextState;
div()
.on_click(|_| {
// Direct access for simple operations
if let Some(ctx) = BlincContextState::try_get() {
ctx.scroll_element_into_view("my-element");
ctx.set_focus(Some("my-input"));
}
})
}
Use Cases
Scroll to Element on Action
#![allow(unused)]
fn main() {
fn scrollable_list(ctx: &WindowedContext) -> impl ElementBuilder {
div()
.flex_col()
.child(
div()
.on_click(|_| {
// Use query() to get handle and scroll
if let Some(handle) = query("list-bottom") {
handle.scroll_into_view();
}
})
.child(text("Jump to Bottom"))
)
.child(
scroll()
.h(400.0)
.child(
div()
.flex_col()
.children((0..100).map(|i| {
div()
.id(format!("item-{}", i))
.child(text(format!("Item {}", i)))
}))
.child(
div().id("list-bottom").h(1.0)
)
)
)
}
}
Focus Management
#![allow(unused)]
fn main() {
fn login_form(ctx: &WindowedContext) -> impl ElementBuilder {
div()
.flex_col()
.gap(16.0)
.child(
text_input(ctx.use_state_keyed::<TextInputState>("username"))
.id("username-input")
.placeholder("Username")
)
.child(
text_input(ctx.use_state_keyed::<TextInputState>("password"))
.id("password-input")
.placeholder("Password")
.on_key_down(|evt| {
if evt.key_code == 9 && evt.shift { // Shift+Tab
if let Some(handle) = query("username-input") {
handle.focus();
}
}
})
)
.child(
div()
.on_click(|_| {
// Focus username on form reset
if let Some(handle) = query("username-input") {
handle.focus();
}
})
.child(text("Reset"))
)
}
}
Measure Element After Layout
#![allow(unused)]
fn main() {
fn responsive_card(ctx: &WindowedContext) -> impl ElementBuilder {
let card_width = ctx.use_signal(0.0f32);
// Register callback to measure after layout
ctx.query("adaptive-card").on_ready(move |bounds| {
// on_ready callback has access to bounds directly
println!("Card width: {}", bounds.width);
});
let width = ctx.get(card_width).unwrap_or(0.0);
let columns = if width > 600.0 { 3 } else if width > 400.0 { 2 } else { 1 };
div()
.id("adaptive-card")
.w_full()
.flex_wrap()
.children((0..9).map(|i| {
div()
.w(pct(100.0 / columns as f32))
.child(text(format!("Item {}", i)))
}))
}
}
Efficient Visual Updates
Use mark_visual_dirty for visual-only changes that don’t affect layout:
#![allow(unused)]
fn main() {
fn highlight_on_selection(ctx: &WindowedContext, selected_id: Option<&str>) -> impl ElementBuilder {
let selected = selected_id.map(|s| s.to_string());
div()
.flex_col()
.children(["item-a", "item-b", "item-c"].iter().map(|id| {
let is_selected = selected.as_deref() == Some(*id);
let id_string = id.to_string();
div()
.id(*id)
.p(12.0)
.bg(if is_selected {
Color::rgba(0.2, 0.5, 1.0, 0.3)
} else {
Color::TRANSPARENT
})
.on_click(move |_| {
// Visual-only update - skips layout recomputation
if let Some(handle) = query(&id_string) {
handle.mark_visual_dirty(
RenderProps::default()
.with_background(Color::rgba(0.2, 0.5, 1.0, 0.3).into())
);
}
})
.child(text(*id))
}))
}
}
Carousel with Snap Points
#![allow(unused)]
fn main() {
fn carousel(ctx: &WindowedContext, items: &[String]) -> impl ElementBuilder {
let current_index = ctx.use_signal(0usize);
div()
.flex_col()
.child(
scroll()
.id("carousel-scroll")
.w(300.0)
.h(200.0)
.scroll_x()
.child(
div()
.flex_row()
.children(items.iter().enumerate().map(|(i, item)| {
div()
.id(format!("slide-{}", i))
.w(300.0)
.h(200.0)
.flex_center()
.child(text(item))
}))
)
)
.child(
div()
.flex_row()
.justify_center()
.gap(8.0)
.children((0..items.len()).map(|i| {
div()
.circle(8.0)
.bg(Color::WHITE.with_alpha(0.5))
.on_click(move |_| {
if let Some(handle) = query(&format!("slide-{}", i)) {
handle.scroll_into_view();
}
})
}))
)
}
---
# Performance Considerations
## Update Granularity
Choose the right update method based on what changed:
| Method | When to Use | Layout Cost |
|--------|-------------|-------------|
| `mark_visual_dirty(props)` | Background, opacity, shadow, transform | None (visual only) |
| `mark_dirty_subtree(div)` | Children structure changed | Subtree only |
| `mark_dirty()` | Unknown changes, fallback | Full rebuild |
| `emit_signal(id)` | Signal-based state change | Targeted stateful |
## Avoid Frequent Queries in Render
```rust
// Bad: Query in render function (called every frame)
fn bad_example(ctx: &WindowedContext) -> impl ElementBuilder {
let bounds = ctx.query("my-element").bounds(); // Called every render!
// ...
}
// Good: Query in event handler or on_ready
fn good_example(ctx: &WindowedContext) -> impl ElementBuilder {
div()
.on_click(|_| {
// query() is designed for use in event handlers
if let Some(handle) = query("my-element") {
let bounds = handle.bounds();
// Use bounds...
}
})
}
}
Use on_ready for Post-Layout Measurements
#![allow(unused)]
fn main() {
// The on_ready callback fires once after first layout
ctx.query("my-element").on_ready(|bounds| {
// Safe to use bounds here - layout is complete
setup_animations_based_on_size(bounds);
});
}
Best Practices
-
Assign meaningful IDs - Use descriptive IDs like
"sidebar","submit-button","user-avatar"rather than generic names. -
Prefer declarative state - Use signals and reactive state for most UI updates. Use ElementHandle for imperative operations like scroll-to and focus.
-
Use visual-only updates - When only colors/opacity/shadows change, use
mark_visual_dirty()to skip layout. -
Handle missing elements - Always check
exists()or handleNonefrombounds()when the element might not be rendered. -
Avoid ID collisions - Each ID should be unique. Consider namespacing like
"dialog-submit","sidebar-nav-home". -
Use on_ready for measurements - Don’t assume bounds are available immediately. Use
on_readyfor post-layout operations.