ViewId

ViewId is view's identifier and operation handle in Flor runtime. View tree, layout state, event handlers, focus table, scroll state, mouse capture and render resources are all associated to same view through it.

This page targets two scenarios: operating current view in event callbacks, or accessing runtime capabilities through self.view_id in custom view implementation. When only want to configure focus order, prefer reading Focus Builder.

Getting ViewId

Event builder callbacks pass target view's ViewId as first parameter:

use flor::view::builder::EventBuilder;
use flor_lys::label::label;

let item = label("Click")
    .on_click(|view_id, key_state, mouse_position| {
        println!("{view_id} clicked at {mouse_position:?}");
    });

Custom views usually save ViewId as field, and create in constructor:

use flor::view::{View, ViewId};

pub struct MyView {
    view_id: ViewId,
}

impl MyView {
    pub fn new() -> Self {
        Self {
            view_id: ViewId::new(),
        }
    }
}

impl View for MyView {
    fn view_id(&self) -> ViewId {
        self.view_id
    }
}

ViewId::new() registers basic ViewState and ViewHandler. ViewId::new_with_layout(...) allows customizing LayoutResolver when creating, generally only used when underlying views or layout system need to take over the initial layout resolver.

Redraw Request

This is Flor view development's highest-level hard rule, please strictly follow it.

No matter whether your view runs in immediate mode or retained mode, whenever internal visual-affecting properties change, must call view_id.request_redraw().

// In on_update_state, on_frame, event callbacks or anywhere modifying visual properties
self.color = new_color;
self.view_id.request_redraw(); // Must call

Flor won't automatically track view internal field changes. If you modified color, text, image handle, animation state or any field affecting drawing result, but don't call request_redraw(), window won't redraw, user won't see change.

This rule applies to:

  • After updating fields in on_update_state
  • After advancing animation state in on_frame
  • After modifying internal state in event callbacks
  • After changing visual properties in any custom method

If you find visual not updating when implementing custom view, first check whether missed request_redraw() call. Other view development chapters will repeatedly emphasize this point, for example View Trait's lifecycle hooks.

State and Layout

ViewId can read current view's layout and state:

let layout = view_id.layout()?;
let abs = view_id.abs_location()?;
let style = view_id.get_current_style()?;

let size = view_id.with_state(|state| state.layout.size)?;
view_id.with_state_mut(|state| {
    state.disable = true;
})?;

layout() returns Taffy calculated taffy::Layout. abs_location() returns absolute position relative to window top-left corner, this value is written to cache when layout refreshes.

with_state and with_state_mut are closure-style access interfaces: they only hold internal state lock during closure execution. Don't do long-time work in closure, also don't save borrowed state reference out.

get_current_style() and with_current_style(...) read style by current ControlState. Current state affects resolver choosing normal, hover, focus, active or disabled variant.

State Update

update_state hands arbitrary Box<dyn Any> to view instance's View::on_update_state, then requests redraw:

view_id.update_state(Box::new(String::from("New title")));

View needs to downcast and update internal fields in its own on_update_state. For example text, image handle, style update objects — these kinds of view private state are all suitable for going through this path.

If enabled class feature, update_class(layer_id, class_str) parses class name, updates layout resolver, and calls view's on_update_class(control_state, class). z-* classes will be recognized as z-index and removed from class list.

View Tree and Window

ViewId records view's relationship in tree and belonging window:

let parent = view_id.parent_view_id();
let window = view_id.window_id();

parent_view_id.push_view(Box::new(child));

push_view adds child view to current view, rebuilds subtree window attribution, and triggers parent view's on_child_push. Application layer usually organizes tree through ViewBuilder::views, ViewBuilder::push_view or views![] macro; directly calling ViewId::push_view is more suitable for runtime appending child views.

Focus Manipulation

User-side explanation of focus mechanism see Focus Mechanism. Here lists runtime methods related to focus on ViewId.

impl ViewId {
    pub fn update_focus_index(self, focus_index: Option<u32>);
    pub fn set_focus(self, virtual_index: Option<u16>);
    pub fn is_focused(self) -> bool;
    pub fn push_focus_scope(self);
    pub fn pop_focus_scope(self);
}

update_focus_index

update_focus_index updates at runtime whether view participates in focus table.

