--- url: /website/guide/book-index.md --- # The Rambling Book > Translator's note: If you can read Chinese, please prioritize reading the Chinese version; this is the author's original manuscript, other language versions are primarily translated by AI models and are for reference only. ## The Beginning Back when I was learning Rust, I had many things I wanted to build, but I was always a giant in thought and a dwarf in action. One day, I impulsively started exploring Rust GUI frameworks, preparing for my future development. At that time, I felt like there weren't any frameworks with high usability. I tried one of them, but due to API issues, I couldn't even implement a simple interface switching feature. There weren't any frameworks that satisfied me at that time, for a simple reason: every framework had its own problems. Aside from the ecosystem, I believed the biggest issue with existing frameworks was "design". Among them, I found the `miniquad` source code and was inspired by it. Eventually, I decided to develop my own Rust GUI framework. ## Thinking, Design, Philosophy, and Results I developed this framework slowly. From being not very skilled at first, always getting stuck by the compiler, to now it being my most proficient language. `flor` has entered the verification phase with major versions, and the current version is the fifth. This version is very different from my initial thoughts, much like how Rust's creator seeing the current Rust would find it completely different from what they imagined. But unlike them, I'm very satisfied with the current design. The current design blends traditional view-based development with various design elements from modern web development. Its performance completely matches my vision of a Rust GUI framework that wants everything, wants more, and wants even more. ### Initial Design Goals - High performance - Low compiled size - High API consistency - Multi-window support - No forced context binding > I believe the problems with existing Rust GUI frameworks: > > - No multi-window support > - Forced context binding > - Cross-scope data access > - View/component extension > > The biggest problem is forced context binding, so every version of my design worked toward this direction. However, earlier versions leaned more toward a window-class approach similar to C#. But after repeated design verification failures, one day I saw the `floem` framework and was deeply inspired by its declarative layout, signal system, and view management philosophy, entering the fifth version (current version) design. After entering the fifth version design, the API consistency I wanted naturally emerged, and extensibility performed very well, gradually leading to where we are now. ### Current Design Results and Performance: - High performance - Low compiled size - High API consistency - Self-written platform support - No forced context binding - Cross-thread interactive reactive signal system \[1] - Handle exposure, supports native library usage - Multi-window support, can create windows in any thread, any location - Retained mode based on immediate mode \[2], naturally supports high-performance animation. Each window can independently set refresh mode. - Declarative UI DSL - Atomic class style parsing support \[3] - Terminal application experience is simple, complexity absorbed by framework/view authors. - Supports async task scheduling and UI updates, no manual UI thread switching needed \[1]: `Signal` supports cross-thread, implements `Copy` trait, can be moved anywhere, assigned anywhere, without performance issues from waiting for UI thread like in C#.\ \[2]: `Retained mode` is implemented based on `immediate mode`. For views needing high refresh, `retained mode` still has `immediate mode` performance.\ \[3]: Provides `atomic class names` during construction. `Atomic class names` are parsed separately by the framework's layout system and corresponding views in order, efficiently and controllably building application layout. > If you also agree with these design philosophies, you will definitely like this framework. These design characteristics together bring some hidden benefits, such as: - Naturally supports collaboration - Programmers from both traditional development and modern web development can find familiar concepts in it. - Supports async task scheduling and UI updates, no manual thread switching needed ### Design Philosophy / Guiding Principles Summary - Prioritize API usage experience. First think about what API design would be best to use, then think about how to implement it. Usage experience, learning difficulty, and maintenance cost are the framework's core requirements. - Using macros in layout DSL is a GUI nightmare. It has major limitations, not to mention IDE hint support may not be good. It's like a black box. When you write something wrong, error messages may not be clear. Greatly increases framework learning difficulty and maintenance difficulty, so in this framework, macros are used only when necessary, with very low priority. - Performance and compiled size are original requirements, so feature capabilities are split as much as possible to ensure framework's minimal capabilities. ### About the Future Q: Will there be another iteration version, or GUI frameworks with other approaches? A: In my view, all solutions appear to answer some question. This framework's main problem to solve is breaking away from forced context binding. So whether there will be another depends on whether new unanswered questions appear. Other frameworks mostly exist for some specific use, some paradigm, or some type of application. This framework exists because no framework exists without being for a specific purpose. So, this framework's vision is: Let `Flor` become one of the default options developers think of and are willing to try in the Rust ecosystem. When developers need native desktop applications, hope it naturally enters the candidate list, just like Qt for C++, WPF for C#. As the name of this framework implies: > _Flowers bloom one after another, each showing its own brilliance._ --- url: /website/guide/startup.md --- # > Translator's note: If you can read Chinese, please prioritize reading the Chinese version; this is the author's original manuscript, other language versions are primarily translated by AI models and are for reference only. ## Crate Structure Graphics backend and platform layer are at the same dependency level, not depending on each other, selected by core framework via feature. | Crate | Purpose | Dependency Level | | ------------------------- | -------------------------------------------------------------------- | ----------------- | | `flor-base` | Shared types, platform abstraction and graphics abstraction. | L0 Base | | `flor-macros` | Procedural macros (`style!`, `color!`, etc.). | L0 Base | | `flor-graphics-direct2d` | Direct2D rendering backend. | L1 Render Backend | | `flor-graphics-opengl` | OpenGL rendering backend. | L1 Render Backend | | `flor-graphics-tiny-skia` | Tiny Skia rendering backend. | L1 Render Backend | | `flor-platform-windows` | Windows platform implementation. | L1 Platform Layer | | `flor` | Core framework: view system, signals, event loop, window management. | L2 Framework | Lower layer numbers mean more foundational; crates at the same layer don't depend on each other. ## Framework Features `flor` doesn't enable any features by default. Applications usually need at least one rendering backend, then enable layout, atomic class, platform capabilities etc. as needed. ```toml [dependencies] flor = { version = "0.1.0", features = ["direct2d", "tiny-skia", "layout-flex", "class"] } ``` Below are features exposed to users by `flor` crate. View libraries may forward some of these features, but framework-side names are as listed here. For detailed usage of platform capabilities, see [Framework Capabilities](/website/guide/features/overview.md). ### Rendering Backends Rendering backends submit Flor's drawing commands to specific graphics implementations. Applications need to enable at least one backend; if both GPU backend and `tiny-skia` are enabled, it will continue trying CPU backend after GPU initialization fails. | Feature | Backend Type | Backend Crate | Supported Platforms | Purpose | | ----------- | ------------ | ------------------------- | -------------------------------- | -------------------------------------------------------------------------------------- | | `direct2d` | GPU | `flor-graphics-direct2d` | Windows | Windows native Direct2D backend. | | `opengl` | GPU | `flor-graphics-opengl` | Windows (current implementation) | OpenGL backend. | | `tiny-skia` | CPU | `flor-graphics-tiny-skia` | Windows (current implementation) | Pure CPU rasterization backend, can be fallback when GPU backend initialization fails. | `direct2d` and `opengl` both enable `gpu-render-backend` marker; choose one GPU backend in the same build. `tiny-skia` enables `cpu-render-backend` marker, can be enabled together with a GPU backend. ### Drawing Capabilities These features are not backends themselves, but add drawing or resource capabilities on enabled backends. | Feature | Supported Backends | Supported Platforms | Purpose | | ------------- | ----------------------------------- | -------------------------------------------------------------------- | --------------------------------------------- | | `svg` | `direct2d` / `opengl` / `tiny-skia` | Follows enabled rendering backend; current implementation is Windows | Enables SVG resource and SVG drawing support. | | `memory-font` | `direct2d` / `opengl` / `tiny-skia` | Follows enabled rendering backend; current implementation is Windows | Enables loading fonts from memory. | Basic shapes, text, images and other conventional drawing capabilities are provided by rendering backends, no need to additionally enable `svg` or `memory-font`. ### Layout and Class | Feature | Purpose | | -------------- | ---------------------------------------------- | | `layout-flex` | Enables Flex layout related capabilities. | | `layout-grid` | Enables Grid layout related capabilities. | | `layout-block` | Enables Block layout related capabilities. | | `class` | Enables `.class(...)` atomic class capability. | For complete layout builder methods, see [Layout Builder](/website/guide/use/builder/layout.md). For class usage, see [Atomic Class](/website/guide/use/builder/class.md). For layout class syntax list, see [Layout Class Syntax](/website/api/layout-class.md). ### Platform Capabilities | Feature | Purpose | | ------------------------------ | --------------------------------------- | | `clipboard` | Clipboard capability. | | `drag-drop` | Drag-drop events and related handlers. | | `tray` | System tray capability. | | `theme-change` | System theme change related capability. | | `monitor` | Monitor information related capability. | | `hi-dpi` | High DPI related capability. | | `cross-thread-window-creation` | Allows cross-thread window creation. | For feature-controlled parts in event builder and handlers, see [Event Builder](/website/guide/use/builder/event.md) and [Handler API](/website/api/handler.md). ### Development and Internal Selection | Feature | Purpose | | -------------------- | --------------------------------------------------------------------------------------------------------------------------- | | `signal-tracing` | Preserves signal labels in release mode for subsequent tracing and debugging. | | `cpu-render-backend` | CPU rendering chain marker, usually indirectly enabled by `tiny-skia`. | | `gpu-render-backend` | GPU rendering chain marker, usually indirectly enabled by `direct2d` or `opengl`. | | `no-check-backend` | Skips "must enable at least one rendering backend" compile check, usually only for special middle layers or test scenarios. | Applications usually directly enable `direct2d`, `opengl` or `tiny-skia`. Don't manually enable only `cpu-render-backend` / `gpu-render-backend` internal markers. ## Introduction to Each Part ### What you need to know for simply using the framework By default you'll use it with a view library, prioritize these contents: - Initialization and message loop - [Window Creation and Control](/website/guide/use/window.md) - Cross-thread reactive signal system - Use class or declarative builder to configure layout and style - Enable framework features as needed ### What you need to know for developing views By default you already know how to use Flor to write applications, then continue with these contents: - `View trait`: Trait that new views need to implement. Framework manages lifecycle, events, drawing etc. flows. - `ViewId`: View's unique ID, also provides runtime access entries for parent-child relationships, focus, scroll, visibility state, redraw etc. - `Resolver` attribute macro and struct: Generates view-state property template code through enums. - `Render API`: Framework provides a drawing API sufficient for most view drawing; when advanced capabilities are needed, get corresponding handle by backend for custom processing. --- url: /website/guide/glossary.md --- # Glossary This page unifies terminology in Flor documentation. When Chinese/English or API naming is inconsistent, refer to this page. ## Flor Flor is a widget-based GUI framework. In Chinese documentation, you can directly use the name `Flor`. Its Chinese translation is "花" (flower). When describing Flor's type, it's called "框架" (framework). It's not just a library providing scattered utility functions, but a complete GUI framework including widget system, signal system, event loop, window management, rendering entry etc. ## Widget / View In Chinese documentation, uniformly called "控件" (widget). In English context and Rust API, uniformly written as `View`. The `View` naming here references Floem's naming convention; the name stayed in the API. Therefore: - In Chinese explanations write "控件" - In types, traits, method names keep `View` - Don't additionally mix "组件" (component) as the main term For example: ```rust use flor::view::View; fn mount(view: impl View) { // view is usually called "控件" in Chinese documentation. } ``` ## Signal / Signal In Chinese documentation called "信号" (signal), in English and API written as `Signal`. Signal is Flor's reactive state container. After value changes, effects, updaters or widget bindings depending on it are rescheduled. ## Reader / Writer / Reader-Writer These three terms are unified names for a class of signal structs, based on which read/write traits they implement. The semantic core is in traits, not struct names themselves. - Signal struct implementing `Read` can be called reader - Signal struct implementing `Write` can be called writer - Signal struct implementing both `Read` and `Write` can be called reader-writer or read-write signal Common types include: - `ReadSignal`: Reader - `WriteSignal`: Writer - `RwSignal`: Reader-writer or read-write signal Read-write split is mainly used to express business access boundaries: pass reader for read-only places, pass writer for write-only places, pass reader-writer when both are needed. ## Builder `Builder` is kept in API, in Chinese documentation usually called "链式构建 API" (chain build API) or "构建器" (builder). For example `ClassBuilder`, `StyleBuilder`, `LayoutBuilder` are all chain APIs for appending style, layout or behavior to widgets. ## Atomic Class "Atomic class" in Flor documentation specifically refers to the capability brought by `.class(...)` builder, i.e., quickly configuring widgets through space-separated class strings. Atomic class is not an independent widget type, nor does it refer to all style systems. It's an optional feature provided by `ClassBuilder`: after enabling `class` feature, widgets can use `.class("...")` to write layout class and widget style class. ## Resolver `Resolver` is kept in API, in Chinese documentation usually called "解析器" (resolver). Resolver is responsible for converting class, style, state etc. descriptions into actual style or layout data used by widgets. --- url: /website/guide/ai.md --- # AI Flor's [Framework DSL](/website/guide/use/framework-dsl.md) has similarities with React's functional UI expression. This means when using AI to assist writing Flor interfaces, AI doesn't necessarily need prior training specifically for Flor. Since many large models have already formed strong understanding capabilities on React, JSX, functional components, declarative UI etc., Flor's DSL can to some extent reuse this expression habit. In other words, when you describe an interface structure to AI, it can leverage its understanding of React-style UI organization to quickly migrate to Flor's DSL, helping you complete rapid interface building, structure adjustment and style organization. For specific view encapsulation writing, see [Framework DSL](/website/guide/use/framework-dsl.md). This is another manifestation of Flor's "want everything, want more, want even more" philosophy: Want the clear structure of view-based development from traditional GUI frameworks, Want the expression efficiency of modern frontend declarative UI, Want to maximally utilize the functional UI thinking AI is already familiar with, making layout design easier to generate, modify and iterate. Of course, a quiet word: Flor's initial design wasn't deliberately created to fit AI usage scenarios. It just naturally evolved along DSL API design, and only later discovered that this design is naturally suitable for AI understanding and generation. Therefore, Flor's documentation site also chose a documentation framework with AI support capabilities for its own construction. Besides regular documentation for developers to read, this site also provides documentation entries for large language models, convenient for you to provide more model-readable materials when using AI-assisted development. If you want AI to help you write Flor interfaces, explain Flor's DSL, or quickly generate a layout from description, prioritize using the LLM documentation links provided below. ## llms.txt [llms.txt](https://llmstxt.org/) is a standard specification helping LLMs discover and use project documentation. Rspress follows this specification and publishes the following two ready-to-use files: - [https://flor-rs.github.io/website/llms.txt](https://flor-rs.github.io/website/llms.txt): Structured index file, containing titles, links and brief descriptions of all documentation pages. - [https://flor-rs.github.io/website/llms-full.txt](https://flor-rs.github.io/website/llms-full.txt): Complete content file, merging all documentation page content into a single file. You can choose appropriate files based on usage scenarios: - `llms.txt` is smaller, consumes fewer tokens, suitable for letting AI fetch specific pages as needed. - `llms-full.txt` contains full documentation content, no need for AI to follow links one by one, suitable for scenarios where AI needs comprehensive understanding of Flor, but will consume more tokens, recommended to use in AI tools supporting large context windows. --- url: /website/guide/use/init.md --- # ## Initialization and Message Loop Because GUI on any platform cannot escape the message loop and some functionality/API registration, initialization and event loop methods are indispensable. ```rust use flor::{FlorGui, views}; fn main() -> Result<(), Box> { // Initialize, and register some things FlorGui.init()?; // Omit window creation and business code // Enter event loop, this method blocks FlorGui.event_loop()?; Ok(()) } ``` --- url: /website/guide/use/window.md --- # ## Window Creation and Control Windows are created by `WindowOption`, returning the current window's `WindowId` after successful creation. ```rust use flor::windows::WindowOption; use flor_lys::label::label; let window_id = WindowOption { title: "Hello Flor".to_string(), width: 800, height: 600, ..WindowOption::default() } .open(move |_window_id| { label("hello flor ~") })?; ``` `WindowOption` handles window's initial configuration: ```rust pub struct WindowOption { pub title: String, pub width: u32, pub height: u32, pub rem_px: f32, pub wait_v_sync: bool, pub show_fps: bool, pub continuous_rendering: bool, pub background_color: Color, pub tooltip_delay: Duration, } ``` Default values: | Field | Default | Description | | ---------------------- | ---------------------------- | ------------------------------------------------------ | | `title` | `"Window"` | Window title. | | `width` | `800` | Initial window width. | | `height` | `600` | Initial window height. | | `rem_px` | `16.0` | Pixel value corresponding to `1rem` in current window. | | `wait_v_sync` | `true` | Whether rendering backend waits for vertical sync. | | `show_fps` | `false` | Whether to show FPS. | | `continuous_rendering` | `false` | Whether to continuously request redraw. | | `background_color` | `Color::rgb(255, 255, 255)` | Window background color. | | `tooltip_delay` | `Duration::from_millis(500)` | Tooltip response delay. | `continuous_rendering` only controls whether event loop continuously triggers redraw, doesn't change Flor's interface model. Regular GUI applications keep default value; animation, real-time preview, game loop etc. scenarios needing per-frame refresh set to `true`. ## open's View Function `open` signature is: ```rust pub fn open(self, view_fn: F) -> Result where F: Fn(WindowId) -> V + Send + Sync + 'static, V: IntoViewIter, ``` `view_fn` is the window root view's build function. After Flor creates platform window, renderer and window entry, it passes current window's `WindowId` to it, and converts return value to window root view tree. ```rust use flor::platform::WindowId; use flor::view::View; use flor_lys::label::label; fn build_view(_window_id: WindowId) -> impl View { label("hello flor ~") } WindowOption::default().open(build_view)?; ``` If you don't need to use window ID, write it as `_window_id`. ## How to Use WindowId in open Parameter It is not recommended to directly call window control methods using the `window_id` in `open(move |window_id| { ... })` during the root view build phase. At this time the window is completing initialization, Flor is still mounting the root view, registering the renderer, initializing focus and refreshing layout; directly calling `set_size`, `set_window_mode`, `request_redraw`, `destroy` etc. in this closure easily makes the initialization order unexpected, producing extra redraw, layout state inconsistency or platform layer behavior issues. This `WindowId` is more suitable for being captured into view events, used to control current window in subsequent events: ```rust use flor::platform::base::WindowApi; use flor::view::builder::EventBuilder; use flor_lys::button::button; WindowOption::default().open(move |window_id| { button("Close Window").on_click(move || { let _ = window_id.destroy(); }) })?; ``` If just setting initial title, size, background color, refresh mode, should prioritize writing in `WindowOption` fields, not changing in `open`'s closure. ## WindowId `WindowId` is platform window's handle wrapper. In current Windows platform implementation it's: ```rust #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub struct WindowId(pub isize); ``` It implements `WindowApi` and `WindowOperations`. Need to import trait when calling these methods: ```rust use flor::platform::base::{WindowApi, WindowOperations}; use flor::platform::WindowId; ``` ### Creation and Lifecycle | API | Purpose | Usage Suggestion | | ------------------------------------------------ | --------------------------------------------- | ------------------------------------------------------------------------------------- | | `WindowOption::open(view_fn)` | Create Flor window and mount root view. | Application side prioritize using this entry. | | `WindowApi::create_window(title, width, height)` | Only create platform window. | Framework internal use; regular applications shouldn't bypass `WindowOption::open`. | | `update_window()` | Synchronously trigger platform window update. | Initialization flow internally calls; application side usually doesn't manually call. | | `destroy()` | Destroy window. | Suitable to call in button click, menu command etc. events. | ### Display and Window Mode | API | Purpose | | ----------------------- | ------------------------- | | `show()` | Show window. | | `hide()` | Hide window. | | `set_window_mode(mode)` | Set window mode. | | `get_window_mode()` | Read current window mode. | `WindowMode` includes `Normal`, `Minimized`, `Maximized`, `Fullscreen`. In current Windows implementation, `Fullscreen` is temporarily treated as maximized. ### Position and Size | API | Purpose | | ----------------------------------------- | ------------------------------------------------------ | | `get_left()` / `get_top()` | Read window top-left screen coordinates. | | `set_left(left)` / `set_top(top)` | Set window horizontal or vertical position separately. | | `set_position((x, y))` | Set window position simultaneously. | | `get_width()` / `get_height()` | Read entire window width/height. | | `set_width(width)` / `set_height(height)` | Set window width or height separately. | | `set_size((width, height))` | Set window size simultaneously. | | `get_client_size()` | Read client area size. | | `get_client_rect()` | Read client area rectangle on screen. | | `get_window_rect()` | Read entire window rectangle on screen. | Position uses `i32`, allowing negative coordinates in multi-monitor environments; size uses `u32`. ### DPI, IME and Mouse | API | Purpose | | ------------------------------- | ----------------------------------------------------------------------------------------------------------- | | `get_scale_factor()` | Read DPI scaling factor; current Windows implementation is still `todo!()`, don't call in application side. | | `get_dpi()` | Read window DPI. | | `set_ime_window_location(rect)` | Set IME candidate window position. | | `set_ime_open_state(is_open)` | Open or close IME state. | | `set_ime_allowed(allow)` | Allow or disable IME. | | `set_cursor(cursor)` | Set current cursor. | | `drag_window()` | Trigger system window drag. | | `capture_mouse()` | Capture mouse. | | `release_mouse()` | Release mouse capture. | ### Redraw | API | Purpose | | ------------------ | ------------------------------------- | | `request_redraw()` | Asynchronously request window redraw. | Regular view state changes are automatically requested for redraw by Flor. Only when you directly change external state through window or platform capabilities that framework can't detect, you need to manually call `request_redraw()`. --- url: /website/guide/use/signal.md --- # Flor Signal Reactive System Flor framework has a built-in lightweight reactive signal system. This signal system is inspired by [Floem](https://github.com/lapce/floem), referencing its API design, but completely independently implemented by us. Unlike Floem, because of Flor's design goals, **Flor Signal naturally supports cross-thread use**, signals can be safely read, written and passed in multi-threaded environments. This article mainly explains how to use Signal. For complete function signatures and trait lists, see [Signal API Reference](/website/api/signal.md). ## Creating Signals Signal usage entry points are basically all under `flor::signal`. First remember these `create_*` APIs, can complete most scenarios: | API | Purpose | | ---------------------------------------------- | ------------------------------------------------------------------- | | `create_signal(value)` | Create a readable and writable value signal | | `create_rw_signal(value)` | Create value signal, and directly split into reader and writer | | `create_signal_with_label(value, label)` | Create value signal with debug label | | `create_rw_signal_with_label(value, label)` | Create value signal with debug label, and directly split read-write | | `create_list_signal(vec)` | Create a readable and writable list signal | | `create_rw_list_signal(vec)` | Create list signal, and directly split into reader and writer | | `create_list_signal_with_label(vec, label)` | Create list signal with debug label | | `create_rw_list_signal_with_label(vec, label)` | Create list signal with debug label, and directly split read-write | ### Value Signal Most commonly used is `create_signal`. It returns a `RwSignal`, can read and write. ```rust use flor::signal::{create_signal, Read, Write}; // Create a readable and writable signal storing i32. let count = create_signal(0); // Reading requires Read trait. let value = count.get(); // Writing requires Write trait. count.set(value + 1); // In-place modification based on old value. count.update(|value| *value += 1); ``` If reading and writing will be handed to different code, using `create_rw_signal` can directly get a pair of read-write handles when creating: ```rust use flor::signal::{create_rw_signal, Read, Write}; // read can only read, write can only write. let (read, write) = create_rw_signal(0); let value = read.get(); write.set(value + 1); ``` Can also first create `RwSignal`, then split or derive where needed: ```rust use flor::signal::{create_signal, Read, Write}; let count = create_signal(0); // Derive read-only and write-only handles. let read = count.as_read(); let write = count.as_write(); write.set(read.get() + 1); // Or directly split into a pair of read-write handles. let (read, write) = count.split(); write.set(read.get() + 1); ``` ## Constant Signal Constant signal is a practical capability: it allows API to simultaneously accept fixed values and reactive values. For example, view property can pass `"Hello"`, or pass a closure reading signal. `ConstSignal` doesn't enter global runtime, doesn't establish subscription, `destroy()` is also empty operation. It just wraps a normal value into a reader implementing `Read`. Most times don't need to directly write `ConstSignal::new`, using `IntoRead` is more natural: ```rust use flor::signal::{IntoRead, Read}; // &str will be converted to ConstSignal. let title = "Hello".into_read(); assert_eq!(title.get(), "Hello".to_string()); ``` `IntoRead` already covers common basic types, `String`, `&str`, `&String`, `ConstSignal`, `ReadSignal` and `RwSignal`. This lets API unify "fixed value" and "reactive value" into reader processing. ## Reading and Writing ### Clone Read `get()` and `try_get()` will clone value, therefore requires `T: Clone + 'static`. ```rust use flor::signal::{create_signal, Read}; let name = create_signal("Flor".to_string()); // get will clone current value. let value = name.get(); // try_get returns None when signal is destroyed. let maybe_value = name.try_get(); ``` `get()` panics when signal doesn't exist or type mismatches; `try_get()` returns `None` when signal doesn't exist. ### Reference Read If value is large, don't want to clone, can use `get_ref()` or `try_get_ref()`. They return `SignalRef<'_, T>`, can be used like `&T`. ```rust use flor::signal::{create_signal, Read, Write}; let name = create_signal("Flor".to_string()); { // get_ref avoids clone, but will hold read lock during guard lifetime. let name_ref = name.get_ref(); assert_eq!(name_ref.len(), 4); } // Write to same signal after guard released. name.set("Signal".to_string()); ``` `SignalRef` holds underlying read lock. Before guard is released, don't call `set()`, `update()` and other operations requiring write lock on same signal in same thread, otherwise may deadlock. ### Writing `set()` will replace entire value, `update()` will modify value in-place. ```rust use flor::signal::{create_signal, Write}; let count = create_signal(0); // Replace entire value. count.set(10); // In-place modification based on old value. count.update(|value| *value += 1); ``` If signal may have been destroyed, use `try_set()` and `try_update()`: ```rust use flor::signal::{create_signal, Signal, Write}; let count = create_signal(0); // Here simulate signal has been destroyed by dynamic window or temporary page. count.destroy(); // try_* returns false when signal doesn't exist, instead of panic. assert!(!count.try_set(1)); assert!(!count.try_update(|value| *value += 1)); ``` ## Reactive Update ### `create_effect` `create_effect` creates basic side effect. Closure will immediately execute once, and receive value returned by previous execution. ```rust use flor::signal::{create_effect, create_signal, Read, Write}; let count = create_signal(0); create_effect(move |previous: Option| { // Reading signal in effect will automatically establish dependency. let current = count.get(); println!("count: {current}, previous: {previous:?}"); // Return value will become previous for next execution. current }); // After writing count, effect depending on count will be re-executed. count.set(1); ``` First execution `previous` is `None`. Subsequent triggered by signal update, `previous` is value returned by previous closure. ### `create_updater` `create_updater` is more suitable for UI and derived state binding. It first executes `compute` to get initial value and returns; subsequent dependency changes re-execute `compute`, then give new value to `on_change`. ```rust use flor::signal::{create_signal, create_updater, Read, Write}; let count = create_signal(0); let initial = create_updater( // compute: read signal and generate derived value. move || format!("Value: {}", count.get()), // on_change: receive new derived value after dependency update. |value| println!("updated: {value}"), ); assert_eq!(initial, "Value: 0"); // Trigger dependency update; on_change will receive new value in subsequent update. count.set(5); ``` Note: `on_change` won't be called during initial calculation, only called after dependency update. ### `create_updater_with_id` Normal business code usually just uses `create_updater`. `create_updater_with_id` is mainly for framework internal use, it returns `(Id, R)`, where `Id` is effect id, `R` is initial value. Framework internally uses it to delay some UI effects until after view activation then enqueue, for example class, layout, transform and other builders. Reason for delay is because during layout assembly, view's `ViewId` may not have been mounted to window and parent view. After view activation, putting effect id into update queue can let these UI updates execute in complete view relationship. ## List Signal List signal saves `Vec`, and allocates independent row-level `Id` for each list element. Structure changes subscribe to list `Id`; reading or modifying some element will subscribe or trigger that element's own row-level `Id`. ### Creation and Basic Operations ```rust use flor::signal::{create_list_signal, ListRead, ListWrite}; // List signal saves Vec. let items = create_list_signal(vec![1, 2]); // Structure write. items.push(3); items.insert(1, 10); // Element write. items.set(0, 100); items.update(2, |value| *value += 1); // Read structure and elements. assert_eq!(items.len(), Some(4)); assert_eq!(items.get(0), 100); // Remove returns removed element. let removed = items.remove(1); assert_eq!(removed, 10); ``` ### Reading List ```rust use flor::signal::{create_list_signal, ListRead}; let items = create_list_signal(vec![1, 2, 3]); // Read structure info. assert_eq!(items.len(), Some(3)); assert_eq!(items.len_or_zero(), 3); assert!(!items.is_empty()); // Read element values. assert_eq!(items.get(0), 1); assert_eq!(items.try_get(10), None); // Read entire list. assert!(items.contains(&2)); assert_eq!(items.to_vec(), vec![1, 2, 3]); ``` `len()` returns `Option`, returns `None` after signal destroyed; `len_or_zero()` treats non-existent list as empty list. `get(index)` panics when list doesn't exist or out of bounds; `try_get(index)` returns `None` when list doesn't exist or out of bounds. ### No-Clone Read When need to avoid clone, can use `for_each_ref()` or `try_borrow()`. ```rust use flor::signal::{create_list_signal, ListRead}; let items = create_list_signal(vec!["a".to_string(), "b".to_string()]); // for_each_ref can avoid cloning each element. items.for_each_ref(|item| { println!("{item}"); }); // try_borrow returns list read-only borrow, suitable for controlling traversal method yourself. if let Some(list_ref) = items.try_borrow() { for item in list_ref.iter() { println!("{item}"); } } ``` `for_each_ref()` will subscribe to list structure and row-level signals traversed. `try_borrow()` currently only subscribes to list structure; if effect needs to re-execute after element value is `set(index, ...)` or `update(index, ...)`, prefer using `get()`, `to_vec()`, `for_each_ref()` or `contains()`. ### Safe Write Version All list write methods have corresponding safe versions: | panic version | safe version | on failure | | ---------------------- | -------------------------- | --------------- | | `push(value)` | `try_push(value)` | returns `false` | | `insert(index, value)` | `try_insert(index, value)` | returns `false` | | `set(index, value)` | `try_set(index, value)` | returns `false` | | `update(index, f)` | `try_update(index, f)` | returns `false` | | `remove(index)` | `try_remove(index)` | returns `None` | | `clear()` | `try_clear()` | returns `false` | Structure changes trigger list `Id`; element value changes only trigger that element's row-level `Id`. Therefore, effect only reading `len()` won't re-execute because of `set(index, ...)`; effect reading some element, `to_vec()` or `for_each_ref()` will be sensitive to corresponding element value changes. ## Batch Update `batch` will open batch processing mode in current thread. Signal writes in closure won't immediately add each change to update queue, instead collect changed signal `Id`, after closure ends uniformly enqueue and deduplicate. ```rust use flor::signal::{batch, create_signal, Write}; let a = create_signal(0); let b = create_signal(0); batch(|| { // Same signal written multiple times in one batch, will deduplicate by signal Id. a.set(1); a.set(2); // Different signals will still separately trigger their own subscription updates. b.set(3); }); ``` `batch` deduplication granularity is signal `Id`. Same signal written multiple times in one `batch`, will only enqueue once for this signal after closure ends; so above `a.set(1)` and `a.set(2)` will only trigger one `a` subscription update, subscriber sees value after last write. Different signals will still separately enqueue, for example `a` and `b` will each trigger their own subscription updates. `batch` is thread-local, only affects current thread. Signal writes in other threads won't enter current thread's batch processing collection. If closure panics, `batch` will restore batch processing state, then continue propagating panic outward. ## Integration with UI Views > This chapter belongs to "View Development" content. If don't need to develop views, quick understanding is enough. Flor UI views usually bind reactive values through `create_updater`. Take `Label::new` pattern as example: ```rust pub fn new(title: P) -> Self { let view_id = ViewId::new_with_layout(|view_id| LayoutResolver::new(view_id)); let title = create_updater( move || title.make(), move |value| view_id.update_state(Box::new(value)), ); Self { view_id, title, style: LabelStyleResolver::new_with_compute_func(view_id, computed_label_style), measure_cache: FxHashMap::default(), } } ``` User side usually passes closure reading signal: ```rust use flor::signal::{create_signal, Read, Write}; use flor_lys::label::label; let title = create_signal("Hello".to_string()); let view = label(move || title.get()); title.set("World".to_string()); ``` Workflow: 1. `create_updater` first executes `compute` once, gets view initial value. 2. When reading signal in `compute`, current effect id is recorded in thread-local `SCOPE`. 3. `Read::track()` or `ListRead::track()` subscribes current effect to read signal. 4. When signal writes, runtime adds corresponding signal id to update queue. 5. Flor event loop calls `RUNTIME.execute_update_queue()`, runs subscribed effects. 6. `on_change` calls `ViewId::update_state` or other UI update entry points. ## Usage Suggestions Prefer passing reactive values through closures: ```rust label(move || title.get()); ``` Don't pass `title.get()` result directly to API needing reactive update. That only reads a normal value once. Use `ReadSignal`, `WriteSignal` to express business intent: ```rust let (read, write) = create_signal(0).split(); child(read); store_writer(write); ``` Avoid writing to signal you depend on in effect: ```rust create_effect(move |_| { let value = count.get(); count.set(value + 1); value }); ``` This kind of code will repeatedly trigger itself in subsequent updates, easily forming infinite updates. When holding `SignalRef` or `ListRef`, first release reference guard, then write to same signal. For complete function signatures and trait lists, see [Signal API Reference](/website/api/signal.md). ## Design Explanation ### Lifetime All value signals and list signals are globally alive. `RwSignal`, `ReadSignal`, `WriteSignal`, `RwListSignal`, `ReadListSignal`, `WriteListSignal` are all `Copy` handles, copying handle won't copy underlying value, won't change lifetime. Global alive is Flor's tradeoff to keep signal handles `Copy` usable: after signal created, it lives in global runtime, dropping some handle won't release underlying data. Flor is not only suitable for large even super large GUI, also suitable for small tool programs; in lightweight programs, Flor's compiled size performance is also one of core design goals. For small tools, simple windows, stable interface structure, window lifetime close to process lifetime programs, letting signals naturally be recycled with process end is usually one of recommended writing styles, memory and performance impact is small. `destroy()` is more suitable for dynamic resources with clear ending lifetime, for example windows, pages, panels that can be repeatedly created and destroyed, or temporary signals with large quantity in heavy, super heavy GUI. After using `destroy()`, then combine with `try_get()`, `try_set()`, `try_update()` to handle possibly invalid handles. ```rust use flor::signal::{create_signal, Read, Signal, Write}; let count = create_signal(1); // Signal handle is Copy; another points to same underlying signal. let another = count; assert!(count.exists()); assert_eq!(another.get(), 1); // After destroying underlying signal, all handles pointing to it will invalid. count.destroy(); assert!(!another.exists()); assert_eq!(another.try_get(), None); assert!(!another.try_set(2)); ``` When list signal is destroyed, it will simultaneously destroy all row-level signals inside it. ### Read-Write Semantics Flor's read-write capability is provided through traits. `ReadSignal` only implements `Read`, so under normal type constraints can't call `set()` or `update()`; `WriteSignal` only implements `Write`, so can't call `get()`. Read-write split expresses business semantics, not security boundary or permission isolation. This sentence doesn't mean reader has writer's permission: `ReadSignal` itself has no write methods, when passed by `Read` constraint can only read; `WriteSignal` itself has no read methods, when passed by `Write` constraint can only write. Read is read, write is write, can maintain clarity in business code and source code search. Therefore, feel free to use corresponding signal types to express business access boundaries: places only needing read pass `ReadSignal`, places only needing write pass `WriteSignal`, when need both then pass `RwSignal`. Such split can make API, call points and code reading all clearer. Meanwhile, Flor won't let this semantic split hold back usage. When you really need write, code will appear `as_write()`, `split()` or writer parameter, reader can clearly see here got new access capability; under normal paths, reader still won't have write permission. ## Debug Label Can set labels for read-write signals, convenient for debugging and tracing. ```rust use flor::signal::{create_list_signal_with_label, create_signal_with_label}; let count = create_signal_with_label(0, "counter"); let items = create_list_signal_with_label(vec![1, 2, 3], "items"); count.set_label("main_counter"); items.set_label("main_items"); ``` Labels are only written to runtime in `debug_assertions` or when `signal-tracing` feature enabled. Currently this part mainly provides runtime-side label recording, complete visual debugging experience still needs to wait for `devtools` related capability to mature; therefore it's more suitable as foundation capability for subsequent tracing and debugging tools, rather than already completed user-side debugging panel. ## Deep: Runtime Mechanism This part is for understanding Signal's internal behavior; normal usage doesn't need to master these details first. `RUNTIME` saves following core states: | Field | Purpose | | ---------------------- | ----------------------------------------------------- | | `values` | Value signals and effect last return values | | `list_signal` | List signals and their row-level elements | | `subscribe` | `Signal Id -> Effect Id` subscription relationship | | `effects` | effect id to effect instance mapping | | `effect_subscriptions` | how many signals subscribe to effect | | `update_queue` | signal id or effect id waiting to execute | | `labels` | debug labels, only exist in debug or `signal-tracing` | Dependency tracking relies on thread-local `SCOPE`: 1. Before effect execution, write current effect id to `SCOPE`. 2. `get()`, `get_ref()`, `ListRead` read methods will call `track()`. 3. `track()` reads `SCOPE`, subscribes current effect to corresponding signal id. 4. When writing signal, runtime adds written signal id to update queue. 5. Event loop executes update queue, finds effects subscribing to that signal and runs. `batch` uses thread-local `BATCH` to collect signal ids. After batch processing ends, these ids are uniformly added to global update queue. Current implementation won't actively clear old dependencies before each effect re-execution. For dynamic branches where dependency set frequently changes, should try to keep read range stable, avoid long-term accumulating no longer needed subscriptions. --- url: /website/guide/use/handler.md --- # External Events External events are a way to append event logic to already existing views. You don't need to implement a new `View` just to handle one click, one mouse move, one shortcut. Create view then chain call `on_*` methods, hand closure to Flor. These `on_*` methods come from `EventBuilder`, import it before use: ```rust use flor::view::builder::EventBuilder; ``` Complete signature see [Handler API](/website/api/handler.md). This page first learns by usage process: start from one click, then gradually use mouse position, keyboard, focus, lifecycle and drag-drop. ## First Click Event Most common usage is directly chaining `.on_click(...)` after view: ```rust use flor::signal::{Read, Write, create_signal}; use flor::view::builder::EventBuilder; use flor_lys::button::button; use flor_lys::label::label; let count = create_signal(0); let click_count = count; let add = button("Add One").on_click(move || { click_count.update(|value| *value += 1); }); let text = label(move || format!("count: {}", count.get())); ``` The complete `on_click` closure has three parameters: ```rust |view_id, key_state, mouse_position| { ... } ``` At first you can write a no-argument closure like above and focus only on the business logic. When you need the event source, mouse position, or modifier key state, add the parameters back. Mouse events also support receiving only `ViewId`, or omitting `ViewId` and receiving only `KeyState` and `MousePosition`. If different examples show different parameter counts, that does not mean the event has multiple dispatch rules. It is the same handler conversion mechanism. Event Builder converts the closure, function, or method item you pass into the target handler. Handlers with extra event parameters usually support four writing forms: | Form | Example | Use Case | | ---------------- | ------------------------------------------------ | -------------------------------------------- | | Full arguments | `\|view_id, key_state, mouse_position\| { ... }` | You need the full event context | | No arguments | `\|\| { ... }` | You only need to trigger business logic | | `ViewId` only | `\|view_id\| { ... }` | You only care which view triggered the event | | Without `ViewId` | `\|key_state, mouse_position\| { ... }` | You only care about event data | Events whose complete parameter list is only `ViewId`, such as `on_mouse_enter` and `on_create`, only support the "full arguments" and "no arguments" forms. For the underlying mechanism, see [Mechanism Explanation](#mechanism-explanation). For complete signatures, see [Handler API](/website/api/handler.md#intoeventhandler). Event closures usually write signal, send command, open window, request redraw, or post business message to own task system. Just remember one thing: event closures will execute in GUI event processing flow, don't do long blocking work inside. ## Any View Can Attach Events `EventBuilder` is implemented for all types implementing `ViewIdentity`, so events are not button exclusive. A label, a container, or a custom view can attach external events as long as it can provide a `ViewId`. A function returning `impl IntoView` can also continue chaining event bindings: ```rust use flor::view::builder::EventBuilder; use flor_lys::label::label; let title = label("Click Title").on_click(|view_id, _, _| { println!("clicked {view_id}"); }); ``` The first complete argument is the `ViewId` of the view that triggered the event. It can be used to access view state, request redraw, set focus, capture mouse, and similar operations. Normal business code may not need it and can omit it directly; when you need to associate an event with a specific view, it is the most direct entry point. ## Mouse Position and Button State Mouse-type events mostly use same set of parameters: ```rust |view_id, key_state, mouse_position| ``` `key_state` records mouse button and modifier key state when event occurs; `mouse_position` saves coordinates. For `on_mouse_move`, `on_button_down`, `on_button_up`, `on_click`, `on_double_click` — these kinds of mouse hit events, coordinates will be converted to target view's local coordinates, `(0, 0)` means view top-left corner. ```rust use flor::view::builder::EventBuilder; use flor_lys::label::label; let area = label("Drag Here").on_mouse_move(|view_id, key_state, pos| { if key_state.lbutton_is_down { println!("drag {view_id}: {}, {}", pos.x, pos.y); } }); ``` Click event is synthetic event: only when left button down and up both land on same hit view, Flor will trigger `on_click`. If just want to know when button down or up, use `on_button_down` and `on_button_up`. ```rust use flor::view::builder::EventBuilder; use flor_lys::label::label; let area = label("press area") .on_button_down(|_, _, pos| { println!("down at {}, {}", pos.x, pos.y); }) .on_button_up(|_, _, pos| { println!("up at {}, {}", pos.x, pos.y); }); ``` Right button also has corresponding events: ```rust use flor::view::builder::EventBuilder; use flor_lys::label::label; let item = label("Right Button Operation").on_right_button_click(|_, _, pos| { println!("right click: {}, {}", pos.x, pos.y); }); ``` Middle button currently exposed `on_middle_button_down`, `on_middle_button_up` and `on_middle_button_double_click`. Source code has internal middle click synthesis logic, but external event API currently doesn't have `on_middle_button_click`. ## Keyboard Events Need to Get Focus First Keyboard events are dispatched to currently focused view. Normal views if want to participate in Tab focus order, need to pair with `focus_index`. More complete focus explanation see [Focus Mechanism](/website/guide/use/focus.md). ```rust use flor::base::platform::{HandleResult, KeyCode}; use flor::view::builder::{EventBuilder, FocusIndexBuilder}; use flor_lys::label::label; let shortcut_area = label("Press Ctrl+S") .focus_index(1) .on_key_down(|code, is_alt, is_ctrl, is_shift| { if !is_alt && is_ctrl && !is_shift && code == KeyCode::S { println!("save"); HandleResult::Handled } else { HandleResult::Default } }); ``` `on_key_down` and `on_key_up` both need to return `HandleResult`. Keyboard events can use the full arguments `|view_id, code, is_alt, is_ctrl, is_shift|`, or omit `ViewId` as above: | Return Value | Meaning | | ----------------------- | ------------------------------------------------ | | `HandleResult::Handled` | This key is already handled by your code | | `HandleResult::Default` | Hand over to default flow to continue processing | Flor internal view logic and external handler both may return `Handled`. As long as either side returns `Handled`, final result is `Handled`. ## Focus Change `on_focus` and `on_blur` are used to listen to focus enter and leave. Closure receives `ViewId` and a `u16` type virtual focus number. Most views only have one focus point, this number is usually `0`. Few composite views can expose multiple virtual focus points through `View::on_focus_count`, then same view internally can distinguish different focus positions. ```rust use flor::view::builder::{EventBuilder, FocusIndexBuilder}; use flor_lys::label::label; let focusable = label("Focusable Area") .focus_index(1) .on_focus(|virtual_index| { println!("focus virtual index: {virtual_index}"); }) .on_blur(|| { println!("blur"); }); ``` ## Create Event `on_create` will trigger after view tree mounts to window. It's suitable for doing initialization work depending on `ViewId` already entering view tree. ```rust use flor::view::builder::EventBuilder; use flor_lys::label::label; let view = label("created").on_create(|view_id| { println!("created {view_id}"); }); ``` If just initializing normal Rust data, usually can directly complete before creating view. Only when logic depends on view already registered, already has `ViewId` and window relationship, then consider `on_create`. ## Wheel Event Current wheel external event API name is `on_wheel_settings_changed`. It triggers in mouse wheel message path, closure receives scroll direction, scroll amount, button state and mouse position. ```rust use flor::base::platform::ScrollAxis; use flor::view::builder::EventBuilder; use flor_lys::label::label; let wheel_area = label("Scroll Here").on_wheel_settings_changed( |axis, delta, key_state, pos| { match axis { ScrollAxis::Vertical => println!("vertical wheel: {delta}"), ScrollAxis::Horizontal => println!("horizontal wheel: {delta}"), } if key_state.control_is_down { println!("ctrl wheel: {}, {}", pos.x, pos.y); } }, ); ``` Wheel dispatch will from hit view upward look for scrollable ancestor; if not found, then post to window root view. Currently this path passes in window client area coordinates, not target view local coordinates. ## Drag-Drop Events Drag-drop events need to enable `drag-drop` feature. Common flow is: 1. `on_drag_enter` judges dragged in data format, sets `DropEffect`; 2. `on_drag_over` continuously updates effect during drag move; 3. `on_drop` reads real data and completes business processing; 4. `on_drag_leave` cleans up hover state. ```rust use flor::base::platform::{DragData, DragFormat, DropEffect}; use flor::view::builder::EventBuilder; use flor_lys::label::label; let drop_area = label("Drag Files Here") .on_drag_enter(|_, _, formats, effect| { let accepts_files = formats .iter() .any(|format| matches!(format, DragFormat::Files(_))); if accepts_files { *effect = DropEffect::Copy; } }) .on_drop(|_, _, data, effect| { if let DragData::Files(files) = data { for file in files { println!("drop file: {}", file.display()); } *effect = DropEffect::Copy; } }); ``` `on_drag_enter` and `on_drag_over` receive available format list, haven't really extracted data; `on_drop` will receive `DragData`. Same as wheel, current drag-drop path passes in window client area coordinates. ## Usage Suggestions External events are suitable for application layer logic: click button modify signal, press shortcut trigger command, drag-drop files to panel, update preview state when mouse move. If you are writing view library, and event logic is view itself inseparable part, prefer to implement `View`'s internal `on_*` methods. For example how input field handles IME, cursor, selection and text editing, should belong to view internal logic; application side only needs to bind higher-level external events or the view's own exposed business callbacks. Event closures need to satisfy `Send + Sync + 'static`. Common practice is capture signal handle, `Arc`, channel sender, or other data that can be safely stored long-term. Don't capture temporary references. ## Mechanism Explanation This section is for understanding external events' position in Flor. Normal use doesn't need to read here first. When each `ViewId` is created, Flor will put a default `ViewHandler` in `VIEW_STORAGE.handlers`. `ViewHandler` is a set of optional handler slots, for example `on_click_handler`, `on_key_down_handler`, `on_focus_handler`. When you call: ```rust view.on_click(|view_id, key_state, pos| { // ... }) ``` `EventBuilder` will take out this view's handler storage, convert the closure through `IntoEventHandler` into the corresponding handler wrapper type, and write to `ViewHandler`'s `on_click_handler` slot. Method returns original `self`, so it can naturally continue chain calling. This conversion is trait-driven: the full-argument signature first goes through the handler wrapper type's `From` implementation, while simplified signatures are handled by other `IntoEventHandler` implementations that fill in the omitted arguments. `Args` is a marker type used for Rust trait inference. You usually do not write it when calling event methods; you only need to carry this generic parameter out when you write your own function that accepts an event handler. See [Framework DSL: Method Composing Views](/website/guide/use/framework-dsl.md#method-composing-views) for the forwarding pattern. After event enters Flor from platform layer, approximate path is: 1. Platform window procedure converts system message into `Message`; 2. `WindowsProcHandler` hands message to `windows::event`; 3. Event bus processes by event type: mouse events first hit test, keyboard events find current focus, drag-drop events maintain current drag-drop target; 4. After finding target `ViewId` call corresponding `call_*` method; 5. `call_*` first executes view internal `View::on_*`, then executes external handler. Most events are "internal logic + external logic" both will execute. Keyboard events will merge both sides' `HandleResult`: either side returns `Handled`, final is `Handled`. Drag-drop events will first use internal `on_drag_*` return value to initialize `DropEffect`, then hand `&mut DropEffect` to external handler to modify. Tooltip is an internal special case: its `call_tooltip_show` and `call_tooltip_hide` use override mode, if external handler exists, only execute external handler; otherwise execute view internal method. However current `EventBuilder` doesn't expose the tooltip corresponding chain method, normal users temporarily don't need to depend on this detail. There are also some handler slots already existing in API, but current event bus hasn't completely dispatched to external handler, for example `on_context_menu`, `on_destroy`, `on_resize`, `on_close_requested`, `on_work_area_changed`, `on_dpi_change` and `on_theme_changed`. When querying specific status, use [Handler API](/website/api/handler.md)'s "Current Dispatch Status" as standard. --- url: /website/guide/use/control-state.md --- # View State Flor framework has implemented a complete view state system to manage a view's state performance in different interaction scenarios. ## ControlState Enum Definition ```rust #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Default)] pub enum ControlState { #[default] Normal, Focus, Hover, Active, Disabled, } ``` ## State Model Design Flor framework defines five basic states for views: - **Normal**: Normal state, a view's default state - **Focus**: Focus state, when a view gets keyboard focus - **Hover**: Hover state, when mouse hovers over a view - **Active**: Active state, when a view is pressed or clicked - **Disabled**: Disabled state, when a view is not interactive ## State Classification Among these states, they can be divided into two categories: ### Enhanced States Enhanced states are states added on top of base state, won't prevent user interaction: - **Focus**: Supplementary state, indicates a view got focus - **Hover**: Supplementary state, indicates mouse hover - **Active**: Supplementary state, indicates a view is activated ### Terminal State Terminal state will prevent user interaction, a view enters unavailable state: - **Disabled**: Independent state, a view is completely disabled ## State Inheritance Relationship Flor framework implements state inheritance relationship as follows: ``` Normal ├─ Focus ├─ Hover │ └─ Active └─ Disabled (Independent) ``` ## Inheritance Rules | State | Inherits From | Reason | | -------- | ------------------------- | ------------------------------------------------------------------- | | Normal | None | Base state, foundation for all other states | | Focus | Normal | Focus is just supplementary, doesn't change a view's basic behavior | | Hover | Normal | Hover is just supplementary, provides visual feedback | | Active | Hover -> Focus -> Normal | Active is usually continuation of Hover, user presses the view | | Disabled | Doesn't inherit any state | Disabled should be completely isolated, prevent all interactions | ### Inheritance Chain Explanation **Active's inheritance chain**: - First check if in Hover state - If not Hover, check if in Focus state - If neither, inherit from Normal state **Disabled's special handling**: - Disabled state is completely independent, doesn't inherit any other state's styles - In disabled state, a view displays unavailable visual effect - All interaction events are ignored ## State Transition Transitions between view states follow these rules: 1. **Normal → Focus**: A view gets keyboard focus 2. **Normal → Hover**: Mouse enters the view area 3. **Hover → Active**: User presses mouse button 4. **Active → Hover**: User releases mouse button (still in the view area) 5. **Any state → Disabled**: A view is disabled 6. **Disabled → Normal**: A view is enabled > Framework built-in system's state transition doesn't represent the final view's behavior pattern ## Practical Application In actual use, you can use state prefixes to apply different styles: ```rust let button = button("Click Me") .class("px-4 py-2 rounded bg-blue-500 text-white") .class("hover:bg-blue-600") .class("active:bg-blue-700") .class("focus:ring-2 focus:ring-blue-300") .class("disabled:bg-gray-300 disabled:text-gray-500 disabled:cursor-not-allowed"); ``` > This is just syntax concept demonstration, class support situation please refer to actual specific documentation. ## State Prefixes Flor supports using following state prefixes to define conditional styles: - `normal:`: Normal state (usually doesn't need explicit specification) - `hover:`: Mouse hover state - `focus:`: Got focus state - `active:`: Active state - `disabled:`: Disabled state This state management approach allows you to flexibly control a view's performance under different interaction states, providing good user experience. --- url: /website/guide/use/builder/focus.md --- # Focus Builder This page only introduces builder methods for configuring focus table. Complete explanation of focus mechanism see [Focus Mechanism](/website/guide/use/focus.md), runtime APIs for manipulating focus through `ViewId` see [ViewId](/website/guide/control/view-id.md). Actually exposed trait is: ```rust pub trait FocusIndexBuilder { fn focus_scope(self, focus_scope: u32) -> Self; fn focus_index(self, focus_index: u32) -> Self; } ``` ## focus\_index `focus_index` adds view to focus table, and sets its sort value in Tab order. ```rust use flor::view::builder::{EventBuilder, FocusIndexBuilder}; use flor_lys::label::label; let name = label("Name") .focus_index(0) .on_focus(|_, virtual_index| { println!("focus virtual index: {virtual_index}"); }); let confirm = label("Confirm").focus_index(1); ``` `0` is legal sort value, don't understand it as canceling focus. Smaller numbers are visited by Tab first. Same sort value can also work, final sorting will still include `ViewId` and virtual focus number; however using clear incrementing values for same group of views is easier to maintain. Not writing `focus_index` means not joining focus table. Such views won't be selected by Tab, also won't become view-level [`on_key_*`](/website/api/handler.md#keyboard-events) focus target. ## focus\_scope `focus_scope` provides a sort offset for current view and its subtree. It's suitable for splitting page into several areas, letting each area internally number from `0`. ```rust use flor::view::builder::FocusIndexBuilder; use flor::views; use flor_lys::div::div; use flor_lys::label::label; let toolbar = div(views![ label("Tool 1").focus_index(0), label("Tool 2").focus_index(1), ]) .focus_scope(100); let content = div(views![ label("Content 1").focus_index(0), label("Content 2").focus_index(1), ]) .focus_scope(200); ``` Final sort values above are: | View | Local Writing | Final Sort Value | | --------- | ------------- | ---------------- | | Tool 1 | `100 + 0` | `100` | | Tool 2 | `100 + 1` | `101` | | Content 1 | `200 + 0` | `200` | | Content 2 | `200 + 1` | `201` | `focus_scope` can nest, parent offset will continue accumulating to subtree. Note: builder's `focus_scope(u32)` only affects sorting, doesn't limit Tab's runtime range. For popup layer, modal and similar focus isolation scenarios, use `ViewId::push_focus_scope()` and `ViewId::pop_focus_scope()`, mechanism explanation see [Focus Mechanism](/website/guide/use/focus.md#runtime-scope). --- url: /website/guide/use/builder/event.md --- # Event Builder Event Builder is used to attach external event handlers to views. It's suitable for application-side code: you don't need to write a new view type, nor implement `View`, just chain bind events when creating views. Complete method list, handler types and dispatch status see [Handler API](/website/api/handler.md). This page only explains how to use. ## Basic Writing After importing `EventBuilder`, all values implementing `ViewIdentity` can chain bind events. Normal views, `ViewBox`, and values returned as `impl IntoView` all satisfy this condition. ```rust use flor::view::builder::EventBuilder; use flor_lys::label::label; let item = label("Save") .on_click(|view_id, key_state, mouse_position| { println!("{view_id} clicked at {mouse_position:?}"); }) .on_mouse_enter(|view_id| { println!("{view_id} entered"); }) .on_mouse_leave(|view_id| { println!("{view_id} left"); }); ``` Complete event callback parameters are not all the same. For example, the complete mouse click parameters are `ViewId`, `KeyState`, and `MousePosition`; mouse enter/leave use `ViewId`; keyboard events use `ViewId`, `KeyCode`, and modifier key state. Most handlers now also support omitting unused parameters. Specific signatures are listed in [EventBuilder API](/website/api/handler.md#eventbuilder) and [handler wrapper types](/website/api/handler.md#handler-wrapper-types). When examples show different parameter counts, read them as different conversion forms for the same handler. Event Builder converts the closure, function, or method item you pass into the corresponding handler. Handlers with extra event parameters usually support four forms: full arguments, no arguments, `ViewId` only, and without `ViewId`. Mechanism details are in [External Events: Mechanism Explanation](/website/guide/use/handler.md#mechanism-explanation) and [IntoEventHandler API](/website/api/handler.md#intoeventhandler). ## Not Only Can Pass Closure Event Builder's parameter is not "only can pass closure". Source code's method parameter is `impl IntoEventHandler`. Handler types keep the full-signature `From` conversion, and additionally support common forms such as no arguments, `ViewId` only, or without `ViewId`. So you can pass a closure, a normal function, an associated function, or a method item. ```rust use flor::base::platform::{KeyState, MousePosition}; use flor::view::builder::EventBuilder; use flor::view::ViewId; use flor_lys::label::label; fn save_clicked(view_id: ViewId, _key_state: KeyState, _pos: MousePosition) { println!("save from {view_id}"); } let save = label("Save") .on_click(save_clicked); ``` Associated function can also be directly passed: ```rust use flor::base::platform::{KeyState, MousePosition}; use flor::view::builder::EventBuilder; use flor::view::ViewId; use flor_lys::label::label; struct Actions; impl Actions { fn open(view_id: ViewId, _key_state: KeyState, _pos: MousePosition) { println!("open from {view_id}"); } } let open = label("Open") .on_click(Actions::open); ``` Generic functions or generic associated functions can also be passed, but Rust must see a function item whose type has already been determined: ```rust use flor::view::builder::EventBuilder; use flor::view::ViewId; use flor_lys::label::label; fn trace_click(view_id: ViewId) { println!("slot {SLOT}: {view_id}"); } let debug = label("Debug") .on_click(trace_click::<1>); ``` Methods with instance state are usually called through a closure that captures the instance; the value passed to the builder is still an `Fn` matching the target signature. Closures are suitable for writing a small amount of logic in place. Functions, method items, and generic functions are useful for reusing logic or splitting page code more clearly. As long as the final signature matches, all of these forms are valid. ## Understanding the `Args` Generic An event method looks like this: ```rust fn on_click( self, handler: impl IntoEventHandler, ) -> Self; ``` `Args` is not a runtime event parameter, and users do not need to write it when calling the method. It is only a marker for Rust trait inference: full arguments, no arguments, `ViewId` only, and without `ViewId` match different `IntoEventHandler` implementations. You only need to write this generic parameter when you accept an event handler in your own function and forward it: ```rust use flor::view::builder::EventBuilder; use flor::view::handler::{IntoEventHandler, OnClickHandler}; use flor::view::View; use flor_lys::button::button; fn action_button( text: &'static str, on_click: impl IntoEventHandler, ) -> impl View { button(text).on_click(on_click) } let save = action_button("Save", || { println!("save"); }); ``` ## Omitting Unused Parameters If you only do not use some parameters, you can still use `_` or `_name` to ignore them: ```rust use flor::view::builder::EventBuilder; use flor_lys::label::label; let item = label("Delete") .on_click(|view_id, _, _| { println!("delete {view_id}"); }); ``` Now you can also reduce the parameter count directly. Common supported forms include: ```rust use flor::view::builder::EventBuilder; use flor_lys::label::label; let save = label("Save").on_click(|| { println!("save"); }); let source = label("Source").on_click(|view_id| { println!("save from {view_id}"); }); let inspect = label("Inspect").on_click(|key_state, _pos| { println!("ctrl: {}", key_state.control_is_down); }); ``` The full-argument form is still available. Reduced parameters are useful when business logic does not care about the view ID, mouse position, or button state. ## Mouse Events Mouse events are usually used for click, press, release, move, enter and leave. ```rust use flor::view::builder::EventBuilder; use flor_lys::label::label; let item = label("Clickable") .on_button_down(|view_id, _, pos| { println!("down {view_id}: {pos:?}"); }) .on_button_up(|view_id, _, pos| { println!("up {view_id}: {pos:?}"); }) .on_click(|view_id, _, _| { println!("click {view_id}"); }); ``` Regular mouse hit events' `MousePosition` is target view's local coordinates. Complete mouse event list see [Handler API's current dispatch status](/website/api/handler.md#current-dispatch-status). ## Keyboard Events Keyboard events are dispatched to current focus view. To let view receive view-level `on_key_down` / `on_key_up`, first let it enter focus table; focus mechanism see [Focus Mechanism](/website/guide/use/focus.md). ```rust use flor::base::platform::{HandleResult, KeyCode}; use flor::view::builder::{EventBuilder, FocusIndexBuilder}; use flor_lys::label::label; let editor = label("Press Ctrl+S to save") .focus_index(0) .on_key_down(|code, is_alt, is_ctrl, is_shift| { if !is_alt && is_ctrl && !is_shift && code == KeyCode::S { println!("save"); HandleResult::Handled } else { HandleResult::Default } }); ``` Keyboard handler returns `HandleResult`. Returning `Handled` means this key is already handled; returning `Default` means using default handling. Keyboard handlers can use the full arguments or omit `ViewId`; the complete signature is in [Keyboard Events API](/website/api/handler.md#keyboard-events). ## Focus and Lifecycle Events `on_focus` / `on_blur` are triggered by focus manager, second parameter is virtual focus number. ```rust use flor::view::builder::{EventBuilder, FocusIndexBuilder}; use flor_lys::label::label; let input = label("Name") .focus_index(0) .on_focus(|virtual_index| { println!("focus index: {virtual_index}"); }) .on_blur(|| { println!("blur"); }); ``` `on_create` is suitable for doing one-time runtime initialization after view tree creation, for example pushing focus scope for popup layer: ```rust use flor::view::builder::EventBuilder; use flor_lys::label::label; let dialog = label("Popup Layer") .on_create(|view_id| { view_id.push_focus_scope(); }); ``` Lifecycle-type handler's aliases and parameters see [View and Lifecycle Events API](/website/api/handler.md#view-and-lifecycle-events). ## Feature-gated Events Drag-drop and theme change — these kinds of events are controlled by feature. After enabling the corresponding feature, related builder methods are available. Complete explanation see [Handler API](/website/api/handler.md). ```rust use flor::view::builder::EventBuilder; use flor_lys::label::label; #[cfg(feature = "drag-drop")] let drop_area = label("Drop Here") .on_drop(|_key_state, _mouse_position, data, _effect| { println!("drop: {data:?}"); }); ``` If you're unsure whether an event is already connected to dispatch path, first see [Current Dispatch Status](/website/api/handler.md#current-dispatch-status). Tutorial page won't repeatedly maintain each method's complete status table. --- url: /website/guide/use/builder/layout.md --- # Layout Builder Layout Builder is used to configure display mode, size, spacing, positioning, Flex/Grid and other layout properties when creating views. Basic writing is calling `.layout(|layout| { ... })` after view, continuously calling layout methods in closure, finally returning modified `layout`. ## Basic Writing After importing `LayoutBuilder`, all views can call `.layout(...)`. Also need to import `LayoutResolverExt`, so `layout` in closure can use `display`, `size`, `flex_grow` and other layout methods. Following example uses flex related types, assuming you already enabled `layout-flex` feature. ```rust use flor::taffy::{Dimension, Display, FlexDirection, LengthPercentage, Size}; use flor::view::builder::LayoutBuilder; use flor::view::resolver::{LayoutResolverExt, Unit}; use flor::views; use flor_lys::div::div; use flor_lys::label::label; let panel = div(views![label("Content")]) .layout(|layout| { layout .display(Display::Flex) .flex_direction(FlexDirection::Column) .size( Size { width: Dimension::Percent(1.0), height: Dimension::Auto, }, Size { width: Unit::Px, height: Unit::Px, }, ) .gap( Size { width: LengthPercentage::Length(8.0), height: LengthPercentage::Length(8.0), }, Size { width: Unit::Px, height: Unit::Px, }, ) }); ``` Closure must return modified `layout`. If closure captures external signal or variable, usually needs to write as `move |layout| { ... }`. Flor currently doesn't have direct `.width(100)`, `.height(40)`, `.x(10)`, `.y(20)` — these kinds of layout builder methods. Size, position, margin are all expressed through `size`, `inset`, `margin` and other layout parameters. ## Chain Writing and set Writing Most times directly chain calling is enough: ```rust layout .display(Display::Flex) .flex_grow(1.0) ``` Each layout method also has a same-named `set_` version, for example `display(...)` corresponds to `set_display(...)`, `flex_grow(...)` corresponds to `set_flex_grow(...)`. This group of `set_` methods is suitable for branch modifying same `layout` in `if`, `match`. These methods are automatically generated by Flor, so naming stays unified: layout item name written as snake\_case method name, `set_` version adds `set_` in front. You just use method names in following table. ```rust use flor::taffy::Display; use flor::view::builder::LayoutBuilder; use flor::view::resolver::LayoutResolverExt; use flor_lys::label::label; let item = label("Toggleable") .layout(move |mut layout| { if true { layout.set_display(Display::None); } else { layout.set_display(Display::Flex); } layout }); ``` > `set_` methods are mainly used when view development, parsing atomic class assignment ## State Layout Layout can also be set separately by view state. After calling `hover()`, `focus()`, `active()` and other state methods, subsequent layout settings will write to corresponding state; calling `normal()` or `base()` can switch back to normal state. | Method | Parameter | Purpose | | --------------------- | --------- | ------------------------------------------- | | `base()` / `normal()` | None | Switch back to normal state. | | `focus()` | None | Subsequent layout items for focus state. | | `hover()` | None | Subsequent layout items for hover state. | | `active()` | None | Subsequent layout items for active state. | | `disabled()` | None | Subsequent layout items for disabled state. | | `clear()` | None | Clear existing layout items. | ```rust use flor::taffy::{Display, FlexDirection}; use flor::view::builder::LayoutBuilder; use flor::view::resolver::LayoutResolverExt; use flor::views; use flor_lys::div::div; use flor_lys::label::label; let view = div(views![label("Content")]) .layout(|layout| { layout .display(Display::Flex) .flex_direction(FlexDirection::Column) .hover() .display(Display::None) }); ``` ## How to Read Unit Parameters Layout values mainly come from `flor::taffy`, unit types come from `flor::view::resolver::Unit`. `Unit` is length unit. It only participates in conversion when layout value is length, finally resolves to px and hands to layout system. | Unit | Meaning | Conversion Base | | ----------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Unit::Px` | Pixel. | No conversion, whatever value is passed is the px value. | | `Unit::Pt` | point, common font/typesetting unit. | Uses current window DPI conversion, formula is `1pt = dpi / 72 px`. Normal layout length uses window's vertical DPI; window reads DPI from platform when creating, updates when DPI changes. | | `Unit::Rem` | root em. | Uses current window's `WindowOption::rem_px`, default is `16.0`, so default `1rem = 16px`. | | `Unit::Vw` | viewport width. | `1vw = current window client area width / 100`. Updates after window resize. | | `Unit::Vh` | viewport height. | `1vh = current window client area height / 100`. Updates after window resize. | Whenever layout value and `Unit` appear together in method, rule is: layout value is responsible for expressing length, percent or auto, `Unit` only decides how to parse this number when value is length. For example `Dimension::Length(12.0)` paired with `Unit::Px` means 12px; `Dimension::Percent(0.5)` and `Dimension::Auto` don't depend on unit. ## Core Layout Parameters These methods are not affected by `layout-flex`, `layout-grid`, `layout-block` feature. | Method | Parameter | Description | | ------------------------------ | -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `display(value)` | `value: Display` | Set layout strategy, for example show, hide and flex/grid/block layout mode after enabling corresponding feature. | | `item_is_table(value)` | `value: bool` | Mark whether child item is processed as table item. Current table layout hasn't implemented, application code usually doesn't need to set it. | | `box_sizing(value)` | `value: BoxSizing` | Control whether `size`, `min_size`, `max_size` and other size styles act on content box or border box. | | `overflow(value)` | `value: Point` | Set overflow behavior in x/y two directions. Use `Point { x, y }` to separately pass horizontal and vertical `Overflow`. | | `scrollbar_width(width, unit)` | `width: f32`, `unit: Unit` | Reserve scrollbar space for `Overflow::Scroll` or `Overflow::Auto`. `unit` explains `width`'s unit. | | `position(value)` | `value: Position` | Set element positioning method, decides which positioning baseline `inset` uses. | | `inset(value, units)` | `value: Rect`, `units: Rect` | Set left/right/top/bottom offset. Each edge's `Length` uses corresponding `Unit` to parse, `Percent` and `Auto` don't depend on unit. | | `size(value, units)` | `value: Size`, `units: Size` | Set initial width and height. `value.width` and `value.height` are width and height separately, `units.width` and `units.height` are length units. | | `min_size(value, units)` | `value: Size`, `units: Size` | Set minimum width and height, parameter structure same as `size`. | | `max_size(value, units)` | `value: Size`, `units: Size` | Set maximum width and height, parameter structure same as `size`. | | `aspect_ratio(value)` | `value: f32` | Set preferred aspect ratio, calculation is width / height. | | `margin(value, units)` | `value: Rect`, `units: Rect` | Set outer margin. All four edges can use length, percent or auto. | | `padding(value, units)` | `value: Rect`, `units: Rect` | Set inner padding. All four edges can use length or percent, doesn't support auto. | | `border(value, units)` | `value: Rect`, `units: Rect` | Set layout-level border thickness. All four edges can use length or percent, doesn't support auto. | Example: ```rust use flor::taffy::{Dimension, LengthPercentageAuto, Rect, Size}; use flor::view::builder::LayoutBuilder; use flor::view::resolver::{LayoutResolverExt, Unit}; use flor::views; use flor_lys::div::div; use flor_lys::label::label; let view = div(views![label("Content")]) .layout(|layout| { layout .size( Size { width: Dimension::Length(320.0), height: Dimension::Auto, }, Size { width: Unit::Px, height: Unit::Px, }, ) .margin( Rect { left: LengthPercentageAuto::Length(12.0), right: LengthPercentageAuto::Length(12.0), top: LengthPercentageAuto::Length(8.0), bottom: LengthPercentageAuto::Length(8.0), }, Rect { left: Unit::Px, right: Unit::Px, top: Unit::Px, bottom: Unit::Px, }, ) }); ``` ## Flex and Grid Shared Parameters These methods are available after enabling either `layout-flex` or `layout-grid` feature. | Method | Parameter | Description | | ------------------------ | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `align_items(value)` | `value: AlignItems` | Set how child items align on cross/block axis. | | `align_self(value)` | `value: AlignSelf` | Set how current node itself aligns on cross/block axis; falls back to parent's `align_items` when not set. | | `align_content(value)` | `value: AlignContent` | Set how container content distributes on cross/block axis. | | `justify_content(value)` | `value: JustifyContent` | Set how container content distributes on main/inline axis. | | `gap(value, units)` | `value: Size`, `units: Size` | Set item spacing in two directions. Field names follow Taffy's `Size { width, height }`, corresponding length values separately use `units.width` and `units.height` to parse. | ## Block Parameters These methods require enabling `layout-block` feature. | Method | Parameter | Description | | ------------------- | ------------------ | ----------------------------------------------- | | `text_align(value)` | `value: TextAlign` | Set inline direction alignment in block layout. | ## Flex Parameters These methods require enabling `layout-flex` feature. | Method | Parameter | Description | | ------------------------- | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | `flex_direction(value)` | `value: FlexDirection` | Set flex main axis direction. | | `flex_wrap(value)` | `value: FlexWrap` | Set whether flex child items wrap. | | `flex_basis(value, unit)` | `value: Dimension`, `unit: Unit` | Set flex child item initial main axis size. `Dimension::Length` uses `unit` to parse, `Percent` and `Auto` don't depend on unit. | | `flex_grow(value)` | `value: f32` | Set remaining space growth ratio. Default value is 0.0, passing positive number means participating in growth. | | `flex_shrink(value)` | `value: f32` | Set shrink ratio when space insufficient. Default value is 1.0, passing positive number means participating in shrink. | Example: ```rust use flor::taffy::{AlignItems, Display, FlexDirection, JustifyContent}; use flor::view::builder::LayoutBuilder; use flor::view::resolver::LayoutResolverExt; use flor::views; use flor_lys::div::div; use flor_lys::label::label; let toolbar = div(views![label("Left"), label("Right")]) .layout(|layout| { layout .display(Display::Flex) .flex_direction(FlexDirection::Row) .align_items(AlignItems::Center) .justify_content(JustifyContent::SpaceBetween) }); ``` ## Grid Parameters These methods require enabling `layout-grid` feature. | Method | Parameter | Description | | ------------------------------ | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | `justify_items(value)` | `value: AlignItems` | Set how grid child items align on inline axis. | | `justify_self(value)` | `value: AlignSelf` | Set how current grid child item itself aligns on inline axis; falls back to parent's `justify_items` when not set. | | `grid_template_rows(value)` | `value: Vec<(TrackSizingFunction, Unit)>` | Set explicit grid row track size. Each track carries a `Unit`, used to parse length values in that track. | | `grid_template_columns(value)` | `value: Vec<(TrackSizingFunction, Unit)>` | Set explicit grid column track size. Each track carries a `Unit`, used to parse length values in that track. | | `grid_auto_rows(value)` | `value: Vec<(NonRepeatedTrackSizingFunction, Unit)>` | Set implicitly created grid row size. | | `grid_auto_columns(value)` | `value: Vec<(NonRepeatedTrackSizingFunction, Unit)>` | Set implicitly created grid column size. | | `grid_auto_flow(value)` | `value: GridAutoFlow` | Set automatic placement of grid item flow. | | `grid_row(value)` | `value: Line` | Set current child item's start/end position in grid row direction. | | `grid_column(value)` | `value: Line` | Set current child item's start/end position in grid column direction. | ## Availability Quick Reference | Method | feature | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | | `display`, `item_is_table`, `box_sizing`, `overflow`, `scrollbar_width`, `position`, `inset`, `size`, `min_size`, `max_size`, `aspect_ratio`, `margin`, `padding`, `border` | Always generated | | `align_items`, `align_self`, `align_content`, `justify_content`, `gap` | `layout-flex` or `layout-grid` | | `text_align` | `layout-block` | | `flex_direction`, `flex_wrap`, `flex_basis`, `flex_grow`, `flex_shrink` | `layout-flex` | | `justify_items`, `justify_self`, `grid_template_rows`, `grid_template_columns`, `grid_auto_rows`, `grid_auto_columns`, `grid_auto_flow`, `grid_row`, `grid_column` | `layout-grid` | --- url: /website/guide/use/builder/class.md --- # ## Atomic Class Atomic class is a style setting method similar to Tailwind CSS, quickly configuring view style and layout through space-separated class strings. ### Enable Feature Atomic class requires enabling `class` feature: ```toml [dependencies] flor = { version = "0.1.0", features = ["class"] } ``` ### Basic Usage ```rust use flor::view::builder::ClassBuilder; view.class("flex flex-col gap-2 p-4 bg-white rounded-lg shadow-md"); ``` ### Class Syntax Atomic classes are separated by spaces: ```text flex flex-col gap-2 p-4 ``` Each class can have a state prefix: ```text p-2 hover:p-4 focus:bg-blue-50 disabled:opacity-50 ``` State prefix rules: | Prefix | State | | ----------- | -------- | | No prefix | Normal | | `hover:` | Hover | | `focus:` | Focus | | `active:` | Active | | `disabled:` | Disabled | ### Layout Classes Layout classes require enabling corresponding features: ```toml [dependencies] flor = { version = "0.1.0", features = ["class", "layout-flex", "layout-grid", "layout-block"] } ``` Common layout classes: ```text flex flex-col flex-row gap-2 p-4 m-2 w-full h-auto ``` For complete layout class list, see [Layout Class Syntax](/website/api/layout-class.md). ### Style Classes Style classes are parsed by views themselves: ```text bg-white text-blue-600 rounded-lg shadow-md ``` Different views may support different style classes. ### Arbitrary Values Many classes support bracket arbitrary value syntax: ```text w-[320px] rounded-[10px] text-[#0f172a] p-[1rem] ``` ### Dynamic Classes Use closure to return dynamic class string: ```rust use flor::signal::create_signal; let is_active = create_signal(false); view.class(move || { if is_active.get() { "bg-blue-500 text-white" } else { "bg-gray-200 text-gray-800" } }); ``` ### Class Priority When multiple style sources set the same property, priority is: 1. Atomic class (highest) 2. Style builder 3. Default style (lowest) For complete class syntax, see [Class Syntax Basics](/website/api/class-syntax.md). --- url: /website/guide/use/builder/style.md --- # Style Builder Style Builder's entry is `.style(|style| { ... })`. It's used for manually writing view style's chain configuration, suitable when you want to directly call view-provided strongly-typed style methods, instead of writing style as class string. ```rust use flor::macros::color; use flor::view::builder::StyleBuilder; use flor_lys::label::{label, LabelStyleResolverExt}; let title = label("Title") .style(|style| { style .font_size(20.0) .text_color(color!("#0f172a")) }); ``` ## Basic Writing When using `.style(...)` usually need two imports: | Import | Purpose | | ------------------------------ | --------------------------------------------------- | | `StyleBuilder` | Let view have `.style(...)` method. | | View's own `*StyleResolverExt` | Let `style` in closure have specific chain methods. | For example `Label`'s style methods come from `LabelStyleResolverExt`: ```rust use flor::macros::color; use flor::view::builder::StyleBuilder; use flor_lys::label::{label, LabelStyleResolverExt}; let label = label("Status") .style(|style| style.text_color(color!("#2563eb"))); ``` `.style(...)`'s closure must return modified `style`. If want to continuously set multiple values, just chain call. ## Not All Views Have It `.style(...)` is for view to manually expose style chain capability. Whether supports `.style(...)`, what methods `style` in closure has, are all decided by specific view. For example `Label` can have `font_size(...)`, `text_color(...)` and similar methods; `Button` can have button variant, color, rounded corner and other methods. Specific methods should see that view's documentation in view library. ```rust use flor::macros::color; use flor::view::builder::StyleBuilder; use flor_lys::button::{button, ButtonStyleResolverExt, ButtonVariant}; let save = button("Save") .style(|style| { style .variant(ButtonVariant::Outline) .color(color!("#2563eb")) }); ``` If some view didn't implement style builder, the compiler will indicate that the method is not available when calling `.style(...)`. ## State Style Style builder can set style by view state. Common state methods include `normal()`, `hover()`, `focus()`, `active()`, `disabled()`. ```rust use flor::macros::color; use flor::view::builder::StyleBuilder; use flor_lys::label::{label, LabelStyleResolverExt}; let item = label("Option") .style(|style| { style .text_color(color!("#334155")) .hover() .text_color(color!("#2563eb")) }); ``` This code means: normal state uses dark gray, hover state uses blue. ## Relationship with class / layout `.style(...)` is view style's strongly-typed writing. It's suitable when you already know target view supports which style methods, and hope to get compile-time check. `.layout(...)` is layout's strongly-typed writing, responsible for display, size, spacing, Flex/Grid and other layout properties. `.class(...)` is quick development string writing. Flor's design philosophy is: class can describe layout and style's arbitrary value expression, so can use one string of class to simultaneously set layout and view style. Also because it's capability added for quick development, `class` is optional feature. All three can be used simultaneously; same configuration item uses later written value as standard. --- url: /website/guide/use/builder/disable.md --- # Disable Builder Disable Builder is used to switch view to `ControlState::Disabled`. It's application-side configuration view state builder, not a method custom view authors need to override in `View` trait. Currently exposed interface is: ```rust pub trait DisableBuilder { fn disable(self, f: F) -> Self where F: Fn() -> bool + 'static; } ``` ## Basic Writing After importing `DisableBuilder`, all views implementing `View` can call `.disable(...)`. ```rust use flor::view::builder::DisableBuilder; use flor_lys::button::button; let save = button("Save").disable(|| true); ``` Closure returns `true` means disabled, returns `false` means normal. Fixed disable can directly write `|| true`, but more common is reading state from signal. ## Reactive Disable `.disable(...)` will create a reactive updater. After signal read in closure changes, framework will recalculate disable state. ```rust use flor::signal::{create_signal, Read, Write}; use flor::view::builder::DisableBuilder; use flor_lys::button::button; let saving = create_signal(false); let save = button("Save") .disable(move || saving.get()); // Disable button when starting save. saving.set(true); ``` When need to combine multiple conditions, directly write business judgment in closure: ```rust use flor::signal::{create_signal, Read}; use flor::view::builder::DisableBuilder; use flor_lys::button::button; let saving = create_signal(false); let form_valid = create_signal(false); let submit = button("Submit") .disable(move || saving.get() || !form_valid.get()); ``` ## Mechanism Explanation `.disable(...)` internally will: 1. Read current view's `ViewId`. 2. Use `create_updater_with_id` to create an updater. 3. Write `ViewState.disable` when updater changes. 4. Attach effect id to current `ViewId`'s pending effect list, view creation process will activate it. Disable state participates in `ControlState` calculation, and has highest priority: `Disabled > Active > Focus > Hover > Normal`. View authors read `ControlState::Disabled` in `on_draw`, `on_measure` or event handling, decide visual and interaction behavior in disabled state. Disable doesn't automatically remove focus table entry, also doesn't automatically cancel current focus. When need runtime change focus capability, use [ViewId](/website/guide/control/view-id.md)'s focus API. --- url: /website/guide/use/builder/z-index.md --- # Z-Index Builder Z-Index Builder is used to set `z_index` for views. It only affects sibling node ordering under same parent view, not global stacking context. ## Basic Writing After importing `ZIndexBuilder`, all views implementing `View` can call `.z_index(...)`. ```rust use flor::view::builder::ZIndexBuilder; use flor::views; use flor_lys::div::div; use flor_lys::label::label; let panel = div(views![ label("Normal Content").z_index(|| 0), label("Floating Content").z_index(|| 10), ]); ``` Current signature is: ```rust pub trait ZIndexBuilder { fn z_index(self, z_index: impl Fn() -> i32 + 'static) -> Self; } ``` So fixed value also needs to write as closure returning `i32`, for example `.z_index(|| 10)`. If layer level depends on signal, read signal in closure: ```rust use flor::signal::{create_signal, Read}; use flor::view::builder::ZIndexBuilder; use flor_lys::label::label; let popup_open = create_signal(false); let popup = label("Menu") .z_index(move || if popup_open.get() { 100 } else { 0 }); ``` ## Relationship with class After enabling `class` feature, `z-*` in layout class will be parsed into same set of runtime layer level values. ```rust use flor::view::builder::ClassBuilder; use flor_lys::label::label; let popup = label("Menu").class("z-100"); ``` `z-auto` will write `0`, `z-10` will write `10`. `z-*` is special item in layout class: it won't enter Taffy layout style, but directly update `ViewId`'s layer level value. If you already use `.z_index(...)` to dynamically control layer level, don't use `z-*` on same view to express another fixed layer level, to avoid subsequent updates overriding each other. ## Suitable Scenarios Common use is separating temporary content and normal content in same container: | Scenario | Suggestion | | ---------------- | -------------------------------------------------------------- | | Popup menu | Give menu value higher than trigger button and normal content. | | Floating toolbar | Give toolbar stable high layer level value. | | Drag preview | During drag switch preview view to higher layer level. | | Normal list item | Keep default `0`, only raise when need floating or dragging. | Layer level only compares between sibling nodes. Two views with different parent views cannot achieve global covering solely through `z_index`; you need to put them into the same sortable parent container, or adjust the view tree structure. ## Mechanism Explanation `.z_index(...)` will create a reactive updater. During initialization will immediately calculate closure result once, and call current view's `ViewId::set_z_index(value)`; afterwards when signal depended by closure updates, will write new layer level value again. `ViewId::set_z_index` will: 1. Write value to `VIEW_STORAGE.view_z_index`. 2. Find current view's parent view. 3. Re-sort parent view's child list according to sibling views' respective `z_index()`. Views that haven't set layer level, `ViewId::z_index()` returns `0`. --- url: /website/guide/use/builder/transform.md --- # Transform Builder Transform Builder is used to set declarative `Transform2D` for views. It affects view itself and its child views' drawing and hit testing, but doesn't change position and size calculated by Taffy layout. ## Basic Writing After importing `TransformBuilder`, all views implementing `View` can call `.transform(...)`. ```rust use flor::types::Transform2D; use flor::view::builder::TransformBuilder; use flor_lys::label::label; let title = label("Flor") .transform(Transform2D::rotation_degrees(-3.0)); ``` Current signature is: ```rust pub trait TransformBuilder { fn transform(self, transform: impl TransformProp) -> Self; } ``` `TransformProp` supports two writing styles: | Writing | Example | Suitable Scenario | | | | ------------------------------- | ------------------------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------- | ----------------------------------- | | Fixed `Transform2D` | `.transform(Transform2D::translation(12.0, 0.0))` | Visual offset, rotation, scale that don't change after creation. | | | | Closure returning `Transform2D` | \`.transform(move | | Transform2D::rotation\_degrees(angle.get()))\` | Dynamic transform driven by signal. | ## Dynamic Transform Closure version will recalculate transform when dependencies update. ```rust use flor::signal::{create_signal, Read}; use flor::types::Transform2D; use flor::view::builder::TransformBuilder; use flor_lys::label::label; let angle = create_signal(0.0f32); let spinner = label("loading") .transform(move || Transform2D::rotate_at_degrees(angle.get(), 40.0, 12.0)); ``` `rotate_at_degrees(degrees, cx, cy)` and `scale_at(sx, sy, cx, cy)` can express effect similar to `transform-origin`. Flor currently doesn't have separate `.origin(...)` builder. ## Transform2D Common Constructors | API | Purpose | | ------------------------------------------------- | ----------------------------------------- | | `Transform2D::IDENTITY` | Identity matrix. | | `Transform2D::new(m11, m12, m21, m22, dx, dy)` | Directly build matrix. | | `Transform2D::translation(x, y)` | Translation. | | `Transform2D::scale(sx, sy)` | Scale. | | `Transform2D::rotation(radians)` | Rotate by radians. | | `Transform2D::rotation_degrees(degrees)` | Rotate by degrees. | | `Transform2D::skew(x_rad, y_rad)` | Skew by radians. | | `Transform2D::skew_degrees(x_deg, y_deg)` | Skew by degrees. | | `Transform2D::rotate_at(radians, cx, cy)` | Rotate around specified point. | | `Transform2D::rotate_at_degrees(degrees, cx, cy)` | Rotate around specified point by degrees. | | `Transform2D::scale_at(sx, sy, cx, cy)` | Scale around specified point. | Chain combination uses `then_*` methods: ```rust use flor::types::Transform2D; let transform = Transform2D::translation(16.0, 0.0) .then_rotate_degrees(8.0) .then_scale(1.05, 1.05); ``` Combination order is first apply current matrix, then apply new matrix. That is, above example will first translate, then rotate, finally scale. ## Not CSS transform String `.transform(...)` receives `Transform2D`, not string. Currently doesn't have these builder methods: ```rust // Currently doesn't exist .transform("rotate(45deg)") .rotate(45.0) .scale(1.2) .translate(10.0, 20.0) .skew(10.0, 0.0) .origin("center") ``` Corresponding writing should change to: ```rust use flor::types::Transform2D; .transform(Transform2D::rotation_degrees(45.0)) .transform(Transform2D::scale(1.2, 1.2)) .transform(Transform2D::translation(10.0, 20.0)) .transform(Transform2D::skew_degrees(10.0, 0.0)) .transform(Transform2D::rotate_at_degrees(45.0, 50.0, 20.0)) ``` ## Mechanism Explanation `.transform(...)` will create a reactive updater. During initialization will immediately calculate `Transform2D` once, and call current view's `ViewId::set_transform(transform)`; afterwards when signal depended by closure updates, will write new transform again and request redraw. Runtime transform is stored in `VIEW_STORAGE.transform`. Layout refresh phase will calculate accumulated transform, drawing phase will `push_transform`, hit testing will use inverse matrix to convert window coordinates back to view local coordinates. If matrix is non-invertible, for example scale to `0.0`, hit testing cannot convert window coordinates back to local coordinates, this view branch will be skipped. In animation don't scale interactive views to non-invertible matrix. --- url: /website/guide/use/resolver-layer.md --- # ## Resolver Layer Mechanism Resolver Layer is Flor's mechanism for organizing view configuration. It allows view's layout and style to be written in segments, repeatedly, and overridden on demand, rather than requiring you to assemble all configuration into one block at once. In usage code, `.class(...)`, `.layout(...)`, `.style(...)` all use this Layer mechanism. Each call to these builders can be understood as appending one layer configuration to view. Layer's merge rule is simple: | Situation | Result | | ------------------------------------------- | -------------------------------------------------------------------------------- | | Different layers wrote different properties | These properties will merge and be retained. | | Different layers wrote same property | Regardless of value same or different, the later written layer takes precedence. | That is, you can split class, layout, style to write multiple times, can also mix together. Later calls won't clear previous whole layer configuration; it will only override properties it rewrote. ## Mixed Calling For example first use class to write common layout, then use layout builder to precisely override one property: ```rust use flor::taffy::Display; use flor::view::builder::{ClassBuilder, LayoutBuilder}; use flor::view::resolver::LayoutResolverExt; use flor::views; use flor_lys::div::div; use flor_lys::label::label; let panel = div(views![label("Content")]) .class("hidden p-4") .layout(|layout| layout.display(Display::Flex)) .class("gap-3"); ``` This code can be understood as three layers: | Layer | Written Content | | | | ---------------------- | ---------------------------- | -------------------------------- | ---------------------- | | `.class("hidden p-4")` | Write `display` and padding. | | | | \`.layout( | layout | layout.display(Display::Flex))\` | Again write `display`. | | `.class("gap-3")` | Write gap. | | | Final result is: `display` uses later written `Display::Flex`, padding comes from `p-4`, gap comes from `gap-3`. ## Same Property Override When same property is written multiple times, last written one takes effect. Class is like this: ```rust use flor::view::builder::ClassBuilder; use flor_lys::label::label; let item = label("Item") .class("p-2") .class("p-4"); ``` Final padding is `p-4`. Layout builder is also like this: ```rust use flor::taffy::Display; use flor::view::builder::LayoutBuilder; use flor::view::resolver::LayoutResolverExt; use flor_lys::label::label; let item = label("Item") .layout(|layout| layout.display(Display::None)) .layout(|layout| layout.display(Display::Flex)); ``` Final `display` is `Display::Flex`. Style builder is also same rule: ```rust use flor::view::builder::{ClassBuilder, StyleBuilder}; use flor_lys::label::{label, LabelStyleResolverExt}; let title = label("Title") .class("text-lg") .style(|style| style.font_size(20.0)); ``` If `text-lg` and `font_size(20.0)` both are setting font size, then final font size uses later written `20.0`. ## Different Property Merge Later calls only override properties they rewrote. Properties not rewritten will continue to be retained: ```rust use flor::macros::color; use flor::view::builder::{ClassBuilder, StyleBuilder}; use flor_lys::label::{label, LabelStyleResolverExt}; let title = label("Title") .class("text-lg font-bold") .style(|style| style.text_color(color!("#2563eb"))); ``` Here `.class(...)` wrote font size and font weight, `.style(...)` only wrote text color, so final will simultaneously retain font size, font weight and text color. ## State Also Merges by Layer State prefix is also same rule. Normal state, hover, focus, active, disabled will separately merge their own properties: ```rust use flor::view::builder::ClassBuilder; use flor_lys::label::label; let item = label("Save") .class("text-slate-700 hover:text-blue-600 hover:font-bold") .class("hover:text-red-600"); ``` Final normal state uses `text-slate-700`. In hover state, `font-bold` will be retained; text color is overridden by the later `hover:text-red-600`. ## What's the Use Layer mechanism's purpose is not to make writing more complex, but to let you split configuration to suitable positions. You can first write a set of base configuration, then later re-specify certain properties: ```rust let item = label("Item") .class("p-2 text-slate-700") .class("p-4"); ``` Here later `p-4` only overrides padding, `text-slate-700` is still retained. You can also hand a certain property individually to reactive signal control, rather than stuffing the entire class string or entire style group into `if else` concatenation: ```rust use flor::signal::{create_signal, Read}; use flor::taffy::Display; use flor::view::builder::{ClassBuilder, LayoutBuilder}; use flor::view::resolver::LayoutResolverExt; use flor_lys::label::label; let open = create_signal(true); let panel = label("Details") .class("p-4 text-slate-700") .layout(move |layout| { if open.get() { layout.display(Display::Flex) } else { layout.display(Display::None) } }); ``` This way, static configuration still writes in `.class(...)`, only the truly changing `display` goes into reactive `.layout(...)`. If putting all this content into one dynamic string, there will usually be repeated classes, complex branches and hard-to-maintain text concatenation. Layer mechanism also supports higher-level view encapsulation: handlers can be passed as parameters, chain-configured views can be passed on as return values. Specifically how to wrap base views into composite views or business modules, see [Framework DSL](/website/guide/use/framework-dsl.md). Maintaining end-user's usage experience is one of the framework's core goals. Layer mechanism is to let application code clearly express "which configurations are base values, which configurations override, which configurations respond to changes", thereby reducing code noise created for implementing interactive states. Just remember one rule: different properties will merge, same property always uses later write overrides earlier write. --- url: /website/guide/use/framework-dsl.md --- # Framework DSL Flor's interface code can be written as a set of normal Rust DSL: functions are responsible for composing views, parameters are responsible for passing in properties, signals and handlers, and the return value is an already configured view. This has similar usage feeling to JSX: page is not a large string template, but a set of view functions that can be split, reused, passed parameters. Difference is Flor doesn't need extra template language, composition itself is Rust's function calls and chain methods. If not yet familiar with how `.class(...)`、`.layout(...)`、`.style(...)` multiple calls merge, first see [Resolver Layer Mechanism](/website/guide/use/resolver-layer.md). ## How to Express View Tree Flor's most common child view list writing is `views![...]`: ```rust use flor::views; use flor_lys::button::button; use flor_lys::div::div; use flor_lys::label::label; let panel = div(views![ label("Account Login"), button("Send Code"), button("Login"), ]); ``` `views![...]` accepts a set of view expressions, and converts them into view list. It's suitable for expanding child nodes at view tree site, especially when directly writing multiple child views in container. If only need to convert single view into generic view object, can use `view!(...)`: ```rust use flor_lys::label::label; let title = flor::view!(label("Account Login")); ``` Macro is not a required entry point. It just makes writing a view tree in place more convenient. When you need to pass a single view according to some parameter type, directly calling `into_view()`, or passing the value to an API that accepts `IntoViewIter`, is often clearer. ## IntoView and IntoViewIter `IntoView` / `IntoViewIter` are used to convert views into Flor's more generic view forms. They are not only used for method return values, but also for view parameters, module parameters, container children, and other places where types need to line up. In the current public API, single-view conversion is `IntoView`, while child-view sequence conversion is `IntoViewIter`. A single view also implements `IntoViewIter`, so passing only one child no longer requires manually converting it into a list first. When encapsulating functions, a single view can return `impl View` or `impl IntoView`; multiple views can return `impl IntoViewIter`: ```rust use flor::view::builder::ClassBuilder; use flor::view::{IntoView, IntoViewIter, View}; use flor::views; use flor_lys::button::button; use flor_lys::label::label; fn title_view() -> impl View { label("Account Login") .class("text-xl font-bold") } fn title_child() -> impl IntoView { label("Account Login") .class("text-xl font-bold") } fn action_buttons() -> impl IntoViewIter { views![ button("Cancel").class("w-full"), button("Confirm").class("w-full"), ] } ``` `title_view()` returns a normal view, caller can still continue chaining `.class(...)`, `.layout(...)` etc. builders. `title_child()` more emphasizes "this return value will be taken as single child view passed in". `action_buttons()` then indicates this function returns a set of child views. For example, containers such as `div(...)` accept `impl IntoViewIter`. When there is only one child view, you can pass that view directly: ```rust use flor::view::builder::ClassBuilder; use flor_lys::div::div; use flor_lys::label::label; let empty = div( label("No Data") .class("text-sm") ); ``` If a view or module parameter wants one child view, mark it as `impl IntoView`; if it wants a set of child views or should accept either a single child or a list, mark it as `impl IntoViewIter`: ```rust use flor::view::builder::ClassBuilder; use flor::view::{IntoView, IntoViewIter, View}; use flor::views; use flor_lys::button::button; use flor_lys::div::div; use flor_lys::label::label; fn dialog(title: impl IntoView, actions: impl IntoViewIter) -> impl View { div(views![ div(title), div(actions), ]) } let confirm = dialog( label("Delete File").class("text-lg font-bold"), button("Confirm").class("w-full"), ); ``` Key point of writing this way is to let parameters and return values express clearly: here needs normal view, generic view object, or a set of child views that can be passed on to a container. Macro and conversion capabilities can be used together. `views![...]` is suitable for writing multiple child nodes in place, and `into_view()` is suitable for aligning an existing view expression to a generic view object: | Writing | Usage | | ------------------------------------- | --------------------------------------------------------------------------------------------------------------- | | `fn f() -> impl View` | Encapsulate a complete view, and retain continue chain configuration capability. | | `fn f() -> impl IntoView` | Encapsulate a single view prepared to be passed as a child, while still allowing common builders to be chained. | | `fn f() -> impl IntoViewIter` | Encapsulate a set of child views prepared for a container; a single view also satisfies this bound. | | `child.into_view()` | Convert a single view into a generic view object. | | `child` as an `IntoViewIter` argument | Treat a single view as a child sequence containing only that view. | | `views![a, b, c]` | Write child view list at view tree site. | | `view!(child)` | Convert a single view into a generic view object when the macro form is useful. | ## Continue Chaining After Return `IntoView` inherits `ViewIdentity`, and common builders now use `ViewIdentity` to read view identity. This means that when a function returns `impl IntoView`, the caller can still continue chaining `.class(...)`, `.layout(...)`, `.on_click(...)`, `.focus_index(...)`, and similar builder methods: ```rust use flor::view::builder::{ClassBuilder, EventBuilder}; use flor::view::IntoView; use flor_lys::button::button; fn toolbar_button(text: &'static str) -> impl IntoView { button(text).class("px-3 py-1") } let save = toolbar_button("Save") .class("font-bold") .on_click(|| { println!("save"); }); ``` This kind of encapsulation no longer needs to force a return type of `impl View` just to keep chaining available. When the function means "return a value that can be passed as a child view", `impl IntoView` is more direct. ## Why Need Macro Flor provides `views![...]`, is to make writing child view list on site more handy, meanwhile avoid Rust tuple's length problem in UI child node expression. Floem and similar frameworks can use tuple to express child views, for example `(a, b, c)`. This writing is very natural, but in stable Rust, framework usually needs to separately implement trait for different length tuples. That is, it's not infinite length: how many child nodes supported, depends on how many groups of tuple implementations framework wrote. `views![...]` will directly expand into view list, doesn't need to separately implement trait for each child node quantity, also doesn't easily let user hit tuple length limit when child view quantity increases. Later adding tuple support is feasible, practice usually is to supplement implementation for common length tuples, let user can write `(a, b)` or `(a, b, c)` in simple scenarios. But as long as stable Rust doesn't have true variadic generics, tuple support itself will still have a framework defined limit. So even if later supports tuple, `views![...]` is also more suitable as stable writing when no fixed child node quantity. ## Method Composing Views Minimum encapsulation is writing a view's layout, style and events into function, then returning it: ```rust use flor::view::builder::{ClassBuilder, EventBuilder, StringProp}; use flor::view::handler::IntoEventHandler; use flor::view::handler::OnClickHandler; use flor::view::View; use flor_lys::button::button; fn primary_button( text: T, on_click: impl IntoEventHandler, ) -> impl View where T: StringProp, { button(text) .class("w-full") .on_click(on_click) } ``` Here `Args` is the inference marker for the event handler's parameter form. With this signature, callers can pass a full click handler, a no-argument handler, a `ViewId`-only handler, or a handler without `ViewId`. If this used the old `impl Into` form, only the full-signature `From` conversion would remain, and simplified parameter forms would be lost. Caller only needs to care about this view's exposed parameters: ```rust let save = primary_button("Save", |view_id| { println!("save from {view_id}"); }); ``` Here `primary_button` is like a small view. How it internally writes class, how binds events, for caller are all collected into function boundary. ## Composing into Module Multiple views can also be encapsulated into a composite view or business module. For example login module can encapsulate title, code button, login button and layout together: ```rust use flor::view::builder::{ClassBuilder, EventBuilder}; use flor::view::handler::{IntoEventHandler, OnClickHandler}; use flor::view::View; use flor::views; use flor_lys::button::button; use flor_lys::div::div; use flor_lys::label::label; fn login_module( on_login: impl IntoEventHandler, on_send_code: impl IntoEventHandler, ) -> impl View { div(views![ label("Account Login").class("text-xl font-bold"), button("Send Code") .class("w-full") .on_click(on_send_code), button("Login") .class("w-full") .on_click(on_login), ]) .class("flex flex-col gap-3 p-4") } ``` Use a separate `Args` generic for each handler parameter, such as `LoginArgs` and `SendCodeArgs` here. They have no business meaning; they only let each handler infer its own parameter form independently. This function returns not template fragment, but a complete view. Caller can pass handler as parameter: ```rust let login = login_module( || { println!("login"); }, || { println!("send code"); }, ); ``` If module needs reactive state, can also pass signal as parameter, let module internally only control properties it cares about. For example whether login button is available, code countdown, whether panel is displayed, can all be independently split into their own parameters, instead of writing entire layout and style as complex string concatenation. ## Continue Configuring After Return View returned by function can still continue using builder. That is, module internally can write default layout and style, caller can externally continue supplementing or overriding: ```rust use flor::view::builder::ClassBuilder; let login = login_module(on_login, on_send_code) .class("w-[360px]"); ``` Here `login_module` internally already wrote `flex flex-col gap-3 p-4`, caller only supplemented width. Because [Resolver Layer Mechanism](/website/guide/use/resolver-layer.md) will merge configuration by layer, so external supplement won't clear module internal configuration; if externally rewrote same property, then later written is standard. ## Why Suitable for AI Flor's this set of DSL walks common functional UI expression path: methods compose views, properties, signals and handlers pass as variables, return value continues participating in composition. Large amount of AI models are already familiar with React, JSX, functional components and declarative UI organization patterns, so they more easily migrate to Flor's writing: recognize view tree, complete properties, split modules, adjust layout and event binding. This is also reason mentioned in [AI](/website/guide/ai.md) page: Flor didn't design this way for AI, but this DSL API naturally walked onto expression path AI already trained on, and is also more easily understood. --- url: /website/guide/use/focus.md --- # Focus Mechanism Flor's focus is an explicit mechanism. Views won't automatically enter the focus system just because they can be clicked, can display text, or bound keyboard events. A view only enters the focus table when explicitly registered, then it will be accessed by Tab, will become the dispatch target for view-level [`on_key_*`](/website/api/handler.md#keyboard-events), and will have `on_focus` and `on_blur` triggered by the focus manager. First look at a minimal example: ```rust use flor::view::builder::{EventBuilder, FocusIndexBuilder}; use flor_lys::label::label; let name = label("Name") // Add to focus table. 0 is sort value, means this is first accessed focus item in current scope. .focus_index(0) .on_focus(|view_id, virtual_index| { println!("focus {view_id}, virtual index: {virtual_index}"); }); ``` Views without `focus_index` won't enter the focus table. They won't be selected by Tab; when clicking them, the framework can't set focus to that view; their view-level `on_focus` / `on_blur` won't be triggered by the focus manager; their view-level [`on_key_*`](/website/api/handler.md#keyboard-events) won't be triggered because "this view got focus". For specific builder writing see [Focus Builder](/website/guide/use/builder/focus.md). For API to manipulate focus at runtime through `ViewId` see [ViewId](/website/guide/control/view-id.md). ## Keyboard Events Depend on Focus Keyboard events are dispatched to the currently focused view. To let a view handle view-level keyboard events, it needs to enter the focus table and become the current focus. ```rust use flor::base::platform::{HandleResult, KeyCode}; use flor::view::builder::{EventBuilder, FocusIndexBuilder}; use flor_lys::label::label; let editor = label("Press Ctrl+S to save") .focus_index(0) .on_key_down(|_, code, is_alt, is_ctrl, is_shift| { if !is_alt && is_ctrl && !is_shift && code == KeyCode::S { println!("save"); HandleResult::Handled } else { HandleResult::Default } }); ``` When there's no current focus view, `key_down` / `key_up` will go through window-level processing; a normal view that didn't get focus won't receive its own view-level [`on_key_*`](/website/api/handler.md#keyboard-events). Text input and IME input also depend on the current focus view. ## Click and Focus Mouse events go through hit test, not the same dispatch path as the focus table. So views without `focus_index` can still receive `on_click`, `on_button_down`, `on_mouse_move` and similar mouse events. When click ends, the framework will try to set focus to the clicked view. This action is still limited by the focus table: if the target view doesn't have `focus_index`, the focus manager can't find a corresponding entry, and the view won't become the current focus. ```rust use flor::view::builder::{EventBuilder, FocusIndexBuilder}; use flor_lys::label::label; let item = label("Get focus after click") .focus_index(0) .on_click(|| { println!("clicked"); }); ``` ## Virtual Focus Entries in focus table are not just `ViewId`, but: ```text (focus_index, ViewId, virtual_index) ``` Most views only have one virtual focus point, number is `0`. So `on_focus` and `on_blur`'s second parameter common value is `0`. ```rust use flor::view::builder::{EventBuilder, FocusIndexBuilder}; use flor_lys::label::label; let view = label("Single focus view") .focus_index(0) .on_focus(|_, virtual_index| { assert_eq!(virtual_index, 0); }); ``` Compound views can expose multiple virtual focus points. For example, an editor view can design text area, completion panel, line number area as different focus positions within the same view. Terminal user only needs to read `virtual_index`, doesn't need to apply for virtual focus count; applying for count belongs to the view developer's `View::on_focus_count` capability. ## Sort Segmentation Complex pages often need to split focus order into several areas. Flor provides `focus_scope(u32)` as sort offset: it will affect final sort value of `focus_index` in the current view subtree. ```rust use flor::view::builder::FocusIndexBuilder; use flor::views; use flor_lys::div::div; use flor_lys::label::label; let toolbar = div(views![ label("Tool 1").focus_index(0), label("Tool 2").focus_index(1), ]) .focus_scope(100); let content = div(views![ label("Content 1").focus_index(0), label("Content 2").focus_index(1), ]) .focus_scope(200); ``` Final sort values for this set of focus are: | View | Local Writing | Final Sort Value | | --------- | ------------- | ---------------- | | Tool 1 | `100 + 0` | `100` | | Tool 2 | `100 + 1` | `101` | | Content 1 | `200 + 0` | `200` | | Content 2 | `200 + 1` | `201` | `focus_scope(u32)` is sort segmentation, not focus isolation in popup. It won't let Tab only stay in this area. ## Runtime Scope Modal, Popup, Sidebar and similar interfaces need another capability: when opened, let Tab only cycle inside popup layer; after closed, restore previous focus position. Flor uses runtime focus scope to handle this scenario. When popup layer opens, push popup layer root view into focus scope; when closing, pop this scope. ```rust use flor::view::builder::{EventBuilder, FocusIndexBuilder}; use flor::views; use flor_lys::div::div; use flor_lys::label::label; let dialog = div(views![ label("Name").focus_index(0), label("Confirm").focus_index(1), ]) .on_create(|dialog_root_id| { dialog_root_id.push_focus_scope(); }); ``` When closing popup layer call `pop_focus_scope`. For complete `ViewId` method description see [ViewId](/website/guide/control/view-id.md). Runtime scope only filters views already in focus table. If no view inside popup layer sets `focus_index`, Tab has no target in this scope. ## Mechanism Explanation When window creates, Flor initializes focus table: 1. Traverse view tree starting from window root node. 2. When encountering `focus_scope(u32)`, add it to current cumulative offset. 3. When encountering `focus_index(u32)`, use "cumulative offset + local index" to generate sort value. 4. Query this view's virtual focus count, default is `1`. 5. Generate one `(focus_index, ViewId, virtual_index)` for each virtual focus. 6. Sort all entries and hand to `FocusManager`. After Tab key enters event bus, `FocusManager::next()` will move backward in current focus table; Shift+Tab will call `prev()` to move forward. Each focus switch, old entry triggers `blur`, new entry triggers `focus`. If there's runtime focus scope, `FocusManager` will first filter focus table to subtree of stack top scope root view, then execute next/prev. Keyboard events query current focus's `ViewId`, then dispatch to this view. When no current focus, view-level keyboard handler won't be called. --- url: /website/guide/features/overview.md --- # Framework Capabilities Framework capabilities section specifically introduces `flor`'s optional capabilities. They are mostly enabled through Cargo features, and after enabling they expose the corresponding API, event builder or platform behavior. If you just want to look up all feature names, first see [Quick Overview](/website/guide/startup.md#framework-features). This section explains platform capabilities in detail: what you can do after enabling, where the entry point is, and which tutorials/APIs are related. ```toml [dependencies] flor = { version = "0.1.0", features = ["direct2d", "clipboard", "drag-drop"] } ``` ## Platform Capabilities | Feature | Capability | Entry | | ------------------------------ | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | | `clipboard` | Read/write system clipboard. | [Clipboard](/website/guide/features/clipboard.md) | | `drag-drop` | Receive system drag-drop enter, hover, leave, release events. | [Drag-Drop](/website/guide/features/drag-drop.md) | | `tray` | Add, update, delete system tray icon, and receive tray mouse events. | [System Tray](/website/guide/features/tray.md) | | `theme-change` | Expose system theme change related event entry. | [Theme Change](/website/guide/features/theme-change.md) | | `monitor` | Query monitor list, work area, DPI, scaling etc. info. | [Monitor Information](/website/guide/features/monitor.md) | | `hi-dpi` | Enable process-level DPI awareness, paired with DPI change event to update layout units. | [High DPI](/website/guide/features/hi-dpi.md) | | `cross-thread-window-creation` | Allow requesting window creation outside event loop thread. | [Cross-thread Window Creation](/website/guide/features/cross-thread-window-creation.md) | Event-type capabilities usually also need to be paired with [Event Builder](/website/guide/use/builder/event.md) use; complete handler signature and current dispatch status see [Handler API](/website/api/handler.md). --- url: /website/guide/features/clipboard.md --- # Clipboard `clipboard` feature is used to enable system clipboard read/write capability. It is suitable for implementing copy, paste, import/export of temporary text and similar functionality. ```toml [dependencies] flor = { version = "0.1.0", features = ["direct2d", "clipboard"] } ``` ## Basic Usage Clipboard entry is in platform layer. When using import `ClipboardApi` trait, then create `Clipboard`. ```rust use flor::base::platform::ClipboardApi; use flor::platform::clipboard::Clipboard; let clipboard = Clipboard(None); clipboard.set_clipboard_text("hello flor".to_string())?; let text = clipboard.get_clipboard_text()?; println!("{text}"); ``` `Clipboard(None)` means not binding specific window. When you need to associate clipboard operation with a given window, can pass `Some(window_id)`. ```rust use flor::base::platform::ClipboardApi; use flor::platform::clipboard::Clipboard; let clipboard = Clipboard(Some(window_id)); clipboard.set_clipboard_text("from this window".to_string())?; ``` ## Supported Content `ClipboardApi` provides two kinds of entries: | Method | Purpose | | -------------------------------------------------- | ----------------------------------------- | | `set_clipboard_text(...)` / `get_clipboard_text()` | Read/write text. | | `set_clipboard(...)` / `get_clipboard(...)` | Read/write raw bytes by clipboard format. | | `register_format(...)` | Register custom clipboard format. | | `set_clipboard_muti_type(...)` | Write multiple formats at once. | Common clipboard formats are represented by `ClipboardType`, including `Text`, `Image`, `Rtf`, `Html`, `Files`. If want to exchange complex data with other applications, can first register custom format, then read/write corresponding bytes. --- url: /website/guide/features/drag-drop.md --- # Drag-Drop `drag-drop` feature is used to enable system drag-drop events. After enabling, views can receive drag enter, hover, leave and release through event builder. ```toml [dependencies] flor = { version = "0.1.0", features = ["direct2d", "drag-drop"] } ``` ## Basic Usage Drag-drop is event capability, entry in [Event Builder](/website/guide/use/builder/event.md). ```rust use flor::base::platform::DropEffect; use flor::view::builder::EventBuilder; use flor_lys::label::label; let drop_area = label("Drag Here") .on_drag_enter(|_view_id, _key_state, _pos, formats, effect| { if formats.iter().any(|format| matches!(format, flor::base::platform::DragFormat::Files(_))) { *effect = DropEffect::Copy; } }) .on_drop(|_view_id, _key_state, _pos, data, effect| { println!("{data:?}"); *effect = DropEffect::Copy; }); ``` Events: | Method | Purpose | | -------------------- | ---------------------------------- | | `on_drag_enter(...)` | Drag content enters view hit area. | | `on_drag_over(...)` | Drag content moves on view. | | `on_drag_leave(...)` | Drag content leaves view. | | `on_drop(...)` | User releases drag content. | Complete signature and current dispatch status see [Handler API's Drag-Drop Events](/website/api/handler.md#drag-drop-events). ## Related Types Drag-drop events will directly encounter these types: | Type | Purpose | | --------------- | --------------------------------------------------------------------------------------------------------------------- | | `DropEffect` | View's drag-drop feedback to system, indicating whether the current state is reject, copy, move, link, etc. | | `DragFormat` | Data formats available in the drag enter and hover phase, used to determine what type of content is being dragged in. | | `DragData` | Real data obtained in the `on_drop(...)` phase. | | `KeyState` | Mouse button and modifier key state when drag-drop occurs. | | `MousePosition` | Mouse coordinates when drag-drop occurs. | `on_drag_enter(...)` and `on_drag_over(...)` get `&[DragFormat]`, suitable for first checking whether the data can be accepted; `on_drop(...)` gets `&DragData`, suitable for reading the real data. ## DropEffect `DropEffect` is used to tell the system how the current view intends to handle the drag content. Values include: | Value | Meaning | | -------------------- | ---------------------------------------------------- | | `DropEffect::None` | Not accept. | | `DropEffect::Copy` | Accept, and indicates copy. | | `DropEffect::Move` | Accept, and indicates move. | | `DropEffect::Link` | Accept, and indicates create link. | | `DropEffect::Scroll` | Indicates scroll feedback occurred during drag-drop. | In `on_drag_enter`, `on_drag_over` and `on_drop` can modify feedback through `&mut DropEffect`. ## DragFormat and DragData `DragFormat` is used in the enter and hover phase, indicating what formats the current drag object declares it can provide: | Value | Meaning | | ----------------------- | -------------------------------------- | | `DragFormat::Files(_)` | File drag-drop. | | `DragFormat::Text(_)` | Text drag-drop. | | `DragFormat::Image(_)` | Image drag-drop. | | `DragFormat::Custom(_)` | Platform or application custom format. | `DragData` is used in the final release phase, indicating the real data handed to the view: | Value | Meaning | | ------------------------ | ------------------------------------------------ | | `DragData::None` | No available data. | | `DragData::Files(paths)` | File path list. | | `DragData::Text(text)` | Text content. | | `DragData::Image(bytes)` | Image byte data. | | `DragData::Raw(value)` | Platform raw data, used for extension scenarios. | --- url: /website/guide/features/tray.md --- # System Tray `tray` feature is used to enable system tray icon capability. After enabling can add, update, delete tray icons, and receive mouse events on tray icons. ```toml [dependencies] flor = { version = "0.1.0", features = ["direct2d", "tray"] } ``` ## Basic Usage Tray capability is exposed through `FlorGui`. `FlorGui.init()?` will initialize the internal window needed by the tray. ```rust use flor::base::platform::{IconSource, TrayEvent, TrayOptions}; use flor::FlorGui; FlorGui.init()?; FlorGui.tray_on_callback(|tray_id, event| { println!("tray {tray_id:?}: {event:?}"); }); let tray_id = FlorGui.tray_add(&TrayOptions { icon_path: Some(IconSource::from_path("app.ico".into())), tooltip: "Flor".to_string(), })?; ``` ## Related Types Tray functionality will use these types: | Type | Purpose | | ------------- | ------------------------------------------------------------------------------------------------------- | | `TrayOptions` | Configuration passed when creating or updating tray icon. Currently contains `icon_path` and `tooltip`. | | `IconSource` | Icon source, can come from disk file, can also come from RGBA pixel data. | | `TrayId` | Tray icon ID returned by `tray_add(...)`, subsequent update, delete and event callback will all use it. | | `TrayEvent` | Mouse event triggered by tray icon. | | `MouseButton` | Mouse button carried in `TrayEvent`, includes `Left`, `Right`, `Middle`. | `TrayEvent` currently includes: | Value | Meaning | | ------------------------------------- | ---------------------------- | | `TrayEvent::MouseDown(button)` | Mouse button down. | | `TrayEvent::MouseUp(button)` | Mouse button up. | | `TrayEvent::MouseDoubleClick(button)` | Mouse double click. | | `TrayEvent::MouseEnter` | Mouse enters tray icon area. | | `TrayEvent::MouseLeave` | Mouse leaves tray icon area. | | `TrayEvent::MouseMove` | Mouse moves on tray icon. | ## Icon Source Tray icon is specified through `IconSource`: | Writing | Purpose | | ------------------------------------------------ | ------------------------------------------------------------------------------ | | `IconSource::from_path(path)` | Load icon file from disk. On Windows, it is currently suitable to pass `.ico`. | | `IconSource::from_raw(width, height, rgba_data)` | Create icon from RGBA pixel data. | Update and delete: ```rust FlorGui.tray_update(tray_id, &TrayOptions { icon_path: None, tooltip: "New Tooltip Text".to_string(), })?; FlorGui.tray_remove(tray_id)?; ``` --- url: /website/guide/features/theme-change.md --- # Theme Change `theme-change` feature is used to enable system theme change related types and event builder. It is suitable for when the system switches from light mode to dark mode, updating the application theme or rebuilding related styles. ```toml [dependencies] flor = { version = "0.1.0", features = ["direct2d", "theme-change"] } ``` ## Event Entry After enabling feature, can use `on_theme_changed(...)`: ```rust use flor::base::platform::ThemeMode; use flor::view::builder::EventBuilder; use flor_lys::label::label; let root = label("Theme") .on_theme_changed(|_view_id, mode| { match mode { ThemeMode::Light => println!("light"), ThemeMode::Dark => println!("dark"), } }); ``` `ThemeMode` currently has two values: | Value | Meaning | | ------------------ | ----------- | | `ThemeMode::Light` | Light mode. | | `ThemeMode::Dark` | Dark mode. | Complete signature and current dispatch status see [Handler API's Theme Change Events](/website/api/handler.md#theme-change-events). If you only need normal view state styles, don't need to enable this feature; it faces system theme change. --- url: /website/guide/features/monitor.md --- # Monitor Information `monitor` feature is used to enable monitor query capability. It is suitable for initial window positioning, multi-screen adaptation, reading work area size, adjusting custom logic by monitor DPI, and similar tasks. ```toml [dependencies] flor = { version = "0.1.0", features = ["direct2d", "monitor"] } ``` ## Basic Usage Monitor entry is in platform layer. When using import `MonitorApi` trait. ```rust use flor::base::platform::MonitorApi; use flor::platform::Monitor; let monitors = Monitor::enumerate_monitors()?; for monitor in monitors { println!( "{} primary={} scale={} dpi=({}, {})", monitor.name(), monitor.is_primary(), monitor.scale_factor(), monitor.dpi_x(), monitor.dpi_y(), ); } ``` ## Query Entry | Method | Purpose | | -------------------------------------------- | ------------------------------------------------------------------------- | | `Monitor::enumerate_monitors()` | Enumerate all monitors. | | `Monitor::monitor_from_point(x, y)` | Query monitor at a given screen coordinate. | | `Monitor::monitor_from_window_id(window_id)` | Query monitor where window is located. | | `monitor.rect()` | Monitor complete area. | | `monitor.work_area()` | Available work area, usually will exclude taskbar and other system areas. | | `monitor.scale_factor()` | Scale factor, usually derived from DPI. | | `monitor.dpi_x()` / `monitor.dpi_y()` | Current monitor DPI. | If just want layout units to update with window DPI, usually don't need to manually query monitor; see [High DPI](/website/guide/features/hi-dpi.md). --- url: /website/guide/features/hi-dpi.md --- # High DPI `hi-dpi` feature is used to enable process-level DPI awareness. It will let the application receive more accurate DPI information, and let Flor update rendering scaling and layout units when window DPI changes. ```toml [dependencies] flor = { version = "0.1.0", features = ["direct2d", "hi-dpi"] } ``` ## What It Affects After enabling, platform initialization will set DPI awareness. When window receives DPI change message, Flor will update: | Content | Result | | ---------------- | ------------------------------------------------------------------- | | Renderer scaling | Rendering backend will receive new `dpi_x` / `dpi_y`. | | Layout units | `pt` and other DPI-dependent units will use new DPI to recalculate. | | Layout cache | After DPI change will clear layout resolver cache and re-layout. | If you need to detect DPI change at the application layer, you can use the event builder: ```rust use flor::view::builder::EventBuilder; use flor_lys::label::label; let root = label("DPI") .on_dpi_change(|_view_id, dpi_x, dpi_y| { println!("dpi changed: {dpi_x}, {dpi_y}"); }); ``` `on_dpi_change(...)`'s complete signature see [Handler API's Window Events](/website/api/handler.md#window-events). --- url: /website/guide/features/cross-thread-window-creation.md --- # Cross-thread Window Creation `cross-thread-window-creation` feature allows calling `WindowOption::open(...)` outside event loop thread to create windows. ```toml [dependencies] flor = { version = "0.1.0", features = ["direct2d", "cross-thread-window-creation"] } ``` ## When to Enable Judgment rule is simple: if application needs "create window anywhere" capability, must enable `cross-thread-window-creation` feature. Typical scenarios include calling `WindowOption::open(...)` in business thread, background task, async callback or non-event-loop thread. Once this call path exists, don't rely on manually guaranteeing "just create window in correct thread". When not enabling this feature, `WindowOption::open(...)` will by default create the window directly in the current thread. This is suitable for all windows created by the event loop thread or main thread simple programs. But if creating window from non-event-loop thread without enabling this feature, the framework may encounter platform UI thread restrictions, event loop sync issues, message queue waiting issues, and even freeze or deadlock. Therefore, as long as the program has cross-thread window creation needs, you should explicitly enable this feature. ## Basic Behavior After enabling `cross-thread-window-creation`, `WindowOption::open(...)` still uses same API. When call happens outside event loop thread, Flor will post window creation request to event loop thread to process, and let the current thread wait for creation result. This can ensure the window is actually created by the correct UI thread, while letting the caller still get a sync creation result. ```rust use flor::windows::WindowOption; use flor_lys::label::label; std::thread::spawn(|| { WindowOption::default() .open(|_window| label("from worker thread")) .expect("create window"); }); ``` ## Usage Conditions Cross-thread window creation depends on event loop thread processing creation queue, therefore one of the following conditions must be satisfied: 1. Event loop is already running; 2. Creation request is already queued before event loop enters, afterwards will be processed by event loop thread. Common usage is: main thread initializes Flor, and enters `FlorGui.event_loop()?`; business thread calls `WindowOption::open(...)` when needed to request creating new window. ```rust use flor::FlorGui; FlorGui.init()?; // You can request creating a window from the business thread // ... FlorGui.event_loop()?; ``` ## Why This Is a Feature `cross-thread-window-creation` will introduce additional cross-thread request queue, sync waiting and related scheduling logic, therefore will bring certain performance and size overhead. For somewhat larger GUI programs, this part of the overhead is usually negligible. But for very small tool-type programs, especially programs where all windows are only created on the main thread, these additional logic is not necessary, and the extra code on the size and startup path is more worth controlling. Therefore Flor designs it as optional feature: programs needing cross-thread window creation capability can explicitly enable; small programs not needing this capability can avoid introducing extra overhead. ## Platform Design Reason Many platforms have requirements for UI thread, among which macOS explicitly requires UI-related operations running in main thread. This is also an important reason Flor adopts this design: the framework needs to let application code initiate window creation requests from a business position, while still ensuring the actual window creation happens on the correct UI / event loop thread. To accommodate such platform restrictions, and to avoid undefined behavior, message loop freezing or deadlock issues caused by directly creating windows on different threads, Flor designs cross-thread window creation as a "request post to event loop thread, with the event loop thread actually creating the window" pattern. That is, `cross-thread-window-creation` does not let any thread truly create a window directly; rather, it lets any thread safely request window creation, and the window is ultimately still created by the correct UI / event loop thread. If all windows in your application are created on the main thread or event loop thread, you don't need to enable this feature. --- url: /website/guide/control/view-id.md --- # 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](/website/guide/use/builder/focus.md). ## Getting ViewId Event builder callbacks pass target view's `ViewId` as first parameter: ```rust 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: ```rust 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()`. ```rust // 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](/website/guide/control/view-trait.md#lifecycle-and-state). ## State and Layout `ViewId` can read current view's layout and state: ```rust 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` to view instance's `View::on_update_state`, then requests redraw: ```rust 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: ```rust 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](/website/guide/use/focus.md). Here lists runtime methods related to focus on `ViewId`. ```rust impl ViewId { pub fn update_focus_index(self, focus_index: Option); pub fn set_focus(self, virtual_index: Option); 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. ```rust 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. ```rust 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. ```rust 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. ```rust 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: ```rust 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: ```rust 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`: ```rust 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: ```rust 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: ```rust 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: ```rust 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: ```rust 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`. --- url: /website/guide/control/view-trait.md --- # View Trait `View` is Flor's view implementation interface. A concrete view provides its `ViewId` through it, and optionally overrides measurement, drawing, hit test, focus, input events, tooltip, drag-drop and state update hooks. Application side usually uses views provided by independent view library (for example `flor_lys`) when composing interface, then configures with builder. Only when writing new views, encapsulating underlying drawing logic or implementing complex interaction, need to directly implement `View`. View authors should focus on `on_*` methods. Besides must-implement `view_id()`, and temporary debugging `tag()`, don't override `call_*`, `bus_*`, `visual_rect()` and other non-`on_*` dispatch methods; they are framework internal scheduling layer. ## Minimal View Every view must save its own `ViewId`. `view_id()` only returns this stable field, don't recreate ID in `view_id()`. ```rust use flor::view::{View, ViewId}; pub struct Spacer { view_id: ViewId, } impl Spacer { pub fn new() -> Self { Self { view_id: ViewId::new(), } } } impl View for Spacer { fn view_id(&self) -> ViewId { self.view_id } } ``` Default view won't draw content, measurement result is `Size::ZERO`, hit test judges by layout rectangle. Actual views usually override `on_draw`; whether to override `on_measure` depends on whether this view needs to declare its natural size to layout system. `tag()` by default returns `"View"`. It's currently mainly entry for temporarily identifying view type during debugging, may be adjusted or deleted later, don't build business logic on `tag()`. ## Drawing and Measurement Flor uses Taffy to calculate layout. Layout phase will call `on_measure`, drawing phase will call `on_draw`. > `on_measure` is not required capability for all views. If you want view to support responsive layout, content adaptation, calculate natural size by text or image, need to implement measurement, let framework know how much space view needs. If it's MFC-like pure fixed layout scenario, caller manually specifies view width and height, can also not implement measurement, directly rely on fixed size in layout style. ```rust use flor::base::graphics::RenderContext; use flor::error::Error; use flor::render::FlorRenderer; use flor::taffy::{AvailableSpace, Size, Style}; use flor::types::Color; use flor::view::resolver::ComputedLayout; use flor::view::{ControlState, View, ViewId}; pub struct ColorBlock { view_id: ViewId, color: Color, } impl ColorBlock { pub fn new(color: Color) -> Self { Self { view_id: ViewId::new(), color, } } } impl View for ColorBlock { fn view_id(&self) -> ViewId { self.view_id } fn on_measure( &mut self, known_dimensions: Size>, _available_space: Size, _style: &Style, _control_state: ControlState, _render: &mut FlorRenderer, ) -> Result, Error> { Ok(Size { width: known_dimensions.width.unwrap_or(80.0), height: known_dimensions.height.unwrap_or(32.0), }) } fn on_draw( &mut self, render: &mut FlorRenderer, _control_state: ControlState, abs_location: (f32, f32), layout: ComputedLayout, ) -> Result<(), Error> { let brush = render.create_solid_color_brush(self.color, None)?; render.fill_quad( abs_location.0, abs_location.1, layout.size.width, layout.size.height, &brush, None, None, )?; Ok(()) } } ``` `on_measure` receives known dimensions, available space, current `Style`, current `ControlState` and renderer passed in by Taffy. Text views usually create text layout here to measure content; image views usually return needed size based on image size, available space and scaling strategy. `on_draw` receives renderer, current `ControlState`, absolute position in window coordinate system and calculated layout. Use passed-in `abs_location` and `layout.size` when drawing, don't re-derive window coordinates. Child views will be drawn after current view; `on_draw_overlay` will be called after child views, suitable for scrollbar, floating decoration and other overlay layers. ## Lifecycle and State Common lifecycle hooks include: | Method | Call Timing | | ------------------ | ------------------------------------------------------------------------------------- | | `on_create` | Called in view creation flow, external `on_create` handler also executes in same path | | `on_update_state` | Executes when `ViewId::update_state(Box)` is called | | `on_frame` | Executes during frame scheduling, can return next wake-up wait time | | `on_child_push` | Notify parent view after child view joins | | `on_child_dispose` | Notify parent view after child view releases | ### on\_create `on_create` is called when window creation flow traverses view tree. Framework actually goes through `call_create`: first activate pending effects mounted on this `ViewId`, then call view internal `on_create`, finally call user's external `on_create` handler bound through builder. If view initialization depends on window, renderer or context already entered view tree, can put in `on_create`. Pure field initialization should be in constructor, don't wait until `on_create`. ### on\_update\_state `on_update_state` is view reactive update entry, usually works with signal system. View library creates updater: read signal in compute closure, call `ViewId::update_state(Box::new(...))` in on-change closure, finally downcast and update internal fields by view's own `on_update_state`. `flor_lys::label` and `flor_lys::button` both use this pattern: title can be fixed value, or closure reading signal; after signal changes, update title and request redraw. ```rust use flor::signal::create_updater; use flor::view::builder::StringProp; use flor::view::{View, ViewId}; use std::any::Any; pub struct TitleView { view_id: ViewId, title: String, } impl TitleView { pub fn new(title: P) -> Self { let view_id = ViewId::new(); // Create reactive updater: read signal value, call update_state when changes let title = create_updater( move || title.make(), move |value| view_id.update_state(Box::new(value)), ); Self { view_id, title } } } impl View for TitleView { fn view_id(&self) -> ViewId { self.view_id } fn on_update_state(&mut self, state: Box) { // downcast and update internal fields if let Ok(title) = state.downcast::() { self.title = *title; // Note: ViewId::update_state already requests redraw after call, // so here don't need to call request_redraw() again } } } ``` If state change affects natural size, for example text change, image handle change or font style change, view needs to clear measurement cache, and mark belonging window for re-layout when needed. Changes only affecting drawing usually just need to request redraw; `ViewId::update_state` already requests redraw after call. ### on\_frame `on_frame` is used for animation and periodic state progression. Default returns `Ok(None)`, indicates this view has no next active wake-up need. Returning `Ok(Some(duration))` indicates view hopes event loop wakes up again no later than this time. Framework traverses visible view subtree, takes minimum value of all child views' returned wait times, then hands to platform wait logic. That is, a GIF view returns next frame remaining time, input view returns cursor blink interval, eventually will use shortest time as next wake-up target. ```rust use flor::error::Error; use flor::view::{View, ViewId}; use std::time::{Duration, Instant}; pub struct CursorView { view_id: ViewId, focused: bool, cursor_visible: bool, last_blink: Instant, } impl View for CursorView { fn view_id(&self) -> ViewId { self.view_id } fn on_frame(&mut self, now: Instant) -> Result, Error> { // If no focus, don't need animation, return None indicates don't need wake-up if !self.focused { return Ok(None); } const BLINK: Duration = Duration::from_millis(530); // Check if reached blink time if now.duration_since(self.last_blink) >= BLINK { // Toggle cursor visible state self.cursor_visible = !self.cursor_visible; self.last_blink = now; // Important: must request redraw after visual state change self.view_id.request_redraw(); } // Return next needed wake-up time Ok(Some(BLINK)) } } ``` `flor_lys::image`'s GIF frame progression is same type of usage: calculate target frame based on current time, request redraw when frame changes, and return remaining time to next frame. Note that current `bus_frame` skips `display: none` and views not marked as visible in most recent draw. #### on\_frame\_policy `on_frame_policy()` by default returns `FramePolicy::VisibleOnly`, current public enum also has `FramePolicy::Always`. Current scheduling path hasn't read this return value; `bus_frame` still decides whether to call `on_frame` by `display != none` and `ViewId::visual()` visibility cache obtained from most recent draw. ### on\_child\_push `on_child_push` is called after child view joins current view. It's mainly for compound views, for example recording how many child views currently, rebuilding child view index, refreshing internal cache or syncing auxiliary data structures. `div` is also compound view type, just it itself has no extra view logic and drawing logic; framework already provides basic layout capability, so simple containers can not override this method. ### on\_child\_dispose `on_child_dispose` is called after child view releases from current view. It corresponds to `on_child_push`, suitable for cleaning compound view's maintained child view count, cache, selection state or hit auxiliary structures. Both methods happen after framework completes basic parent-child relationship update. View authors don't need to manually write `VIEW_STORAGE.child_ids` here. ## class Update After enabling `class` feature, `ViewId::update_class` will first parse state prefix, for example `hover:`, `focus:`, `active:`, `disabled:`, then hand view style class to: ```rust fn on_update_class(&mut self, control_state: ControlState, class: &str) -> Result<(), Error>; ``` View can convert class name to its own style update here. Layout classes are still handled by `LayoutResolver`; `z-*` will be parsed into runtime z-index, won't enter view style parsing. ## Focus and Keyboard Focus table manages by `(focus_index, ViewId, virtual_index)`. View can override these methods: | Method | Purpose | | ---------------------------------------------- | -------------------------------------------------------------------------------- | | `on_focus_count` | Declare how many virtual focus points this view has, default `1` | | `on_virtual_focus_at` | Decide which virtual focus to focus when clicking by mouse position, default `0` | | `on_focus` | Called when current view gains some virtual focus | | `on_blur` | Called when current view loses some virtual focus | | `on_key_down` / `on_key_up` | Receive keyboard events when current view holds focus | | `on_ime_start` / `on_ime_input` / `on_ime_end` | Current focus view receives IME input flow | `on_focus_count` return value decides how many virtual focus records to generate when initializing focus table. For example returning `3` and this view set `focus_index`, focus table will generate virtual numbers `0`, `1`, `2` for it. `on_virtual_focus_at` is used to map one click to some virtual focus. Keyboard methods return `HandleResult`. Internal `on_key_*` and external handler return values will merge: any side returns `Handled`, final is `Handled`. Complete focus mechanism see [Focus Mechanism](/website/guide/use/focus.md). Runtime focus API see [ViewId](/website/guide/control/view-id.md). ## Mouse and Wheel Events Mouse events are usually dispatched to target view after hit test: | Method | Description | | ------------------------------------------------------------------ | ----------------------------------------- | | `on_mouse_enter` / `on_mouse_move` / `on_mouse_leave` | Mouse enter, move, leave | | `on_button_down` / `on_button_up` / `on_click` / `on_double_click` | Left button down, up, click, double click | | `on_right_button_*` | Right button events | | `on_middle_button_*` | Middle button events | | `on_wheel_scroll_lines_changed` | Wheel line scroll event | Most events' `call_*` default implementation first executes view internal `on_*`, then executes user's external handler bound through builder. View authors override `on_*`; `call_*` is framework dispatch layer, don't override in view implementation. ## Tooltip and Drag-Drop Tooltip has independent overlay mode: ```rust fn on_tooltip_show(&mut self, key_state: KeyState, mouse_position: MousePosition) -> Result<(), Error>; fn on_tooltip_hide(&mut self) -> Result<(), Error>; ``` If user bound tooltip handler, framework only executes external handler; only calls view's `on_tooltip_show` or `on_tooltip_hide` when not bound. Drag-drop methods are controlled by `drag-drop` feature: | Method | Return | | --------------- | --------------------------- | | `on_drag_enter` | `Result` | | `on_drag_over` | `Result` | | `on_drag_leave` | `Result<(), Error>` | | `on_drop` | `Result` | `drag_enter`, `drag_over` and `drop` external handlers can modify final effect through `&mut DropEffect`. ## Hit Test Hit test is capability for irregular visual views, most views don't need to override. Default implementation already handles rectangular layout frame hit judgment. Hit test starts from window coordinates, framework uses accumulated transform to convert mouse position to each view's local coordinates. `mouse_position` passed to following two methods is view local coordinates: ```rust fn on_hit_test_overlay(&self, mouse_position: MousePosition, key_state: KeyState) -> bool; fn on_hit_test(&self, mouse_position: MousePosition, key_state: KeyState) -> bool; ``` Each node's call order is: 1. If view not marked as visible by most recent draw, `display: none`, or mouse not in parent clipping area, skip this branch. 2. First call current view's `on_hit_test_overlay`. When it hits, directly returns current view, no longer checks child nodes. 3. If overlay didn't hit, then check child views by reverse drawing order. Later drawn child views test first, matches intuition of upper layer elements hit first. 4. When all child views didn't hit, finally call current view's `on_hit_test`. `on_hit_test_overlay` is for "overlay above child views" areas, for example scrollbar, drag resize handle, floating button. Default implementation judges scrollbar area based on scrollbar size in layout. `on_hit_test` is for view main area. Default implementation judges whether point is in `(0, 0, width, height)` layout rectangle. Only need to override this method when needing circular button, irregular path, transparent area penetration, enlarged click hot area. Hit test only decides event target. View's visible area clipping, whether drawing happens, whether focus is obtainable, are decided by other mechanisms. ## View State Flor internally inherits view state mechanism, many methods receive `ControlState` parameter. Drawing and measurement both receive this state, state priority is `Disabled > Active > Focus > Hover > Normal`. View should use this state to read its own style resolver, or choose different drawing branches. Disabled state is not view development method on `View` trait. Application side sets `ViewState.disable` through [Disable Builder](/website/guide/use/builder/disable.md); view authors only need to correctly respond to `ControlState::Disabled`, for example drawing disabled style, ignoring input or changing hit behavior. ## Visible Area After layout refresh completes, framework traverses view tree, calls each view's `visual_rect()` and caches to `VIEW_STORAGE.visual_rect`. Default `visual_rect()` uses layout boundary; it internally reads `on_visual_overflow()` to extend this boundary. Drawing phase reads cached `visual_rect`. If a view's visible rectangle has no intersection with parent clipping area, framework skips this view, doesn't call `on_draw`, also doesn't write it to `VIEW_STORAGE.visual`. If not culled, framework first writes visible mark, then dispatches `on_draw`. View authors don't override `visual_rect()`, should override: ```rust fn on_visual_overflow(&self) -> VisualOverflow; ``` This method has two main purposes: - When view draws outside layout frame, expand visible area. For example shadow, outer glow, focus ring, tooltip arrow. - Irregular views or irregular path views need more accurate bounds to participate in visibility judgment, avoiding clearly having content on screen, but being missed by the visible mark due to default layout frame being too small. `VisualOverflow::Uniform` is suitable for uniform distance expansion in four directions, `Custom` is suitable for shadow with directional offset, `Path` uses path bounds. It affects visibility culling and visible mark, not equal to pixel-level clipping or hit test; hit test should still be decided by `on_hit_test` / `on_hit_test_overlay` itself. ## Resource Loading All `View: LoadRenderResource` can find belonging window renderer through its own `view_id()`, and create rendering resources: ```rust use flor::render::LoadRenderResource; let handle = self.load_image(bytes)?; ``` `load_raw_image` supports raw frame data, supports `load_svg` after enabling `svg` feature. If view hasn't associated to renderer, returns `FlorRendererError::RenderNotFound`. ## VIEW\_STORAGE If need more underlying data capability access, can use `VIEW_STORAGE` global variable. It stores view tree, layout state, visibility cache, focus table and other runtime data. Most view development scenarios don't need to directly access `VIEW_STORAGE`, prefer using methods provided by `ViewId`. --- url: /website/guide/control/resolver/style-unit.md --- # Style Unit Flor's layout system uses `Unit` and `Length` types to represent size units. All units are converted to pixel values before being passed to layout engine. ```rust use flor::view::resolver::{Unit, Length}; ``` ## Unit `Unit` is unit type marker, used to mark which unit system a number belongs to. ```rust pub enum Unit { /// Pixel, no conversion #[default] Px, /// Point, uses window DPI conversion: 1pt = dpi / 72px Pt, /// Root em, uses window configured rem_px conversion Rem, /// Viewport width unit, 1vw = 1% of client area width Vw, /// Viewport height unit, 1vh = 1% of client area height Vh, } ``` Default value is `Unit::Px`. ### Px (Pixel) Pixel is absolute unit, no conversion. Layout engine directly uses pixel value. ```rust let unit = Unit::Px; ``` ### Pt (Point) Point is printing unit, uses window DPI for conversion: ``` 1pt = dpi / 72px ``` Under standard 96 DPI, `1pt = 1.333px`, `12pt = 16px`. ```rust let unit = Unit::Pt; ``` ### Rem (Root em) Rem is unit relative to root element font size. Flor uses window configured `rem_px` as conversion base: ``` 1rem = rem_px px ``` Default `rem_px = 16.0`, i.e. `1rem = 16px`. ```rust let unit = Unit::Rem; ``` ### Vw (Viewport Width) Vw is unit relative to viewport width: ``` 1vw = viewport_width / 100 px ``` Viewport width equals window client area width. `50vw` means half of viewport width. ```rust let unit = Unit::Vw; ``` ### Vh (Viewport Height) Vh is unit relative to viewport height: ``` 1vh = viewport_height / 100 px ``` Viewport height equals window client area height. `50vh` means half of viewport height. ```rust let unit = Unit::Vh; ``` ## Length `Length` is length type with number, binding number and unit together. ```rust pub enum Length { /// Pixel value Px(f32), /// Point value Pt(f32), /// Root em value Rem(f32), /// Viewport width value Vw(f32), /// Viewport height value Vh(f32), } ``` Example: ```rust let length = Length::Px(16.0); // 16 pixels let length = Length::Rem(1.0); // 1 rem let length = Length::Vw(50.0); // 50% viewport width ``` ## UnitMetrics `UnitMetrics` stores window-level unit conversion parameters, each window maintains one instance. ```rust pub struct UnitMetrics { /// 1rem corresponding pixel value, default 16.0 pub rem_px: AtomicF32, /// Horizontal DPI, default 96.0 pub dpi_x: AtomicF32, /// Vertical DPI, default 96.0 pub dpi_y: AtomicF32, /// Viewport width (client area width, pixels) pub viewport_width: AtomicF32, /// Viewport height (client area height, pixels) pub viewport_height: AtomicF32, } ``` ### Default Values | Parameter | Default Value | | ----------------- | ------------- | | `rem_px` | 16.0 | | `dpi_x` | 96.0 | | `dpi_y` | 96.0 | | `viewport_width` | 1024.0 | | `viewport_height` | 768.0 | ### Update Timing These values are initialized when window is created, and updated at following times: - When window size changes, update `viewport_width` and `viewport_height` - When DPI changes, update `dpi_x` and `dpi_y` - When user modifies `WindowOption::rem_px`, update `rem_px` ## Unit Conversion Formula | Unit | Formula | Example (Default Parameters) | | ---- | ---------------------------- | ----------------------------- | | Px | No conversion | `16px` → `16px` | | Pt | `pt * dpi_y / 72` | `12pt` (dpi=96) → `16px` | | Rem | `rem * rem_px` | `1rem` (rem\_px=16) → `16px` | | Vw | `vw * viewport_width / 100` | `50vw` (width=1024) → `512px` | | Vh | `vh * viewport_height / 100` | `50vh` (height=768) → `384px` | ## Usage Suggestions - **Px**: For precise sizes, like border width, icon size - **Rem**: For font size, spacing, convenient for responsive design - **Pt**: For printing related scenarios, or when need to stay consistent with CSS pt - **Vw / Vh**: For viewport related layout, like fullscreen container, center positioning --- url: /website/guide/control/resolver/resolver-struct.md --- # Resolver Struct > This page doesn't need deliberate learning, just read to have an impression. When developing views, directly use [Resolver Derive Macro](/website/guide/control/resolver/resolver-derive.md) is enough. `Resolver` is Flor framework's internal style/layout resolver, used to manage state variants, unit conversion and calculation cache. View authors usually don't need to directly operate `Resolver`, instead through [Resolver Derive Macro](/website/guide/control/resolver/resolver-derive.md) automatically generate implementation. ```rust use flor::view::resolver::Resolver; ``` ## Overview `Resolver` core responsibilities: - Store style variants under different `ControlState` - Perform unit conversion through `UnitResolver` - Cache calculation results, avoid repeated calculation - Support multi-layer style stacking (layer) View authors only need to know: `Resolver` will automatically select corresponding style variant based on current view state (Normal, Hover, Focus, Active, Disabled), and calculate final result. ## new\_with\_compute\_func `new_with_compute_func` is `Resolver`'s constructor entry, needs to pass `ViewId` and calculation function: ```rust pub fn new_with_compute_func(view_id: ViewId, compute_func: F) -> Self ``` Calculation function signature: ```rust F: for<'a> Fn(&UnitResolver, &ResolverComputeMap) -> D ``` It receives unit resolver and merged style variants, returns calculated data. ### Example: LayoutResolver `LayoutResolver` is an instance of `Resolver`, used to calculate Taffy layout style: ```rust use flor::view::resolver::LayoutResolver; impl LayoutResolver { pub fn new(view_id: ViewId) -> Self { Self::new_with_compute_func(view_id, computed_layout) } } ``` `computed_layout` function and `LayoutResolver` type are both automatically generated by `#[derive(Resolver)]` derive macro. It will traverse all layout variants, call `UnitResolver` for unit conversion, finally generate `taffy::Style`. ## Cache Mechanism `Resolver` internally maintains cache: - `cache_data`: Cache calculation results by `ControlState` - `dirty`: Dirty flag, used to determine whether need to recalculate When style variant changes, cache will be cleared. Getting data will automatically trigger calculation. View authors don't need to care about cache details, only need to know: multiple calls to `get_data_clone` or `get_data_borrow` won't repeat calculation. ## Usage Method View authors shouldn't manually create `Resolver`. Correct way is: 1. Define style enum, for example `Layout` or custom style type 2. Use `#[derive(Resolver)]` derive macro to automatically generate `Resolver` implementation 3. Call generated `new` method in view constructor See [Resolver Derive Macro](/website/guide/control/resolver/resolver-derive.md) for details. --- url: /website/guide/control/resolver/resolver-derive.md --- # Resolver Derive Macro `#[derive(Resolver)]` is Flor's style parsing derive macro. It automatically generates Resolver type, chain methods, state variant support and reactive update capability for style enums. This macro has built-in view state mechanism, mainly used for view style system. It can work with [class system](/website/guide/use/builder/class.md), parse class names in `on_update_class` and update styles. View authors only need to define style enum, derive macro will automatically generate all Resolver related code. ## Minimal Usage Define a style enum, add `#[derive(Resolver)]`: ```rust use flor::view::resolver::Resolver; use flor::types::Color; #[derive(Clone, Debug, Resolver)] pub enum LabelStyle { TextColor(Color), FontSize(f32), } ``` Derive macro will automatically generate: - `LabelStyleKey` - Style property key enum - `LabelStyleResolver` - Resolver type alias - `LabelStyleResolverExt` trait - Chain methods (`.text_color(...)`, `.font_size(...)`) - `LabelStyleComputed` - Calculated style struct - `LabelStyleUpdate` - Reactive update enum - `computed_label_style` function - Maps raw style values to `Computed` struct ## Generated Types ### Key Enum `{EnumName}Key` is style property key, used for Resolver internal lookup: ```rust pub enum LabelStyleKey { TextColor, FontSize, } ``` ### Resolver Type Alias `{EnumName}Resolver` is complete Resolver type alias: ```rust pub type LabelStyleResolver = Resolver< LabelStyleKey, LabelStyle, LabelStyleComputed, fn(&UnitResolver, &ResolverComputeMap) -> LabelStyleComputed >; ``` ### Computed Struct `{EnumName}Computed` is calculated style struct, all fields are `Option`: ```rust #[derive(Clone, Debug, Default)] pub struct LabelStyleComputed { pub text_color: Option, pub font_size: Option, } ``` Read computed struct when drawing: ```rust fn on_draw(&mut self, ...) { let style = self.style_resolver.get_data_clone(ControlState::Normal); let color = style.text_color.unwrap_or(Color::BLACK); let size = style.font_size.unwrap_or(16.0); } ``` ## Chain Methods `{EnumName}ResolverExt` trait provides chain methods for Resolver. Method name is variant name's snake\_case form: ```rust pub trait LabelStyleResolverExt { fn text_color(self, value: Color) -> Self; fn font_size(self, value: f32) -> Self; fn set_text_color(&mut self, value: Color) -> &mut Self; fn set_font_size(&mut self, value: f32) -> &mut Self; } ``` Usage example: ```rust let resolver = LabelStyleResolver::new(view_id) .text_color(Color::RED) .font_size(14.0); ``` ## State Variants Resolver supports storing different style variants by `ControlState`. Switch current state through `.normal()`, `.hover()`, `.focus()`, `.active()`, `.disabled()`: ```rust let resolver = LabelStyleResolver::new(view_id) .normal() .text_color(Color::BLACK) .font_size(16.0) .hover() .text_color(Color::BLUE) .focus() .text_color(Color::RED); ``` Read by current view state when drawing: ```rust let style = resolver.get_data_clone(control_state); ``` State inheritance rules: - `Hover` inherits `Normal` styles, then covers Hover variants - `Focus` inherits `Normal` styles, then covers Focus variants - `Active` inherits `Normal` and `Focus` styles, then covers Active variants - `Disabled` doesn't inherit, only uses Disabled variants ## StyleBuilder Derive macro by default generates `StyleBuilder` trait implementation for view, allows application side to modify styles through `.style(...)` method: ```rust impl StyleBuilder for Label { fn style(mut self, style_fn: impl Fn(LabelStyleResolver) -> LabelStyleResolver) -> Self { self.style = style_fn(self.style); self } } ``` Application side usage: ```rust label("Title") .style(|s| s.text_color(Color::RED).font_size(24.0)); ``` `.style(...)` method receives a function, can use ResolverExt's chain methods to modify styles in function. ## Update Enum Derive macro by default generates `{EnumName}Update` enum and `update_view` method: ```rust pub enum LabelStyleUpdate { TextColor(ControlState, Color), FontSize(ControlState, f32), } impl LabelStyle { pub fn update_view( resolver: &mut LabelStyleResolver, update: LabelStyleUpdate ) { ... } } ``` `Update` enum contains update type for each style variant, `update_view` method will update corresponding state's style value in Resolver. ## Configuration Parameters Configure generated content through `#[resolver(...)]` attribute: ### update\_view = false Skip Update enum and update\_view method generation: ```rust #[derive(Clone, Debug, Resolver)] #[resolver(update_view = false)] pub enum Layout { Display(Display), } ``` Suitable for style types not needing reactive update. ### computed = false Skip Computed struct generation: ```rust #[derive(Clone, Debug, Resolver)] #[resolver(computed = false, data = taffy::Style)] pub enum Layout { Display(Display), } ``` Suitable for directly using external type (like `taffy::Style`) as calculation result. ### computed\_fn = false Skip `computed_xxx` independent function generation. By default, derive macro generates a `pub fn computed_{enum_snake_name}(...)` function, mapping enum's raw style values to `Computed` struct: ```rust // Automatically generated function (using LabelStyle as example) pub fn computed_label_style( _unit_resolver: &UnitResolver, variants: &ResolverComputeMap, ) -> LabelStyleComputed { let mut computed = LabelStyleComputed::default(); for (k, v) in variants.iter() { match k { LabelStyleKey::TextColor => { if let LabelStyle::TextColor(val) = v { computed.text_color = Some(val.clone()); } } LabelStyleKey::FontSize => { if let LabelStyle::FontSize(val) = v { computed.font_size = Some(val.clone()); } } _ => {} } } computed } ``` This function will be passed into `new_with_compute_func`: ```rust LabelStyleResolver::new_with_compute_func(view_id, computed_label_style) ``` When don't need to automatically generate compute function, can turn off: ```rust #[derive(Clone, Debug, Resolver)] #[resolver(computed_fn = false)] pub enum MyStyle { Value(f32), } ``` > **Note**: `computed_fn = false` usually paired with `computed = false` (like Layout), at this time neither generates `Computed` struct nor compute function. If only turn off `computed_fn` but keep `computed`, then need to manually write compute function. ### data = Type Specify Resolver's data type, generate type alias: ```rust #[derive(Clone, Debug, Resolver)] #[resolver(data = taffy::Style)] pub enum Layout { Display(Display), } ``` Generated `LayoutResolver` will use `taffy::Style` as D generic. ### builder = false Skip StyleBuilder generation: ```rust #[derive(Clone, Debug, Resolver)] #[resolver(builder = false)] pub enum InternalStyle { Value(f32), } ``` Suitable for internal style types, don't need to expose to application side builder. ### default = false Skip Resolver's Default implementation: ```rust #[derive(Clone, Debug, Resolver)] #[resolver(default = false)] pub enum CustomStyle { Value(f32), } ``` ## Variant Attributes ### skip\_attr Skip all generation for some variant: ```rust #[derive(Clone, Debug, Resolver)] pub enum Style { Color(Color), #[resolver(skip_attr)] InternalDebug(String), // Don't generate Key, methods etc } ``` ### skip\_linkfn Skip chain method generation, but keep Key and other content: ```rust #[derive(Clone, Debug, Resolver)] pub enum Layout { #[resolver(skip_linkfn)] Size(Size), // Don't generate .size() method Display(Display), } ``` ## Actual Usage in View Following example shows Resolver's complete usage flow in view. ### Constructor Create Resolver when view constructs, can pass custom calculation function: ```rust use flor::view::ViewId; use flor::view::resolver::LabelStyleResolver; pub struct Label { view_id: ViewId, title: String, style: LabelStyleResolver, } impl Label { pub fn new(title: String) -> Self { let view_id = ViewId::new(); Self { view_id, title, // Use new_with_compute_func to pass calculation function style: LabelStyleResolver::new_with_compute_func(view_id, computed_label_style), } } } ``` `computed_label_style` function is automatically generated by derive macro, it will traverse all style variants and fill `LabelStyleComputed`. ### on\_measure Read style by current view state when measuring: ```rust fn on_measure( &mut self, known_dimensions: Size>, available_space: Size, _style: &Style, control_state: ControlState, render: &mut FlorRenderer, ) -> Result, Error> { // Get current state's style let computed = self.style.get_data_borrow(control_state); // Use style values for measurement let font_size = computed.font_size.unwrap_or(16.0); let font_family = computed.font_family.clone().unwrap_or_default(); // Create text format and measure let text_format = render.create_text_format(&font_family)?; text_format.set_font_size(font_size); let layout_text = render.create_text_layout(self.title.clone(), bounds, text_format)?; let (width, height) = layout_text.measure_text()?; Ok(Size { width, height }) } ``` `get_data_borrow` returns RwLock's mapping guard, avoids clone overhead. If need multiple accesses or cross-method passing, use `get_data_clone`. ### on\_draw Read style by view state when drawing too: ```rust fn on_draw( &mut self, render: &mut FlorRenderer, control_state: ControlState, abs_location: (f32, f32), layout: ComputedLayout, ) -> Result<(), Error> { let computed = self.style.get_data_borrow(control_state); // Read style values, use unwrap_or to provide default values let text_color = computed.text_color.unwrap_or(Color::BLACK); let font_size = computed.font_size.unwrap_or(16.0); // Use style to draw let brush = render.create_solid_color_brush(text_color, None)?; // ... } ``` ### on\_update\_class `on_update_class` is class system's entry. View parses class names and updates styles here: ```rust fn on_update_class(&mut self, control_state: ControlState, class: &str) -> Result<(), Error> { // 1. Switch to current state self.style.switch_control_state(control_state); // 2. Parse class name and set style let class = class.trim(); if let Some(rest) = class.strip_prefix("text-") { // text-red-500 -> set color if let Some(color) = parse_color(rest) { self.style.set_text_color(color); } // text-lg -> set font size if let Some(size) = self.style.unit_resolver.parse_tw_font_size(rest) { self.style.set_font_size(size); } } if let Some(rest) = class.strip_prefix("bg-") { // bg-blue-500 -> set background if let Some(color) = parse_color(rest) { self.style.set_background(Background::Color(color)); } } Ok(()) } ``` Key steps: 1. `switch_control_state(control_state)` - Switch Resolver to current state 2. `set_xxx(...)` - Use generated chain methods to set style values Each class name match success directly returns, if not match then continue trying other rules. ### on\_update\_state Handle `LabelStyleUpdate` when reactive update: ```rust fn on_update_state(&mut self, state: Box) { // Try to handle title update if let Ok(title) = state.downcast::() { self.title = *title; return; } // Try to handle style update if let Ok(update) = state.downcast::() { // Call macro-generated update_view method LabelStyle::update_view(&mut self.style, *update); } } ``` `LabelStyleUpdate` is automatically generated by derive macro, contains update enum for each style variant. `update_view` method will update corresponding state's style value in Resolver. --- url: /website/guide/control/resolver/class-resolve.md --- # Class Name Resolution Helper Functions `flor::view::resolver::shared` provides helper functions for class name resolution, used to parse state prefixes, colors, rounded corners, font weights and other common style class names. ```rust use flor::view::resolver::shared::*; ``` ## parse\_state\_prefix Parse state prefix in class name, return corresponding `ControlState` and remaining class name. ```rust pub fn parse_state_prefix(class: &str) -> (ControlState, &str) ``` ### Supported Prefixes | Prefix | Returned State | | ----------- | ------------------------ | | `hover:` | `ControlState::Hover` | | `focus:` | `ControlState::Focus` | | `active:` | `ControlState::Active` | | `disabled:` | `ControlState::Disabled` | | No prefix | `ControlState::Normal` | ### Example ```rust let (state, rest) = parse_state_prefix("hover:bg-red-500"); // state = ControlState::Hover, rest = "bg-red-500" let (state, rest) = parse_state_prefix("bg-blue-200"); // state = ControlState::Normal, rest = "bg-blue-200" ``` ## extract\_bracket\_value Extract value inside brackets, used to parse `[value]` form arbitrary value class names. ```rust pub fn extract_bracket_value(s: &str) -> Option<&str> ``` ### Parameters | Parameter | Type | Description | | --------- | ------ | --------------- | | `s` | `&str` | String to parse | ### Return Value - If string starts with `[` and ends with `]`, return content inside brackets - Otherwise return `None` ### Example ```rust let val = extract_bracket_value("[#fff]"); // val = Some("#fff") let val = extract_bracket_value("#fff"); // val = None ``` ## parse\_color Parse color value, supports keywords, Hex and Tailwind color names. ```rust pub fn parse_color(value: &str) -> Option ``` ### Supported Formats | Format | Example | Description | | ----------- | ------------------------------- | ------------------------------ | | Keyword | `transparent`, `black`, `white` | Predefined color names | | Hex | `#fff`, `#ffffff` | 3-digit or 6-digit hexadecimal | | Bracket Hex | `[#fff]`, `[#ffffff]` | Hex wrapped in brackets | | Tailwind | `red-500`, `blue-100` | TW palette names | ### Tailwind Palette Supports following palettes, each palette contains 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950 total 11 shades: - Gray series: `slate`, `gray`, `zinc`, `neutral` - Red series: `red`, `orange`, `amber`, `yellow` - Green series: `lime`, `green`, `emerald`, `teal`, `cyan` - Blue series: `sky`, `blue`, `indigo` - Purple series: `violet`, `purple`, `fuchsia`, `pink`, `rose` ### Example ```rust let color = parse_color("transparent"); // color = Some(Color::rgba(0, 0, 0, 0)) let color = parse_color("#fff"); // color = Some(Color::from_hex_str("#fff")) let color = parse_color("red-500"); // color = Some(Color::RED_500) let color = parse_color("slate-700"); // color = Some(Color::SLATE_700) ``` ## parse\_tw\_color Parse Tailwind color name, return corresponding `Color`. ```rust pub fn parse_tw_color(color_name: &str, shade: &str) -> Option ``` ### Parameters | Parameter | Type | Description | | ------------ | ------ | -------------------------------- | | `color_name` | `&str` | Palette name, like `red`, `blue` | | `shade` | `&str` | Shade number, like `500`, `100` | ### Return Value - If both palette name and shade are valid, return corresponding `Color` - Otherwise return `None` ### Example ```rust let color = parse_tw_color("red", "500"); // color = Some(Color::RED_500) let color = parse_tw_color("emerald", "700"); // color = Some(Color::EMERALD_700) let color = parse_tw_color("unknown", "500"); // color = None ``` ## parse\_rounded Parse `rounded-*` class name, return rounded corner value. ```rust pub fn parse_rounded(class: &str) -> Option ``` ### Supported Class Names | Class Name | Return Value | Description | | --------------- | ------------ | -------------------------- | | `rounded-none` | `0.0` | No rounded corner | | `rounded-sm` | `2.0` | Small rounded corner | | `rounded` | `4.0` | Default rounded corner | | `rounded-md` | `6.0` | Medium rounded corner | | `rounded-lg` | `8.0` | Large rounded corner | | `rounded-xl` | `12.0` | Larger rounded corner | | `rounded-2xl` | `16.0` | Extra large rounded corner | | `rounded-3xl` | `24.0` | Huge rounded corner | | `rounded-full` | `9999.0` | Fully circular | | `rounded-[Npx]` | `N.0` | Custom pixel value | | `rounded-[N]` | `N.0` | Custom numeric value | ### Example ```rust let radius = parse_rounded("rounded-lg"); // radius = Some(8.0) let radius = parse_rounded("rounded-[12px]"); // radius = Some(12.0) let radius = parse_rounded("rounded-unknown"); // radius = None ``` ## parse\_font\_weight Parse font weight in `font-*` class name, return `FontWeight`. ```rust pub fn parse_font_weight(name: &str) -> Option ``` ### Supported Weight Names | Class Name | Return Value | CSS Weight | | ----------------- | ------------------------ | ---------- | | `font-thin` | `FontWeight::Thin` | 100 | | `font-extralight` | `FontWeight::ExtraLight` | 200 | | `font-light` | `FontWeight::Light` | 300 | | `font-normal` | `FontWeight::Normal` | 400 | | `font-medium` | `FontWeight::Medium` | 500 | | `font-semibold` | `FontWeight::SemiBold` | 600 | | `font-bold` | `FontWeight::Bold` | 700 | | `font-extrabold` | `FontWeight::ExtraBold` | 800 | | `font-black` | `FontWeight::Black` | 900 | ### Parameters | Parameter | Type | Description | | --------- | ------ | ----------------------------------------------------------- | | `name` | `&str` | Weight name (without `font-` prefix), like `bold`, `medium` | ### Example ```rust let weight = parse_font_weight("bold"); // weight = Some(FontWeight::Bold) let weight = parse_font_weight("semibold"); // weight = Some(FontWeight::SemiBold) let weight = parse_font_weight("unknown"); // weight = None ``` --- url: /website/guide/control/create-view.md --- # Developing a Switch View from Scratch This chapter will guide you through implementing a complete `Switch` view from scratch. Through this process, you will master all core aspects of view development: defining style enums, implementing the `View` trait, drawing, measuring, supporting atomic classes, and responding to mouse events. `Switch` is an ideal learning case: it has "on/off" states, requires drawing a track and thumb, needs to respond to clicks, and can be customized with atomic classes for colors and sizes. After completing this chapter, you can follow the same pattern to develop your own views. ## Final Effect Preview ```rust use flor_lys::switch::switch; let enabled = create_signal(false); // Use atomic classes to customize style switch(enabled).class("switch-track-blue switch-size-md"); // Use style builder to customize style switch(enabled) .style(|s| s .track_color(Color::GREEN) .thumb_color(Color::WHITE) ); ``` *** ## Step 1: Create File Create a new file in your view library crate: ``` flor-lys/crates/flor-lys/src/switch.rs ``` And register it in `lib.rs`: ```rust pub mod switch; ``` *** ## Step 2: Define Style Enum Use `#[derive(Resolver)]` to define the style properties supported by `Switch`. The derive macro will automatically generate helper types like `SwitchStyleKey`, `SwitchStyleResolver`, and `SwitchStyleComputed`. ```rust use flor::macros::Resolver; use flor::types::Color; /// Switch style enum /// /// Each variant corresponds to a configurable style property. /// #[derive(Resolver)] automatically generates for you: /// - SwitchStyleKey — Property key enum /// - SwitchStyleResolver — Resolver type alias /// - SwitchStyleComputed — Computed style value struct /// - SwitchStyleResolverExt — Chain method trait #[derive(Clone, Debug, Resolver)] pub enum SwitchStyle { /// Track color (on state) TrackColor(Color), /// Track color (off state) TrackOffColor(Color), /// Thumb color ThumbColor(Color), /// Thumb color (hover state) ThumbHoverColor(Color), /// Switch width Width(f32), /// Switch height Height(f32), /// Corner radius CornerRadius(f32), /// Opacity Opacity(f32), } ``` **Key Points**: - Each variant carries a concrete type, and the `Resolver` macro generates corresponding chain methods for each variant. - Variant names use `PascalCase`, and generated method names automatically convert to `snake_case` (e.g., `TrackColor` → `.track_color(...)`). - Types in variants are wrapped as `Option` in the `Computed` struct. *** ## Step 3: Define View Struct ```rust use flor::view::ViewId; use flor::signal::RwSignal; /// Switch view /// /// Contains a bool signal to control on/off state. /// All switches hold their own style resolver. #[derive(Debug)] pub struct Switch { /// View unique ID, framework manages view tree through it view_id: ViewId, /// Current on/off state (reactively updated by create_updater) enabled: bool, /// On/off state signal reference (for writing) enabled_signal: RwSignal, /// Style resolver style: SwitchStyleResolver, } ``` **Key Design**: - `enabled: bool` is a snapshot of the current value, automatically kept in sync by the reactive dependency established through `create_updater`. - `enabled_signal: RwSignal` retains the signal reference for writing new values in `on_button_down`. - This split is Flor's recommended pattern: read using the value, write using the signal; when external code modifies the signal, `create_updater` automatically triggers `on_update_state` to update the `enabled` field. *** ## Step 4: Implement View Trait ### 4.1 Must Implement Methods ```rust use flor::view::{View, ControlState}; impl View for Switch { fn view_id(&self) -> ViewId { // Directly return the view_id in the struct, don't create a new ID here self.view_id } fn tag(&self) -> &str { // Debug identifier, just return the view name for now "Switch" } } ``` ### 4.2 on\_update\_state — Reactive Update When external code updates styles or states through signals, the framework calls `on_update_state`. You need to downcast and update internal fields: ```rust use std::any::Any; impl View for Switch { // ... view_id, tag ... fn on_update_state(&mut self, state: Box) { // First handle enabled state update (triggered by create_updater) if let Ok(value) = state.downcast::() { self.enabled = *value; return; } // Handle style update (SwitchStyleUpdate is auto-generated by Resolver macro) if let Ok(update) = state.downcast::() { SwitchStyle::update_view(&mut self.style, *update); } } } ``` > **Reactive Principle**: The constructor uses `create_updater` to register a `bool` update callback. When external code modifies the `enabled` signal (e.g., `enabled.set(true)`), `create_updater` detects the change and automatically calls `view_id.update_state(Box::new(new_value))`, ultimately triggering `on_update_state` here. You don't need to manually pass signals to the view. ### 4.3 on\_measure — Measuring `on_measure` tells the layout system how much space this view needs. For `Switch`, size is primarily determined by style: ```rust use flor::error::Error; use flor::render::FlorRenderer; use flor::taffy::{AvailableSpace, Size, Style}; impl View for Switch { // ... fn on_measure( &mut self, known_dimensions: Size>, _available_space: Size, _style: &Style, control_state: ControlState, _render: &mut FlorRenderer, ) -> Result, Error> { let computed = self.style.get_data_borrow(control_state); // Default size let width = computed.width.unwrap_or(44.0); let height = computed.height.unwrap_or(24.0); Ok(Size { width: known_dimensions.width.unwrap_or(width), height: known_dimensions.height.unwrap_or(height), }) } } ``` **Key Points**: - `known_dimensions` is the known size constraint passed down from the parent container. If the parent sets fixed width/height, you'll receive `Some(...)` here. - Prioritize using `known_dimensions`, only use the view's default value when not available. - `self.style.get_data_borrow(control_state)` gets the computed style for the current view state. ### 4.4 on\_draw — Drawing This is the most core method in view development. `Switch` needs to draw a track (background) and thumb (circle): ```rust use flor::base::graphics::RenderContext; use flor::view::resolver::ComputedLayout; impl View for Switch { // ... fn on_draw( &mut self, render: &mut FlorRenderer, control_state: ControlState, abs_location: (f32, f32), layout: ComputedLayout, ) -> Result<(), Error> { let x = abs_location.0; let y = abs_location.1; let w = layout.size.width; let h = layout.size.height; let computed = self.style.get_data_borrow(control_state); let is_on = self.enabled; // Track color: select based on on/off state let track_color = if is_on { computed.track_color.unwrap_or_else(|| { Color::from_hex_str("#3b82f6").unwrap_or_default() }) } else { computed.track_off_color.unwrap_or_else(|| { Color::from_hex_str("#d1d5db").unwrap_or_default() }) }; let corner_radius = computed.corner_radius.unwrap_or(h / 2.0); let opacity = computed.opacity.unwrap_or(1.0); // Draw track let track_brush = render.create_solid_color_brush( track_color.with_alpha((255.0 * opacity) as u8), None, )?; render.fill_quad(x, y, w, h, &track_brush, Some(corner_radius), None)?; // Calculate thumb position let thumb_size = h - 4.0; // Leave 2px on top and bottom let thumb_x = if is_on { x + w - thumb_size - 3.0 } else { x + 3.0 }; let thumb_y = y + 2.0; // Thumb color: use hover color when hovering let thumb_color = if control_state == ControlState::Hover { computed .thumb_hover_color .unwrap_or_else(|| Color::from_hex_str("#f3f4f6").unwrap_or_default()) } else { computed .thumb_color .unwrap_or_else(|| Color::WHITE) }; let thumb_brush = render.create_solid_color_brush( thumb_color.with_alpha((255.0 * opacity) as u8), None, )?; render.fill_quad( thumb_x, thumb_y, thumb_size, thumb_size, &thumb_brush, Some(thumb_size / 2.0), // Circle None, )?; Ok(()) } } ``` **Drawing Key Points**: - Use the provided `abs_location` (absolute coordinates) and `layout.size` (layout size), don't re-derive them. - Read style values from `computed`, use `unwrap_or` to provide default values. - Draw different states based on `control_state` (normal vs hover vs disabled). - Corner radius value `h / 2.0` creates a "capsule" shape. - Thumb uses `thumb_size / 2.0` as corner radius to create a circle. ### 4.5 on\_update\_class — Atomic Class Support Allow users to set styles through `.class("switch-track-blue")`: ```rust use flor::view::resolver::parse_color; impl View for Switch { // ... fn on_update_class(&mut self, control_state: ControlState, class: &str) -> Result<(), Error> { // Switch to the corresponding state style layer self.style.switch_control_state(control_state); let class = class.trim(); // 1. Track color (on state): switch-track-{color} if let Some(rest) = class.strip_prefix("switch-track-") { if let Some(color) = parse_color(rest) { self.style.set_track_color(color); return Ok(()); } } // 2. Track color (off state): switch-track-off-{color} if let Some(rest) = class.strip_prefix("switch-track-off-") { if let Some(color) = parse_color(rest) { self.style.set_track_off_color(color); return Ok(()); } } // 3. Thumb color: switch-thumb-{color} if let Some(rest) = class.strip_prefix("switch-thumb-") { if let Some(color) = parse_color(rest) { self.style.set_thumb_color(color); return Ok(()); } } // 4. Size: switch-size-{sm|md|lg} match class { "switch-size-sm" => { self.style.set_width(36.0); self.style.set_height(20.0); return Ok(()); } "switch-size-md" => { self.style.set_width(44.0); self.style.set_height(24.0); return Ok(()); } "switch-size-lg" => { self.style.set_width(52.0); self.style.set_height(28.0); return Ok(()); } _ => {} } Ok(()) } } ``` **Atomic Class Parsing Key Points**: - First use `switch_control_state(control_state)` to switch to the current state layer, so subsequent `set_*` calls write to the correct state. - Use `strip_prefix` to match class name prefix, then use shared parsing methods like `parse_color`. - After successful parsing, directly `return Ok(())`, don't let subsequent rules accidentally match. - Unrecognized class names are simply ignored, `on_update_class` is called multiple times, each time handling only one class name. ### 4.6 on\_button\_down — Responding to Click `Switch` toggles state when clicked: ```rust use flor::base::platform::{HandleResult, KeyState, MousePosition}; impl View for Switch { // ... fn on_button_down( &mut self, key_state: KeyState, mouse_position: MousePosition, ) -> Result { // Toggle switch state: write through signal, triggers create_updater callback self.enabled_signal.set(!self.enabled); Ok(HandleResult::Handled) } } ``` > You could also use `on_click` here. The difference is `on_click` requires down and up to hit the same view, while `on_button_down` triggers immediately on press. For switches, `on_button_down` provides a more immediate experience. *** ## Step 5: Constructor and Factory Function ```rust use flor::signal::{RwSignal, create_updater}; use flor::view::resolver::LayoutResolver; use flor::view::builder::ViewBuilder; impl Switch { /// Create switch view pub fn new(enabled: RwSignal) -> Self { // Create ViewId, and create LayoutResolver let view_id = ViewId::new_with_layout(|view_id| { LayoutResolver::new(view_id) }); // Build reactive dependency: when enabled signal changes, automatically call on_update_state let enabled_value = create_updater( move || enabled.get(), move |v: bool| view_id.update_state(Box::new(v)), ); Self { view_id, enabled: enabled_value, enabled_signal: enabled, // computed_switch_style is auto-generated by #[derive(Resolver)] macro style: SwitchStyleResolver::new_with_compute_func(view_id, computed_switch_style), } } } /// Factory function: create Switch #[inline] pub fn switch(enabled: RwSignal) -> Switch { Switch::new(enabled) } ``` **Constructor Key Points**: - `ViewId::new_with_layout` creates a `LayoutResolver` for the view. - `create_updater` is key: it receives two closures—the first reads the signal value (establishing reactive tracking), the second triggers `on_update_state` through `view_id.update_state` when the value changes. The return value is the signal's current value, stored as `enabled: bool`. - `enabled_signal` retains the signal reference for write operations in `on_button_down`. - `computed_switch_style` function is auto-generated by the `#[derive(Resolver)]` macro, you **don't need to write it manually**. *** ## Step 6: Understanding compute Function (Macro Auto-generated) The `#[derive(Resolver)]` macro automatically generates a `computed_switch_style` function by default, which maps the enum's raw style values to the `Computed` struct. You don't need to write it manually, the macro has already generated code equivalent to the following: ```rust // Below is auto-generated by #[derive(Resolver)], here only shows the effect pub fn computed_switch_style( _unit_resolver: &UnitResolver, variants: &ResolverComputeMap, ) -> SwitchStyleComputed { let mut computed = SwitchStyleComputed::default(); for (k, v) in variants.iter() { match k { SwitchStyleKey::TrackColor => { if let SwitchStyle::TrackColor(val) = v { computed.track_color = Some(val.clone()); } } SwitchStyleKey::TrackOffColor => { if let SwitchStyle::TrackOffColor(val) = v { computed.track_off_color = Some(val.clone()); } } // ... other variants same _ => {} } } computed } ``` **Key Points**: - Macro generated compute function name rule is `computed_{EnumName's snake_case}` (e.g., `LabelStyle` → `computed_label_style`). - It only does simple `clone` mapping, doesn't handle unit conversion. Unit px conversion is handled internally by `Resoled`'s `get_data_borrow`. - If you don't need this function, you can disable it via `#[resolver(computed_fn = false)]`. See [Resolver Derive Macro](/website/guide/control/resolver/resolver-derive.md#computed_fn--false) documentation. *** ## Complete File Overview Below is the complete code for `switch.rs`: ```rust use flor::base::graphics::RenderContext; use flor::base::platform::{HandleResult, KeyState, MousePosition}; use flor::error::Error; use flor::macros::Resolver; use flor::render::FlorRenderer; use flor::signal::{RwSignal, create_updater}; use flor::taffy::{AvailableSpace, Size, Style}; use flor::types::Color; use flor::view::builder::ViewBuilder; use flor::view::resolver::{ComputedLayout, LayoutResolver, parse_color}; use flor::view::{ControlState, View, ViewId}; use std::any::Any; // ============================================================================ // SwitchStyle style enum // ============================================================================ #[derive(Clone, Debug, Resolver)] pub enum SwitchStyle { TrackColor(Color), TrackOffColor(Color), ThumbColor(Color), ThumbHoverColor(Color), Width(f32), Height(f32), CornerRadius(f32), Opacity(f32), } // ============================================================================ // Switch struct // ============================================================================ #[derive(Debug)] pub struct Switch { view_id: ViewId, /// Current state value (reactively updated by create_updater) enabled: bool, /// Signal reference (for writing) enabled_signal: RwSignal, style: SwitchStyleResolver, } // ============================================================================ // View trait implementation // ============================================================================ impl View for Switch { fn view_id(&self) -> ViewId { self.view_id } fn tag(&self) -> &str { "Switch" } fn on_update_state(&mut self, state: Box) { // Handle enabled state update (triggered by create_updater) if let Ok(value) = state.downcast::() { self.enabled = *value; return; } // Handle style update if let Ok(update) = state.downcast::() { SwitchStyle::update_view(&mut self.style, *update); } } fn on_measure( &mut self, known_dimensions: Size>, _available_space: Size, _style: &Style, control_state: ControlState, _render: &mut FlorRenderer, ) -> Result, Error> { let computed = self.style.get_data_borrow(control_state); let width = computed.width.unwrap_or(44.0); let height = computed.height.unwrap_or(24.0); Ok(Size { width: known_dimensions.width.unwrap_or(width), height: known_dimensions.height.unwrap_or(height), }) } fn on_draw( &mut self, render: &mut FlorRenderer, control_state: ControlState, abs_location: (f32, f32), layout: ComputedLayout, ) -> Result<(), Error> { let x = abs_location.0; let y = abs_location.1; let w = layout.size.width; let h = layout.size.height; let computed = self.style.get_data_borrow(control_state); let is_on = self.enabled; let track_color = if is_on { computed .track_color .unwrap_or_else(|| Color::from_hex_str("#3b82f6").unwrap_or_default()) } else { computed .track_off_color .unwrap_or_else(|| Color::from_hex_str("#d1d5db").unwrap_or_default()) }; let corner_radius = computed.corner_radius.unwrap_or(h / 2.0); let opacity = computed.opacity.unwrap_or(1.0); let track_brush = render.create_solid_color_brush( track_color.with_alpha((255.0 * opacity) as u8), None, )?; render.fill_quad(x, y, w, h, &track_brush, Some(corner_radius), None)?; let thumb_size = h - 4.0; let thumb_x = if is_on { x + w - thumb_size - 3.0 } else { x + 3.0 }; let thumb_y = y + 2.0; let thumb_color = if control_state == ControlState::Hover { computed .thumb_hover_color .unwrap_or_else(|| Color::from_hex_str("#f3f4f6").unwrap_or_default()) } else { computed.thumb_color.unwrap_or(Color::WHITE) }; let thumb_brush = render.create_solid_color_brush( thumb_color.with_alpha((255.0 * opacity) as u8), None, )?; render.fill_quad( thumb_x, thumb_y, thumb_size, thumb_size, &thumb_brush, Some(thumb_size / 2.0), None, )?; Ok(()) } fn on_update_class(&mut self, control_state: ControlState, class: &str) -> Result<(), Error> { self.style.switch_control_state(control_state); let class = class.trim(); if let Some(rest) = class.strip_prefix("switch-track-") { if let Some(color) = parse_color(rest) { self.style.set_track_color(color); return Ok(()); } } if let Some(rest) = class.strip_prefix("switch-track-off-") { if let Some(color) = parse_color(rest) { self.style.set_track_off_color(color); return Ok(()); } } if let Some(rest) = class.strip_prefix("switch-thumb-") { if let Some(color) = parse_color(rest) { self.style.set_thumb_color(color); return Ok(()); } } match class { "switch-size-sm" => { self.style.set_width(36.0); self.style.set_height(20.0); return Ok(()); } "switch-size-md" => { self.style.set_width(44.0); self.style.set_height(24.0); return Ok(()); } "switch-size-lg" => { self.style.set_width(52.0); self.style.set_height(28.0); return Ok(()); } _ => {} } Ok(()) } fn on_button_down( &mut self, _key_state: KeyState, _mouse_position: MousePosition, ) -> Result { self.enabled_signal.set(!self.enabled); Ok(HandleResult::Handled) } } // ============================================================================ // Constructor and factory function // ============================================================================ impl Switch { pub fn new(enabled: RwSignal) -> Self { let view_id = ViewId::new_with_layout(|view_id| LayoutResolver::new(view_id)); let enabled_value = create_updater( move || enabled.get(), move |v: bool| view_id.update_state(Box::new(v)), ); Self { view_id, enabled: enabled_value, enabled_signal: enabled, style: SwitchStyleResolver::new_with_compute_func(view_id, computed_switch_style), } } } #[inline] pub fn switch(enabled: RwSignal) -> Switch { Switch::new(enabled) } ``` *** ## Usage Examples ### Basic Usage ```rust use flor::signal::create_signal; use flor_lys::switch::switch; let notifications = create_signal(false); switch(notifications); ``` ### With Atomic Classes ```rust switch(notifications) .class("switch-track-blue switch-size-lg"); ``` ### With Signal Linkage ```rust let dark_mode = create_signal(false); // Other views can read the dark_mode signal to respond let bg_color = move || { if dark_mode.get() { Color::from_hex_str("#1a1a2e").unwrap() } else { Color::WHITE } }; let panel = div(views![ label("Dark Mode"), switch(dark_mode).class("switch-track-purple"), ]) .class("flex items-center gap-3 p-4"); ``` ### With Style Builder ```rust switch(dark_mode) .style(|s| s .track_color(Color::GREEN) .track_off_color(Color::GRAY) .thumb_color(Color::WHITE) ); ``` *** ## View Development Checklist Review the entire process, developing a Flor view requires completing these steps: 1. **Define Style Enum** — Use `#[derive(Resolver)]` to declare all configurable style properties. 2. **Define View Struct** — Hold `view_id`, state fields, and `style resolver`. 3. **Implement `View` trait**: - `view_id()` — Return the `view_id` in the struct. - `tag()` — Return the debug label name. - `on_update_state()` — Handle style updates passed through `update_state`. - `on_measure()` — Views with natural size need to implement this, return the size the view needs. - `on_draw()` — Core drawing method, read computed style and view state to draw. - `on_update_class()` — Parse atomic class strings, convert to style updates. - Override mouse/keyboard event handling as needed (`on_button_down`, `on_click`, etc.). 4. **Write Constructor** — Create `ViewId`, `LayoutResolver`, `StyleResolver`, use `create_updater` to build reactive dependency, `computed_xxx` function is auto-generated by macro. 5. **Write Factory Function** — Provide concise API entry. *** ## Advanced Topics ### Adding Animation If you want to add translation animation to the thumb, you can implement it in `on_frame`: ```rust fn on_frame(&mut self, now: Instant) -> Result, Error> { let target_x = if self.enabled { /* on position */ } else { /* off position */ }; // Calculate current transition position // If position is still changing, return Some(Duration) to request next frame // If already reached target, return None } ``` ### Supporting Disabled State In `on_draw`, check `ControlState::Disabled`, draw gray track and semi-transparent thumb. In `on_button_down`, check `self.view_id.control_state() == ControlState::Disabled` and ignore clicks. ### Expanding Click Hot Area If your thumb is too small to click easily, override `on_hit_test` to expand the hit area: ```rust fn on_hit_test(&self, mouse_position: MousePosition, _key_state: KeyState) -> bool { // Expand 4px on the layout rectangle basis let padding = 4.0; mouse_position.x >= -padding && mouse_position.x <= self.layout_width() + padding && mouse_position.y >= -padding && mouse_position.y <= self.layout_height() + padding } ``` --- url: /website/api/index.md --- # API Overview ## Class Syntax Basics ### [Class Syntax Basics](/website/api/class-syntax.md) - [Entry Point](/website/api/class-syntax.md#entry-point) - [Splitting Rules](/website/api/class-syntax.md#splitting-rules) - [State Prefixes](/website/api/class-syntax.md#state-prefixes) - [Arbitrary Value Syntax](/website/api/class-syntax.md#arbitrary-value-syntax) - [Length Values](/website/api/class-syntax.md#length-values) - [Keywords](/website/api/class-syntax.md#keywords) - [Color Values](/website/api/class-syntax.md#color-values) - [Shared Style Helper Syntax](/website/api/class-syntax.md#shared-style-helper-syntax) - [Processing Path](/website/api/class-syntax.md#processing-path) ## Layout Class Syntax ### [Layout Class Syntax](/website/api/layout-class.md) - [Value Syntax](/website/api/layout-class.md#value-syntax) - [Display](/website/api/layout-class.md#display) - [Position](/website/api/layout-class.md#position) - [Box Sizing](/website/api/layout-class.md#box-sizing) - [Z Index](/website/api/layout-class.md#z-index) - [Overflow](/website/api/layout-class.md#overflow) - [Size](/website/api/layout-class.md#size) - [Padding](/website/api/layout-class.md#padding) - [Margin](/website/api/layout-class.md#margin) - [Inset](/website/api/layout-class.md#inset) - [Border Width](/website/api/layout-class.md#border-width) - [Gap](/website/api/layout-class.md#gap) - [Flex](/website/api/layout-class.md#flex) - [Flex / Grid Shared Alignment](/website/api/layout-class.md#flex--grid-shared-alignment) - [Grid](/website/api/layout-class.md#grid) - [Block Text Align](/website/api/layout-class.md#block-text-align) - [Aspect Ratio](/website/api/layout-class.md#aspect-ratio) - [Scrollbar Width](/website/api/layout-class.md#scrollbar-width) - [State Prefixes](/website/api/layout-class.md#state-prefixes) - [Merge Behavior](/website/api/layout-class.md#merge-behavior) - [Currently Not Supported](/website/api/layout-class.md#currently-not-supported) ## Signal API ### [Signal API](/website/api/signal.md) - [`Signal`](/website/api/signal.md#signal) - [`Read`](/website/api/signal.md#readt) - [`Write`](/website/api/signal.md#writet) - [`RwSignal`](/website/api/signal.md#rwsignalt) - [`ListRead`](/website/api/signal.md#listreadt) - [`ListWrite`](/website/api/signal.md#listwritet) - [Creation Functions](/website/api/signal.md#creation-functions) ## View Conversion API ### [View Conversion API](/website/api/view.md) - [Imports](/website/api/view.md#imports) - [`ViewBox`](/website/api/view.md#viewbox) - [`ViewIdentity`](/website/api/view.md#viewidentity) - [`IntoView`](/website/api/view.md#intoview) - [`IntoViewIter`](/website/api/view.md#intoviewiter) - [`ViewBuilder`](/website/api/view.md#viewbuilder) - [Macros](/website/api/view.md#macros) ## Handler API ### [Handler API](/website/api/handler.md) - [Imports](/website/api/handler.md#imports) - [`EventBuilder`](/website/api/handler.md#eventbuilder) - [`IntoEventHandler`](/website/api/handler.md#intoeventhandler) - [Current Dispatch Status](/website/api/handler.md#current-dispatch-status) - [`ViewHandler`](/website/api/handler.md#viewhandler) - [Handler Wrapper Types](/website/api/handler.md#handler-wrapper-types) - [Type Aliases](/website/api/handler.md#type-aliases) - [Parameter Descriptions](/website/api/handler.md#parameter-descriptions) --- url: /website/api/class-syntax.md --- # Class Syntax Basics This page describes the general parsing rules for `.class(...)` strings. For usage instructions, see [Atomic Class Support](/website/guide/use/builder/class.md). For the complete list of layout classes, see [Layout Class Syntax](/website/api/layout-class.md). ## Entry Point ```rust use flor::view::builder::ClassBuilder; view.class("flex gap-2 p-4"); ``` `ClassBuilder` requires the `class` feature to be enabled. There is only one public entry point: ```rust pub trait ClassBuilder { fn class(self, class_str: M) -> Self; } ``` `M` needs to implement `ClassProp`. Currently supported inputs include: | Input | Example | | | | -------------------------- | ------------------------------------ | - | ------------------------------------------------------------ | | `&'static str` | `.class("flex gap-2")` | | | | `String` | `.class(format!("w-[{}px]", width))` | | | | `Fn() -> String + 'static` | \`.class(move | | if open.get() { "block".into() } else { "hidden".into() })\` | The closure form is re-evaluated by the reactive updater, suitable for scenarios where the class string depends on signals. ## Splitting Rules The input string is split by whitespace characters: ```text "flex flex-col gap-2 p-4" ``` Will be split into: ```text flex flex-col gap-2 p-4 ``` There is no quote escaping or preserving spaces within class tokens. If special characters are needed in values, use the bracket arbitrary value syntax. ## State Prefixes Class tokens can have a state prefix: | Prefix | State | | ----------- | -------- | | No prefix | Normal | | `hover:` | Hover | | `focus:` | Focus | | `active:` | Active | | `disabled:` | Disabled | Example: ```rust .class("p-2 hover:p-4 focus:bg-blue-50 disabled:opacity-50") ``` State prefixes are only parsed one level deep. Combined prefixes like `md:hover:p-4` or `hover:focus:p-4` are not currently parsed as multiple states. ## Arbitrary Value Syntax Many classes support bracket notation: ```text w-[320px] rounded-[10px] text-[#0f172a] aspect-[16/9] ``` The parser removes the outer `[` and `]`, then parses the value according to the corresponding property's syntax. Brackets are not supported for all classes; check the specific class documentation or view implementation. ## Length Values Layout classes and some view style classes use unified length parsing. | Syntax | Example | Description | | ----------- | -------- | ------------------------------------------------------------------------------------------------------------------- | | Bare number | `4` | Parsed according to Tailwind spacing conventions, `4` equals `1rem`. With default `1rem = 16px`, `4` equals `16px`. | | `px` | `12px` | Pixels. | | `rem` | `1.5rem` | Uses the current window's `WindowOption::rem_px`, default `16.0`. | | `pt` | `12pt` | Uses current window DPI, formula is `1pt = dpi / 72 px`. | | `vw` | `50vw` | Percentage of current window client area width. | | `vh` | `50vh` | Percentage of current window client area height. | | Percentage | `50%` | Parsed as percentage. | | Fraction | `1/2` | Converted to percentage. | Examples: ```text p-4 w-1/2 h-[240px] mt-[1rem] max-h-[80vh] ``` ## Keywords Different properties support different keywords. Common keywords include: | Keyword | Common Usage | | --------------------- | ------------------------------------------- | | `auto` | `Dimension::Auto` or auto margin. | | `full` | 100%. | | `screen` | Current window client area width or height. | | `fit` / `min` / `max` | Treated as auto in current size parsing. | For the scope of layout class support for these keywords, see [Layout Class Syntax](/website/api/layout-class.md). For view style class support, see the view library documentation for specific views. ## Color Values View style classes using Flor's shared color parsing typically support: | Syntax | Example | | ---------------- | ---------------------------------- | | Keywords | `transparent`, `black`, `white` | | Hexadecimal | `#fff`, `#ffffff` | | Bracket hex | `[#0f172a]` | | Tailwind palette | `slate-900`, `blue-600`, `red-500` | Whether color parsing is used for a specific class is determined by the view. For example, whether `text-blue-600` or `bg-slate-100` works depends on whether the current view implements the corresponding style class. ## Shared Style Helper Syntax Some views reuse these parsing functions: | Syntax | Example | Description | | --------------- | -------------------------------------------------------------------------------------------------------------------- | ----------------------------- | | `rounded` | `rounded` | 4px corner radius. | | `rounded-*` | `rounded-none`, `rounded-sm`, `rounded-md`, `rounded-lg`, `rounded-xl`, `rounded-2xl`, `rounded-3xl`, `rounded-full` | Common corner radius presets. | | `rounded-[Npx]` | `rounded-[10px]` | Custom corner radius. | | `font-*` | `font-thin`, `font-light`, `font-normal`, `font-medium`, `font-semibold`, `font-bold`, `font-black` | Font weight. | These are shared parsing capabilities, not automatically supported by all views. Views must use these parsing results in their own class update logic. ## Processing Path Class names passed to `.class(...)` go through these user-visible processing steps: 1. Split by whitespace into multiple class tokens. 2. `z-*` is handled as view layer level. 3. Other tokens are split into state and actual class name by state prefix. 4. The actual class name is passed to the view's style parsing logic. 5. The same batch of classes is also passed to the layout class parsing logic. For specific layout class support, see [Layout Class Syntax](/website/api/layout-class.md). For view style class support, see the view documentation or view source code. --- url: /website/api/layout-class.md --- # Layout Class Syntax This page lists the current support scope of Flor's built-in layout classes. For `.class(...)` usage instructions, see [Atomic Class Support](/website/guide/use/builder/class.md). For general class string parsing rules, see [Class Syntax Basics](/website/api/class-syntax.md). Layout classes are uniformly parsed by Flor and available for all views. Flex, Grid, Block related classes are controlled by features. ## Value Syntax The `*` in the table below represents a value. For general value syntax, see [Class Syntax Basics](/website/api/class-syntax.md#length-values). | Syntax | Example | Description | | ----------- | ------------------------------------------------ | --------------------------------------------------------------------- | | Bare number | `4` | Parsed according to Tailwind spacing conventions, default `4 = 16px`. | | With unit | `[12px]`, `[1rem]`, `[12pt]`, `[50vw]`, `[50vh]` | Supports `px`, `rem`, `pt`, `vw`, `vh`. | | Percentage | `50%`, `[50%]` | Converted to percentage. | | Fraction | `1/2`, `1/3` | Converted to percentage. | | Keywords | `auto`, `full`, `screen` | Interpreted by specific property. | ## Display | Class | Effect | feature | | -------- | ---------------- | -------------- | | `hidden` | `Display::None` | None | | `flex` | `Display::Flex` | `layout-flex` | | `grid` | `Display::Grid` | `layout-grid` | | `block` | `Display::Block` | `layout-block` | ## Position | Class | Effect | | ---------- | -------------------- | | `relative` | `Position::Relative` | | `absolute` | `Position::Absolute` | Currently `fixed` and `sticky` are not supported. ## Box Sizing | Class | Effect | | ------------- | ----------------------- | | `box-border` | `BoxSizing::BorderBox` | | `box-content` | `BoxSizing::ContentBox` | ## Z Index `z-*` is handled as layer level in `.class(...)` entry. | Class | Effect | | -------- | -------------------- | | `z-auto` | z-index set to `0`. | | `z-10` | z-index set to `10`. | | `z--1` | z-index set to `-1`. | `z-*` currently does not go through state prefix parsing; for example `hover:z-10` is not processed as hover layer level. ## Overflow | Class | Effect | | ------------------------------------------------------------------------------------ | ----------------------------- | | `overflow-visible` | x/y both `Overflow::Visible`. | | `overflow-hidden` | x/y both `Overflow::Hidden`. | | `overflow-clip` | x/y both `Overflow::Clip`. | | `overflow-scroll` | x/y both `Overflow::Scroll`. | | `overflow-x-visible` / `overflow-x-hidden` / `overflow-x-clip` / `overflow-x-scroll` | Only sets x direction. | | `overflow-y-visible` / `overflow-y-hidden` / `overflow-y-clip` / `overflow-y-scroll` | Only sets y direction. | ## Size | Class Pattern | Effect | Supported Values | | --------------------------------------- | ------------------------------------ | ---------------------------------------------------------------------------------------------- | | `w-*` | Width | `auto`, `full`, `fit`, `min`, `max`, numbers, fractions, percentages, arbitrary length values. | | `h-*` | Height | Same as `w-*`. | | `size-*` | Sets both width and height | Same as `w-*`. | | `w-screen` / `h-screen` / `size-screen` | Uses current window client area size | `screen`. | | `min-w-*` / `min-h-*` | Minimum width / minimum height | Same as `w-*`, also supports `screen`. | | `max-w-*` / `max-h-*` | Maximum width / maximum height | Same as `w-*`, also supports `screen`. | Examples: ```text w-full h-auto w-1/2 min-w-[240px] max-h-[80vh] size-10 ``` ## Padding | Class Pattern | Effect | | ------------- | ------------------- | | `p-*` | Four-side padding. | | `px-*` | Left/right padding. | | `py-*` | Top/bottom padding. | | `pl-*` | Left padding. | | `pr-*` | Right padding. | | `pt-*` | Top padding. | | `pb-*` | Bottom padding. | Supports numbers, percentages, fractions, `full` and arbitrary length values. ```text p-4 px-2 pt-[10px] pl-1/2 ``` ## Margin | Class Pattern | Effect | | ------------- | ------------------ | | `m-*` | Four-side margin. | | `mx-*` | Left/right margin. | | `my-*` | Top/bottom margin. | | `ml-*` | Left margin. | | `mr-*` | Right margin. | | `mt-*` | Top margin. | | `mb-*` | Bottom margin. | Supports `auto`, numbers, percentages, fractions, `full` and arbitrary length values. ```text m-4 mx-auto mt-2 mb-[20px] ``` ## Inset Inset classes are typically used with `absolute` or `relative`. | Class Pattern | Effect | | ------------- | ------------------ | | `inset-*` | Four-side offset. | | `inset-x-*` | Left/right offset. | | `inset-y-*` | Top/bottom offset. | | `left-*` | Left offset. | | `right-*` | Right offset. | | `top-*` | Top offset. | | `bottom-*` | Bottom offset. | Supports `auto`, numbers, percentages, fractions, `full` and arbitrary length values. ```text inset-0 top-4 left-1/2 right-[10px] ``` ## Border Width Border classes here only set layout-level border thickness, not border color or corner radius. | Class Pattern | Effect | | --------------------------------------------------------- | ---------------------------------- | | `border` | Four-side thickness 1px. | | `border-*` | Four-side thickness. | | `border-x-*` / `border-y-*` | Left/right / top/bottom thickness. | | `border-l-*` / `border-r-*` / `border-t-*` / `border-b-*` | Single side thickness. | ```text border border-2 border-b-[1px] ``` ## Gap Requires `layout-flex` or `layout-grid` feature. | Class Pattern | Effect | | ------------- | ---------------------------- | | `gap-*` | Spacing for both directions. | | `gap-x-*` | x direction spacing. | | `gap-y-*` | y direction spacing. | ```text gap-4 gap-x-2 gap-y-[10px] ``` ## Flex Requires `layout-flex` feature. | Class | Effect | | --------------------------- | ---------------------------------------- | | `flex-row` | `FlexDirection::Row` | | `flex-row-reverse` | `FlexDirection::RowReverse` | | `flex-col` | `FlexDirection::Column` | | `flex-col-reverse` | `FlexDirection::ColumnReverse` | | `flex-wrap` | `FlexWrap::Wrap` | | `flex-wrap-reverse` | `FlexWrap::WrapReverse` | | `flex-nowrap` | `FlexWrap::NoWrap` | | `flex-1` | `grow: 1`, `shrink: 1`, `basis: 0%` | | `flex-auto` | `grow: 1`, `shrink: 1`, `basis: auto` | | `flex-initial` | `grow: 0`, `shrink: 1`, `basis: auto` | | `flex-none` | `grow: 0`, `shrink: 0`, `basis: auto` | | `grow` | `FlexGrow(1.0)` | | `grow-*` / `grow-[1.5]` | Custom grow value. | | `shrink` | `FlexShrink(1.0)` | | `shrink-*` / `shrink-[0.5]` | Custom shrink value. | | `basis-*` | `FlexBasis`, value parsed as size value. | ## Flex / Grid Shared Alignment Requires `layout-flex` or `layout-grid` feature. | Class | Effect | | ----------------- | ------------------------------ | | `items-start` | `AlignItems::Start` | | `items-end` | `AlignItems::End` | | `items-center` | `AlignItems::Center` | | `items-baseline` | `AlignItems::Baseline` | | `items-stretch` | `AlignItems::Stretch` | | `self-start` | `AlignSelf::Start` | | `self-end` | `AlignSelf::End` | | `self-center` | `AlignSelf::Center` | | `self-baseline` | `AlignSelf::Baseline` | | `self-stretch` | `AlignSelf::Stretch` | | `justify-start` | `JustifyContent::Start` | | `justify-end` | `JustifyContent::End` | | `justify-center` | `JustifyContent::Center` | | `justify-between` | `JustifyContent::SpaceBetween` | | `justify-around` | `JustifyContent::SpaceAround` | | `justify-evenly` | `JustifyContent::SpaceEvenly` | | `justify-stretch` | `JustifyContent::Stretch` | | `content-start` | `AlignContent::Start` | | `content-end` | `AlignContent::End` | | `content-center` | `AlignContent::Center` | | `content-between` | `AlignContent::SpaceBetween` | | `content-around` | `AlignContent::SpaceAround` | | `content-evenly` | `AlignContent::SpaceEvenly` | | `content-stretch` | `AlignContent::Stretch` | ## Grid Requires `layout-grid` feature. | Class | Effect | | --------------------------- | --------------------------------------------------- | | `grid-flow-row` | `GridAutoFlow::Row` | | `grid-flow-col` | `GridAutoFlow::Column` | | `grid-flow-row-dense` | `GridAutoFlow::RowDense` | | `grid-flow-col-dense` | `GridAutoFlow::ColumnDense` | | `row-start-*` / `row-end-*` | Sets grid row start/end lines, value is integer. | | `col-start-*` / `col-end-*` | Sets grid column start/end lines, value is integer. | | `row-span-*` | Sets row span, value is positive integer. | | `col-span-*` | Sets column span, value is positive integer. | | `justify-items-start` | `JustifyItems::Start` | | `justify-items-end` | `JustifyItems::End` | | `justify-items-center` | `JustifyItems::Center` | | `justify-items-stretch` | `JustifyItems::Stretch` | | `justify-self-start` | `JustifySelf::Start` | | `justify-self-end` | `JustifySelf::End` | | `justify-self-center` | `JustifySelf::Center` | | `justify-self-stretch` | `JustifySelf::Stretch` | Currently `grid-cols-*`, `grid-rows-*`, `auto-cols-*`, `auto-rows-*` are not supported. ## Block Text Align Requires `layout-block` feature. | Class | Effect | | ------------- | ------------------------- | | `text-left` | `TextAlign::LegacyLeft` | | `text-center` | `TextAlign::LegacyCenter` | | `text-right` | `TextAlign::LegacyRight` | Note: These are block layout text/inline alignment classes. Specific views may also parse `text-*` as view style, such as text color or font size. ## Aspect Ratio | Class | Effect | | --------------- | --------------- | | `aspect-square` | 1:1. | | `aspect-video` | 16:9. | | `aspect-[N]` | Custom ratio. | | `aspect-[N/M]` | Fraction ratio. | ```text aspect-square aspect-video aspect-[4/3] ``` ## Scrollbar Width | Class Pattern | Effect | | ------------- | ------------------------------------- | | `scrollbar-*` | Sets `ScrollbarWidth`, value uses px. | ```text scrollbar-0 scrollbar-4 scrollbar-[10px] ``` ## State Prefixes Layout classes support general state prefixes: ```text p-4 hover:p-6 focus:p-8 ``` State prefix rules see [Class Syntax Basics](/website/api/class-syntax.md#state-prefixes). `z-*` is an exception: it is handled as layer level before entering state prefix parsing, so `hover:z-10` is currently not supported. ## Merge Behavior The same batch of classes is parsed in declaration order: ```text p-4 pl-2 pt-1 ``` Result is left overridden by `pl-2`, top overridden by `pt-1`, other sides retain `p-4`. ```text p-4 p-8 ``` Result is `p-8` overriding `p-4`. Multiple calls to `.class(...)` create multiple layers; later created layers override previous layers on the same layout property. ## Currently Not Supported Here are common but currently unimplemented syntax in layout classes: | Syntax | Status | | -------------------------------------------------- | ---------------------------------------------------------------------------------------- | | `fixed`, `sticky` | Not supported. | | `grid-cols-*`, `grid-rows-*` | Not supported. | | `auto-cols-*`, `auto-rows-*` | Not supported. | | `order-*` | Not supported. | | `place-content-*`, `place-items-*`, `place-self-*` | Not supported. | | `space-x-*`, `space-y-*` | Not supported. | | Responsive prefixes `sm:`, `md:`, `lg:` | Not supported. | | Negative value prefix `-m-4`, `-top-2` | Not supported; use builder or supplement implementation when negative values are needed. | --- url: /website/api/signal.md --- # Signal API This page is for querying the main public APIs of `flor::signal`. For usage instructions, see [Signal Reactive System](/website/guide/use/signal.md). ## `Signal` ```rust trait Signal { fn id(&self) -> Id; fn exists(&self) -> bool; fn destroy(&self); } ``` ## `Read` ```rust trait Read: Signal { fn track(&self); fn try_get(&self) -> Option where T: Clone + 'static; fn get(&self) -> T where T: Clone + 'static; fn get_ref(&self) -> SignalRef<'_, T>; fn try_get_ref(&self) -> Option>; } ``` ## `Write` ```rust trait Write: Signal { fn set(&self, new_value: T) where T: 'static; fn try_set(&self, new_value: T) -> bool where T: 'static; fn update(&self, f: impl FnOnce(&mut T)) where T: 'static; fn try_update(&self, f: impl FnOnce(&mut T)) -> bool where T: 'static; } ``` ## `RwSignal` `RwSignal` is usually returned by `create_signal` or `create_signal_with_label`. Business code generally doesn't need to manually construct signal handles from `Id`. ```rust impl RwSignal { fn split(self) -> (ReadSignal, WriteSignal); fn as_read(&self) -> ReadSignal; fn as_write(&self) -> WriteSignal; fn set_label(&self, label: &str); } ``` ## `ListRead` ```rust trait ListRead: Signal { fn track(&self); fn len(&self) -> Option; fn len_or_zero(&self) -> usize; fn is_empty(&self) -> bool; fn contains(&self, value: &T) -> bool where T: PartialEq + 'static; fn try_contains(&self, value: &T) -> Option where T: PartialEq + 'static; fn try_get(&self, index: usize) -> Option where T: Clone + 'static; fn get(&self, index: usize) -> T where T: Clone + 'static; fn to_vec(&self) -> Vec where T: Clone + 'static; fn try_to_vec(&self) -> Option> where T: Clone + 'static; fn for_each_ref(&self, f: F) -> Option<()> where F: FnMut(&T), T: 'static; fn try_borrow(&self) -> Option> where T: 'static; } ``` ## `ListWrite` ```rust trait ListWrite: Signal { fn track(&self); fn push(&self, value: T) where T: 'static; fn try_push(&self, value: T) -> bool where T: 'static; fn set(&self, index: usize, value: T) where T: 'static; fn try_set(&self, index: usize, value: T) -> bool where T: 'static; fn insert(&self, index: usize, value: T) where T: 'static; fn try_insert(&self, index: usize, value: T) -> bool where T: 'static; fn remove(&self, index: usize) -> T where T: 'static; fn try_remove(&self, index: usize) -> Option where T: 'static; fn clear(&self); fn try_clear(&self) -> bool; fn update(&self, index: usize, f: impl FnOnce(&mut T)) where T: 'static; fn try_update(&self, index: usize, f: impl FnOnce(&mut T)) -> bool where T: 'static; } ``` ## Creation Functions ```rust fn create_signal(value: T) -> RwSignal; fn create_rw_signal(value: T) -> (ReadSignal, WriteSignal); fn create_signal_with_label(value: T, label: &str) -> RwSignal; fn create_rw_signal_with_label( value: T, label: &str, ) -> (ReadSignal, WriteSignal); fn create_list_signal(value: Vec) -> RwListSignal; fn create_rw_list_signal( value: Vec, ) -> (ReadListSignal, WriteListSignal); fn create_list_signal_with_label( value: Vec, label: &str, ) -> RwListSignal; fn create_rw_list_signal_with_label( value: Vec, label: &str, ) -> (ReadListSignal, WriteListSignal); fn create_effect(f: impl Fn(Option) -> T + 'static) where T: Any + 'static; fn create_updater( compute: impl Fn() -> R + 'static, on_change: impl Fn(R) + 'static, ) -> R where R: 'static; fn create_updater_with_id( compute: impl Fn() -> R + 'static, on_change: impl Fn(R) + 'static, ) -> (Id, R) where R: 'static; fn batch(f: impl Fn()); ``` --- url: /website/api/view.md --- # View Conversion API This page documents the public APIs in `flor::view` related to view identity, generic view objects, and child-view sequences. For usage-oriented guidance, see [Framework DSL](/website/guide/use/framework-dsl.md). ## Imports ```rust use flor::view::{ IntoView, IntoViewIter, View, ViewBox, ViewIdentity, }; use flor::view::builder::ViewBuilder; ``` ## `ViewBox` `ViewBox` is the generic view object commonly used inside the framework and by container APIs: ```rust pub type ViewBox = Box; ``` The `view!(...)` macro converts a single view into `ViewBox`, and the `views![...]` macro generates `Vec`. ## `ViewIdentity` `ViewIdentity` is the abstraction builders use to read view identity: ```rust pub trait ViewIdentity { fn identity(&self) -> ViewId; } ``` Currently both `View` and `ViewBox` implement `ViewIdentity`. Therefore builders based on `ViewIdentity` can be used not only with concrete views, but also with generic view objects and view wrappers whose return type is `impl IntoView`. Common builders such as `ClassBuilder`, `LayoutBuilder`, `EventBuilder`, `FocusIndexBuilder`, `DisableBuilder`, `TransformBuilder`, and `ZIndexBuilder` all use `ViewIdentity` to locate the target view. ## `IntoView` `IntoView` converts a single view into `ViewBox`: ```rust pub trait IntoView: ViewIdentity + Send + Sync + 'static { fn into_view(self) -> ViewBox; } ``` Implementations: | Type | Behavior | | --------------------------------- | --------------------------------------------------- | | `T: View + Send + Sync + 'static` | Wraps it as `Box` | | `ViewBox` | Returns it unchanged | `IntoView` inherits `ViewIdentity`. If a function returns `impl IntoView`, the caller can still chain common builders: ```rust use flor::view::IntoView; use flor::view::builder::{ClassBuilder, EventBuilder}; use flor_lys::button::button; fn action(text: &'static str) -> impl IntoView { button(text).class("px-2") } let save = action("Save") .class("font-bold") .on_click(|| { println!("save"); }); ``` ## `IntoViewIter` `IntoViewIter` converts a value into an iterator of `ViewBox`. It is mainly used by container children, window root views, and `ViewBuilder::views`: ```rust pub trait IntoViewIter { type Iter: Iterator; fn into_view_iter(self) -> Self::Iter; } ``` Implementations: | Type | Behavior | | -------------- | -------------------------------------------------- | | `T: IntoView` | Converts into an iterator containing one `ViewBox` | | `Vec` | Consumes the list directly | This means APIs accepting `impl IntoViewIter` can accept both a single view and the view list generated by `views![...]`: ```rust use flor::views; use flor_lys::button::button; use flor_lys::div::div; use flor_lys::label::label; let single = div(label("Only one child")); let multiple = div(views![ label("Title"), button("Confirm"), ]); ``` ## `ViewBuilder` `ViewBuilder` appends child views to an existing view: ```rust pub trait ViewBuilder { fn views(self, views: impl IntoViewIter) -> Self; fn push_view(self, view: impl IntoView) -> Self; } ``` `views(...)` accepts a child-view sequence. Because a single view also implements `IntoViewIter`, it can accept either one view or a `views![...]` list. `push_view(...)` appends one child view. ```rust use flor::view::builder::ViewBuilder; use flor::views; use flor_lys::button::button; use flor_lys::div::div; use flor_lys::label::label; let panel = div(views![label("Title")]) .push_view(button("Save")) .views(label("Additional note")); ``` When passing initial children while creating a container, prefer the container constructor or `views![...]`. `ViewBuilder` is more suitable after you already have the parent view value. ## Macros | Macro | Return Value | Purpose | | ----------------- | -------------- | ---------------------------------------------- | | `view!(child)` | `ViewBox` | Boxes a single view into a generic view object | | `views![a, b, c]` | `Vec` | Builds a list of multiple child views | The macros call `IntoView::into_view(...)` internally, so each element must implement `IntoView`. --- url: /website/api/handler.md --- # Handler API This page is for querying the main public APIs of `flor::view::handler` and `flor::view::builder::EventBuilder`. For usage instructions, see [External Events](/website/guide/use/handler.md). ## Imports ```rust use flor::view::builder::EventBuilder; use flor::view::handler::*; use flor::view::ViewId; use flor::base::platform::{ HandleResult, KeyCode, KeyState, MousePosition, ScrollAxis, }; ``` After enabling drag-drop or theme features, you will also need these types: ```rust use flor::base::platform::{DragData, DragFormat, DropEffect}; use flor::base::platform::ThemeMode; ``` ## `EventBuilder` `EventBuilder` is implemented for all `V: ViewIdentity`. All methods save the handler and return `Self` for chain calls. Normal views, `ViewBox`, and values returned as `impl IntoView` can all keep using these methods. ```rust pub trait EventBuilder { fn on_mouse_move(self, handler: impl IntoEventHandler) -> Self; fn on_double_click(self, handler: impl IntoEventHandler) -> Self; fn on_click(self, handler: impl IntoEventHandler) -> Self; fn on_button_down(self, handler: impl IntoEventHandler) -> Self; fn on_button_up(self, handler: impl IntoEventHandler) -> Self; fn on_right_button_double_click( self, handler: impl IntoEventHandler, ) -> Self; fn on_right_button_click(self, handler: impl IntoEventHandler) -> Self; fn on_right_button_down(self, handler: impl IntoEventHandler) -> Self; fn on_right_button_up(self, handler: impl IntoEventHandler) -> Self; fn on_middle_button_double_click( self, handler: impl IntoEventHandler, ) -> Self; fn on_middle_button_down(self, handler: impl IntoEventHandler) -> Self; fn on_middle_button_up(self, handler: impl IntoEventHandler) -> Self; fn on_context_menu(self, handler: impl IntoEventHandler) -> Self; fn on_key_down(self, handler: impl IntoEventHandler) -> Self; fn on_key_up(self, handler: impl IntoEventHandler) -> Self; fn on_mouse_enter(self, handler: impl IntoEventHandler) -> Self; fn on_mouse_leave(self, handler: impl IntoEventHandler) -> Self; fn on_focus(self, handler: impl IntoEventHandler) -> Self; fn on_blur(self, handler: impl IntoEventHandler) -> Self; fn on_create(self, handler: impl IntoEventHandler) -> Self; fn on_destroy(self, handler: impl IntoEventHandler) -> Self; fn on_resize(self, handler: impl IntoEventHandler) -> Self; fn on_close_requested(self, handler: impl IntoEventHandler) -> Self; fn on_work_area_changed(self, handler: impl IntoEventHandler) -> Self; fn on_wheel_settings_changed( self, handler: impl IntoEventHandler, ) -> Self; fn on_dpi_change(self, handler: impl IntoEventHandler) -> Self; #[cfg(feature = "theme-change")] fn on_theme_changed(self, handler: impl IntoEventHandler) -> Self; #[cfg(feature = "drag-drop")] fn on_drag_enter(self, handler: impl IntoEventHandler) -> Self; #[cfg(feature = "drag-drop")] fn on_drag_over(self, handler: impl IntoEventHandler) -> Self; #[cfg(feature = "drag-drop")] fn on_drag_leave(self, handler: impl IntoEventHandler) -> Self; #[cfg(feature = "drag-drop")] fn on_drop(self, handler: impl IntoEventHandler) -> Self; } ``` ## `IntoEventHandler` `IntoEventHandler` is the conversion entry used by event builders. It converts closures, normal functions, associated functions, method items, or generic functions with already specified generic arguments into the target handler wrapper type. ```rust pub trait IntoEventHandler { fn into_event_handler(self) -> T; } ``` The conversion has two layers: | Layer | Mechanism | | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | Full arguments | The handler wrapper type implements `From`, and `IntoEventHandler` calls that `From` conversion through `F: Into` | | Simplified arguments | `IntoEventHandler` has implementations that fill omitted arguments for forms such as no arguments, `ViewId` only, or without `ViewId` | So the full-argument form is driven by `From` conversion, and Event Builder uses `IntoEventHandler` to unify different parameter forms into one entry. `Args` is a compile-time marker used to distinguish parameter forms; Rust usually infers it when you call `.on_click(...)`, `.on_key_down(...)`, and similar methods. For example, the underlying type of `on_click` is `MouseHandler`. All four forms below can convert into `OnClickHandler`: ```rust view.on_click(|view_id, key_state, mouse_position| { /* full arguments */ }); view.on_click(|| { /* does not need event parameters */ }); view.on_click(|view_id| { /* only cares about the view */ }); view.on_click(|key_state, mouse_position| { /* omits ViewId */ }); ``` `Handler` alias events whose complete parameter list is only `ViewId`, such as `on_mouse_enter`, `on_mouse_leave`, `on_create`, `on_destroy`, and `on_resize`, do not have an "omit `ViewId` but keep event data" form. They only support full arguments and no arguments. Functions and method items participate in the same conversion as long as they satisfy the corresponding `Fn(...) + Send + Sync + 'static` signature: ```rust fn clicked(view_id: ViewId) { println!("{view_id}"); } struct Actions; impl Actions { fn open(view_id: ViewId) { println!("open {SLOT}: {view_id}"); } } view.on_click(clicked); view.on_click(Actions::open::<1>); ``` If you accept and forward handlers in your own view wrapper, do not go back to `impl Into`. Keep the `Args` generic so callers can still use full arguments, no arguments, `ViewId` only, or without `ViewId`: ```rust fn accept_click(handler: impl IntoEventHandler) -> OnClickHandler { handler.into_event_handler() } ``` ## Current Dispatch Status | API | Current Status | | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | `on_mouse_move` | Dispatched. Target is the mouse-capturing view or current hover view | | `on_mouse_enter` / `on_mouse_leave` | Dispatched. Triggered when hover target changes | | `on_button_down` / `on_button_up` | Dispatched. Left button down/up | | `on_click` | Dispatched. Synthesized when left button down/up hit the same view | | `on_double_click` | Dispatched. Left button double click | | `on_right_button_down` / `on_right_button_up` | Dispatched. Right button down/up | | `on_right_button_click` | Dispatched. Synthesized when right button down/up hit the same view | | `on_right_button_double_click` | Dispatched. Right button double click | | `on_middle_button_down` / `on_middle_button_up` | Dispatched. Middle button down/up | | `on_middle_button_double_click` | Dispatched. Middle button double click | | `on_key_down` / `on_key_up` | Dispatched. Target is current focus view | | `on_focus` / `on_blur` | Dispatched. Triggered by focus manager | | `on_create` | Dispatched. Triggered after window creates view tree | | `on_wheel_settings_changed` | Dispatched. Current mouse wheel messages go through this handler | | `on_drag_enter` / `on_drag_over` / `on_drag_leave` / `on_drop` | Dispatched, requires `drag-drop` feature | | `on_context_menu` | Has binding slot, currently no external dispatch entry in source | | `on_destroy` | Has binding slot, currently no external dispatch entry in source | | `on_resize` | Has binding slot, current Resize only updates layout and render size, no external handler call | | `on_close_requested` | Has binding slot, current close request doesn't call external handler | | `on_work_area_changed` | Has binding slot, current window bus implementation is empty | | `on_dpi_change` | Has binding slot, current DPI message only updates renderer, units and layout, no external handler call | | `on_theme_changed` | Has binding slot, requires `theme-change` feature, current window bus implementation is empty | ## `ViewHandler` `ViewHandler` is a set of handler slots for each `ViewId`. Application code usually writes to these slots through `EventBuilder`, without directly manipulating `ViewHandler`. ```rust #[derive(Default)] pub struct ViewHandler { pub on_mouse_move_handler: Option, pub on_double_click_handler: Option, pub on_click_handler: Option, pub on_button_down_handler: Option, pub on_button_up_handler: Option, pub on_right_button_double_click_handler: Option, pub on_right_button_click_handler: Option, pub on_right_button_down_handler: Option, pub on_right_button_up_handler: Option, pub on_middle_button_double_click_handler: Option, pub on_middle_button_down_handler: Option, pub on_middle_button_up_handler: Option, pub on_context_menu_handler: Option, pub on_key_down_handler: Option, pub on_key_up_handler: Option, pub on_mouse_enter_handler: Option, pub on_mouse_leave_handler: Option, pub on_focus_handler: Option, pub on_blur_handler: Option, pub on_create_handler: Option, pub on_destroy_handler: Option, pub on_tooltip_show_handler: Option, pub on_tooltip_hide_handler: Option, pub on_resize_handler: Option, pub on_close_requested_handler: Option, pub on_work_area_changed_handler: Option, pub on_wheel_settings_changed_handler: Option, pub on_dpi_change_handler: Option, #[cfg(feature = "theme-change")] pub on_theme_changed_handler: Option, #[cfg(feature = "drag-drop")] pub on_drag_enter_handler: Option, #[cfg(feature = "drag-drop")] pub on_drag_over_handler: Option, #[cfg(feature = "drag-drop")] pub on_drag_leave_handler: Option, #[cfg(feature = "drag-drop")] pub on_drop_handler: Option, } ``` ## Handler Wrapper Types All handler wrapper types implement `From` for the full parameter signature, where `F` is a closure or function with the corresponding signature and satisfies `Send + Sync + 'static`. Event Builder also supports several simplified parameter forms through `IntoEventHandler`, so user code usually passes closures directly without manually constructing wrapper types. ### `Handler` ```rust pub struct Handler(pub Arc); ``` Used for events that only need `ViewId`. Accepted forms: | Form | Example | | -------------- | --------------------- | | Full arguments | `\|view_id\| { ... }` | | No arguments | `\|\| { ... }` | ### `MouseHandler` ```rust pub struct MouseHandler( pub Arc, ); ``` Used for mouse move, click, button down, button up, double click and similar events. Accepted forms: | Form | Example | | ---------------- | ------------------------------------------------ | | Full arguments | `\|view_id, key_state, mouse_position\| { ... }` | | No arguments | `\|\| { ... }` | | `ViewId` only | `\|view_id\| { ... }` | | Without `ViewId` | `\|key_state, mouse_position\| { ... }` | ### `KeyHandler` ```rust pub struct KeyHandler( pub Arc< dyn Fn(ViewId, KeyCode, bool, bool, bool) -> HandleResult + Send + Sync + 'static, >, ); ``` Parameters are `ViewId`, `KeyCode`, `is_alt`, `is_ctrl`, `is_shift` in order. Accepted forms: | Form | Example | | ---------------- | ---------------------------------------------------------------------- | | Full arguments | `\|view_id, code, is_alt, is_ctrl, is_shift\| -> HandleResult { ... }` | | No arguments | `\|\| -> HandleResult { ... }` | | `ViewId` only | `\|view_id\| -> HandleResult { ... }` | | Without `ViewId` | `\|code, is_alt, is_ctrl, is_shift\| -> HandleResult { ... }` | ### `FocusHandler` ```rust pub struct FocusHandler( pub Arc, ); ``` The second parameter is the virtual focus index. Accepted forms: | Form | Example | | ---------------- | ---------------------------------- | | Full arguments | `\|view_id, focus_index\| { ... }` | | No arguments | `\|\| { ... }` | | `ViewId` only | `\|view_id\| { ... }` | | Without `ViewId` | `\|focus_index\| { ... }` | ### `OnWheelSettingsChangedHandler` ```rust pub struct OnWheelSettingsChangedHandler( pub Arc< dyn Fn(ViewId, ScrollAxis, f32, KeyState, MousePosition) + Send + Sync + 'static, >, ); ``` Parameters are `ViewId`, scroll direction, scroll amount, key state, mouse position in order. Accepted forms: | Form | Example | | ---------------- | ------------------------------------------------------------- | | Full arguments | `\|view_id, axis, delta, key_state, mouse_position\| { ... }` | | No arguments | `\|\| { ... }` | | `ViewId` only | `\|view_id\| { ... }` | | Without `ViewId` | `\|axis, delta, key_state, mouse_position\| { ... }` | ### `OnDpiChangeHandler` ```rust pub struct OnDpiChangeHandler( pub Arc, ); ``` The second and third parameters are `dpi_x` and `dpi_y`. Accepted forms: | Form | Example | | ---------------- | ----------------------------------- | | Full arguments | `\|view_id, dpi_x, dpi_y\| { ... }` | | No arguments | `\|\| { ... }` | | `ViewId` only | `\|view_id\| { ... }` | | Without `ViewId` | `\|dpi_x, dpi_y\| { ... }` | ### `OnThemeChangedHandler` Requires `theme-change` feature. ```rust pub struct OnThemeChangedHandler( pub Arc, ); ``` Accepted forms: | Form | Example | | ---------------- | --------------------------------- | | Full arguments | `\|view_id, theme_mode\| { ... }` | | No arguments | `\|\| { ... }` | | `ViewId` only | `\|view_id\| { ... }` | | Without `ViewId` | `\|theme_mode\| { ... }` | ### `DragEnterOverHandler` Requires `drag-drop` feature. ```rust pub struct DragEnterOverHandler( pub Arc< dyn Fn(ViewId, KeyState, MousePosition, &[DragFormat], &mut DropEffect) + Send + Sync + 'static, >, ); ``` Used for `on_drag_enter` and `on_drag_over`. Accepted forms: | Form | Example | | ---------------- | ----------------------------------------------------------------- | | Full arguments | `\|view_id, key_state, mouse_position, formats, effect\| { ... }` | | No arguments | `\|\| { ... }` | | `ViewId` only | `\|view_id\| { ... }` | | Without `ViewId` | `\|key_state, mouse_position, formats, effect\| { ... }` | ### `DropHandler` Requires `drag-drop` feature. ```rust pub struct DropHandler( pub Arc< dyn Fn(ViewId, KeyState, MousePosition, &DragData, &mut DropEffect) + Send + Sync + 'static, >, ); ``` Used for `on_drop`. Accepted forms: | Form | Example | | ---------------- | -------------------------------------------------------------- | | Full arguments | `\|view_id, key_state, mouse_position, data, effect\| { ... }` | | No arguments | `\|\| { ... }` | | `ViewId` only | `\|view_id\| { ... }` | | Without `ViewId` | `\|key_state, mouse_position, data, effect\| { ... }` | ## Type Aliases ### Mouse Events | Alias | Underlying Type | | ---------------------------------- | --------------- | | `OnMouseMoveHandler` | `MouseHandler` | | `OnDoubleClickHandler` | `MouseHandler` | | `OnClickHandler` | `MouseHandler` | | `OnButtonDownHandler` | `MouseHandler` | | `OnButtonUpHandler` | `MouseHandler` | | `OnRightButtonDoubleClickHandler` | `MouseHandler` | | `OnRightButtonClickHandler` | `MouseHandler` | | `OnRightButtonDownHandler` | `MouseHandler` | | `OnRightButtonUpHandler` | `MouseHandler` | | `OnMiddleButtonDoubleClickHandler` | `MouseHandler` | | `OnMiddleButtonDownHandler` | `MouseHandler` | | `OnMiddleButtonUpHandler` | `MouseHandler` | | `OnContextMenuHandler` | `MouseHandler` | Currently there is no `OnMiddleButtonClickHandler`. ### Keyboard Events | Alias | Underlying Type | | ------------------ | --------------- | | `OnKeyDownHandler` | `KeyHandler` | | `OnKeyUpHandler` | `KeyHandler` | ### View and Lifecycle Events | Alias | Underlying Type | | --------------------- | --------------- | | `OnMouseEnterHandler` | `Handler` | | `OnMouseLeaveHandler` | `Handler` | | `OnFocusHandler` | `FocusHandler` | | `OnBlurHandler` | `FocusHandler` | | `OnCreateHandler` | `Handler` | | `OnDestroyHandler` | `Handler` | ### Tooltip Events | Alias | Underlying Type | | ---------------------- | --------------- | | `OnTooltipShowHandler` | `MouseHandler` | | `OnTooltipHideHandler` | `Handler` | Currently `EventBuilder` does not expose chain binding methods for tooltip. ### Window Events | Alias | Underlying Type | | ------------------------------- | --------------------------------------------------------- | | `OnResizeHandler` | `Handler` | | `OnCloseRequestedHandler` | `Handler` | | `OnWorkAreaChangedHandler` | `Handler` | | `OnWheelSettingsChangedHandler` | Independent wrapper type | | `OnDpiChangeHandler` | Independent wrapper type | | `OnThemeChangedHandler` | Independent wrapper type, requires `theme-change` feature | ### Drag-Drop Events Requires `drag-drop` feature. | Alias | Underlying Type | | -------------------- | ------------------------ | | `OnDragEnterHandler` | `DragEnterOverHandler` | | `OnDragOverHandler` | `DragEnterOverHandler` | | `OnDragLeaveHandler` | `Handler` | | `DropHandler` | Independent wrapper type | ## Parameter Descriptions | Type | Description | | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `ViewId` | ID of the target view that triggered the event | | `KeyState` | Mouse button and modifier key state | | `MousePosition` | Mouse coordinates. Regular mouse hit events use target view local coordinates; wheel and drag-drop currently use window client area coordinates | | `KeyCode` | Key enumeration. Common keys include letters, numbers, arrow keys, function keys, `Enter`, `Escape`, `Tab`, `Backspace`, `Space`, `Delete` | | `HandleResult` | Keyboard event handling result, `Handled` means processed, `Default` means default processing | | `ScrollAxis` | `Vertical` or `Horizontal` | | `DropEffect` | Drag-drop feedback effect, commonly `None`, `Copy`, `Move`, `Link` | | `DragFormat` | Data formats available during drag enter/over phase | | `DragData` | Actual data passed during drop phase | --- url: /website/index.md --- # Flor A Rust GUI Framework That Wants Everything, More, and Even More > Solving Rust lifetime pain points, redefining GUI development experience [Quick Start](guide/book-index) | [GitHub](https://github.com/flor-rs/flor) ## Features - [⚡ **Ultimate Performance**](/guide/book-index): High-performance rendering engine, low compiled size, retained mode design based on immediate mode, naturally supports high-performance animations. - [🔗 **No Forced Context Binding**](/guide/use/signal): Signal system supports cross-thread, implements Copy trait, can be moved anywhere, assigned anywhere, without performance issues from waiting for UI thread like in C#. - [🎨 **Declarative DSL**](/guide/use/framework-dsl): Declarative UI DSL, atomic class style parsing support, terminal application experience is simple, complexity absorbed by framework/view authors. - [🖥️ **Multi-Window Support**](/guide/use/window): Supports multi-window, can create windows in any thread, any location. Each window can independently set refresh mode. - [📚 **High API Consistency**](/guide/startup): Unified builder pattern, consistent event handling, clear view lifecycle, easy to learn and use. - [🤖 **AI-Friendly Design**](/guide/ai): DSL design similar to React's functional UI expression, allowing AI to quickly understand and generate interface code.