view_id.update_focus_index(Some(0));  // Add or update focus table, sort value 0
view_id.update_focus_index(Some(10)); // Add or update focus table, sort value 10
view_id.update_focus_index(None);     // Remove from focus table

Some(0) is legal sort value. Only None indicates exiting the focus system.

Current implementation updates by single focus entry: it first removes this ViewId's existing focus entries, then inserts (index, view_id, 0) when Some(index). If view developer implemented multiple virtual focus, initializing focus table will expand by on_focus_count(); runtime update_focus_index currently only inserts virtual focus 0.

set_focus

set_focus sets or cancels current focus.

view_id.set_focus(Some(0)); // Focus to first virtual focus
view_id.set_focus(Some(1)); // Focus to second virtual focus
view_id.set_focus(None);    // If current focus is on this ViewId, cancel it

set_focus(Some(index)) requires this (ViewId, virtual_index) already exists in focus table. Views without focus_index set won't be successfully focused. When passing non-existent virtual focus number, method won't change current focus.

set_focus(None) only clears focus held by this ViewId: if current focus is on it, will trigger blur and let window enter no current focus state; if current focus is on other view, won't clear other view.

is_focused

is_focused judges whether current focus is on this ViewId, doesn't distinguish virtual focus number.

if view_id.is_focused() {
    println!("focused");
}

push_focus_scope and pop_focus_scope

These two methods are for runtime focus scope. When opening Modal, Popup, Sidebar, push root view into scope; when closing, pop scope.

dialog_root_id.push_focus_scope();

// When closing popup layer
dialog_root_id.pop_focus_scope();

After pushing into scope, Tab and Shift+Tab only cycle in this root view's subtree. pop_focus_scope will pop current scope, and try to restore focus before entering scope.

View State

ViewId can judge view's runtime interaction state:

let state = view_id.control_state();
let focused = view_id.is_focused();
let hovered = view_id.is_hover();
let active = view_id.is_active();

control_state() priority is Disabled > Active > Focus > Hover > Normal. control_state_with_pressed(pressed) allows view to calculate ControlState with a temporary pressed state, but it only considers Disabled, passed pressed, Hover and Normal.

is_active() queries framework recorded pressed state. is_hover() queries belonging window's current hover target.

Visibility and Layer

z_index controls drawing and hit order under same parent node:

view_id.set_z_index(10);
let z = view_id.z_index();

set_z_index() updates z-index in storage, and re-sorts sibling child nodes when having parent node.

visual() returns whether current view was marked as visible in most recent draw. Framework internally uses it to skip invisible views' on_frame, custom views generally only need to know: this value comes from drawing phase's visibility cache, not complete replacement of layout style.

Scroll

Scroll capability becomes effective after view registers ScrollState:

if view_id.is_scroll_view() {
    let current = view_id.scroll_offset();
    let max = view_id.max_scroll_offset();
}

view_id.scroll_to(0.0, 120.0);
view_id.scroll_by(0.0, 32.0);
view_id.scroll_to_top();
view_id.scroll_to_bottom();

scroll_to and scroll_by clamp target value to 0.0..=max. If this ViewId didn't register scroll state, these methods won't change anything. When scroll position changes, will request redraw.

Mouse Capture

When dragging, scrollbar dragging, holding mouse then continuing to track movement, can capture mouse:

view_id.capture_mouse()?;
view_id.release_mouse()?;

After capturing, window records capture_view_id, platform layer also enters mouse capture state. After releasing, events return to normal hit test path.

Transform and Coordinate Conversion

Declarative transform affects view itself and child views' drawing, layout's accumulated transform and hit test:

use flor::types::Transform2D;

view_id.set_transform(Transform2D::translation(12.0, 0.0));
let transform = view_id.get_transform();
view_id.clear_transform();

set_transform and clear_transform request redraw. After layout refresh, framework calculates each view's accumulated transform. When needing to convert window coordinates to view local coordinates, can use:

let local = view_id.window_to_local_position(mouse_position);

If no accumulated transform, or transform not invertible, this method returns original coordinates.

Render Resource Loading

ViewId implements LoadRenderResource, can use belonging window's renderer to create image resources:

use flor::render::LoadRenderResource;

let image = view_id.load_image(bytes)?;

load_raw_image supports raw frame data and frame delay. After enabling svg feature can also call load_svg. If current ViewId can't find corresponding renderer, these methods return FlorRendererError::RenderNotFound.