egui_taffy

repository·main·Indexed 42 days ago

https://github.com/ppakalns/egui_taffy

A flexible layout library for egui that brings modern CSS-like layout capabilities, including Block, Flexbox, and Grid, using the high-performance Taffy engine. Version 0.14.0 provides a TuiBuilder for defining styles, managing space allocation, and integrating standard egui widgets via the TuiWidget trait.

Tokens
6.9K
Snippets
20
Records
24
Agent score
53%

What's inside egui_taffy

  1. Use the Tui layout engine in egui

    main

    To use egui_taffy, initialize a tui instance from an egui::Ui object. You can then use a builder pattern to define styles and add elements.

    Key capabilities:

    • Reserve Space: Use .reserve_available_space() to allow the layout to occupy the full available window space.
    • Styling: Use .style(Style { ... }) to set layout properties like flex_direction, align_items, or flex_wrap. You can also use .mut_style(|style| { ... }) to modify specific fields.
    • Adding Elements:
      • Standard egui UI: Use .ui(|ui| { ... }) to pass a standard egui::Ui block.
      • Direct Widgets: Use .ui_add(widget) for widgets implementing the TuiWidget trait.
      • Helpers: Use convenience methods like .label("text") or .separator().
      • Borders: Use .add_with_border(|tui| { ... }) to add elements with a custom background/border.
    • Layout Types: Supports CSS Block, Flexbox, and Grid layouts.
    egui::Window::new("Flex wrap demo").show(ctx, |ui| {
        tui(ui, ui.id().with("demo"))
            .reserve_available_space()
            .style(Style {
                flex_direction: taffy::FlexDirection::Column,
                align_items: Some(taffy::AlignItems::Stretch),
                ..default_style()
            })
            .show(|tui| {
                // Add egui ui as node
                tui.ui(|ui| {
                    ui.label("Hello from egui ui!");
                    ui.button("Egui button");
                });
    
                // Add egui widgets directly
                tui.ui_add(egui::Label::new("label"));
                
                // Use helper functions
                tui.separator();
                tui.label("Text");
    
                // Custom style via builder
                tui.style(Style {
                    align_self: Some(taffy::AlignItems::Center),
                    ..Default::default()
                })
                .label("Centered text");
    
                // Modify existing style
                tui.style(default_style())
                    .mut_style(|style| {
                        style.align_self = Some(taffy::AlignItems::End);
                    })
                    .label("Right aligned text");
    
                // Add with border
                tui.add_with_border(|tui| {
                    tui.label("Text with border");
                });
            });
    });
  2. Enable egui multiple passes for better layout

    main

    For the best visual results, enable egui multiple passes support. This allows the layout to be immediately recalculated upon changes.

    Note: If you are using bevy_egui, you must use a special approach (see the simple_multipass example in bevy_egui) to make request_discard functionality work.

    ctx.options_mut(|options| {
        options.max_passes = std::num::NonZeroUsize::new(2).unwrap();
    });
  3. Configure text wrapping in layouts

    main

    By default, egui text wrapping tries to use as little width as possible, which can cause text to appear in a single column in dynamic layouts. To fix this, use one of the following methods:

    1. Set Widths: Specify a minimal width or fixed width for elements and set text elements to fill the parent width.
    2. Custom Wrap Mode: Apply a specific wrap mode to a tui element using .wrap_mode().
    3. Global Disable: Disable text wrapping globally by setting the wrap_mode to Extend in the egui style.
    // Option 2: Custom wrap mode for a specific element
    tui.wrap_mode(egui::TextWrapMode::Truncate).add(|tui| { ... })
    
    // Option 3: Disable text wrapping globally
    ctx.all_styles_mut(|style| {
      style.wrap_mode = Some(egui::TextWrapMode::Extend);
    });
  4. How TuiBackground handles dynamic visuals

    main

    The TuiBackground API supports three modes for resolving values like color, width, and corner radius:

    1. Custom: A static value provided directly (e.g., egui::Color32).
    2. Visuals: A value derived from the current egui::Visuals. This is useful for making backgrounds respect the application's theme (e.g., using visuals.panel_fill).
    3. VisualsResponse: A value derived from egui::Visuals AND the widget's egui::Response. This is used for state-dependent styling, such as changing the background color when a user hovers over or clicks the container.
  5. How the TUI lifecycle and layout work

    main

    The egui_taffy library bridges egui and the taffy layout engine. The workflow follows these steps:

    1. Initialization: tui() creates a TuiInitializer to define how much space the TUI should occupy in the parent egui::Ui.
    2. The Tui Object: Inside the .show() closure, you receive a Tui object. This object acts as the context for building the layout tree.
    3. Node Creation: As you add children (using methods like add_container or custom widgets), the library creates corresponding nodes in a taffy tree.
    4. Layout Calculation: When the closure finishes, the library calls recalculate(). This computes the layout for the entire tree based on the provided constraints and the intrinsic sizes of the leaf nodes.
    5. Space Allocation: If space wasn't pre-allocated via the initializer, the library uses the calculated content size to call ui.allocate_space() in egui, ensuring the parent UI knows how much room the TUI actually used.
  6. Use `TuiContainerUi` to access layout information

    main

    The TaffyContainerUi struct provides geometric information about a specific node in the layout. This is useful for positioning custom elements or understanding the node's relationship to its parent.

    Key methods:

    • layout(): Returns the underlying taffy::Layout (location and size).
    • full_container(): Returns the egui::Rect representing the full container (including borders).
    • full_container_without_border(): Returns the egui::Rect for the content area inside the borders.
    • full_container_without_border_and_padding(): Returns the egui::Rect for the content area inside both borders and padding.
    • parent_rect(): The rectangle of the parent node used for relative calculations.
    • sticky_offset(): Returns the compensation amount for sticky elements based on scroll offsets.
    // Inside a Tui container closure
    tui.add_container(params, |ui, container| {
        let rect = container.full_container_without_border();
        // Use rect to draw custom egui elements
        ui.painter().rect_filled(rect, 0.0, egui::Color32::RED);
        
        TuiContainerResponse { 
            inner: (), 
            min_size: egui::Vec2::ZERO, 
            max_size: egui::Vec2::INFINITY, 
            infinite: egui::Vec2b::FALSE 
        }
    });
  7. Handle TuiInnerResponse for interactive elements

    main

    Methods like clickable, button, filled_button, and selectable return a TuiInnerResponse<R>. This struct wraps both the return value of your closure (inner) and the egui::Response of the background area (response).

    Because TuiInnerResponse implements Deref<Target = egui::Response>, you can call egui::Response methods (like .clicked(), .hovered(), etc.) directly on the result of the function call.

    let response = tui.tui().button(|tui| {
        tui.label("Submit");
    });
    
    if response.clicked() {
        // Handle click
    }
  8. Implement virtualized grid rows with VirtualGridRowHelper

    main

    To efficiently render large datasets in a grid without overwhelming the layout engine, use VirtualGridRowHelper::show. This helper calculates which rows are currently visible within the viewport and only calls your drawing closure for those rows, using empty placeholder nodes to maintain correct scroll height.

    Requirements

    • All rows must have equal height. The helper uses the first row to estimate the height of all subsequent rows.
    • You must provide VirtualGridRowHelperParams specifying the number of header rows to skip and the total data row count.

    Usage Pattern

    1. Define VirtualGridRowHelperParams with your header_row_count and row_count.
    2. Call VirtualGridRowHelper::show(params, tui, |tui, row| { ... }).
    3. Inside the closure, use the provided VirtualGridRow to access the data index (idx) and the calculated grid_row position.
    4. Use row.grid_row_setter() to automatically apply the correct grid row style to your elements via tui.mut_style().
    5. Use row.id_gen() to generate unique TuiIds for cells within that row.
    use crate::virtual_tui::{VirtualGridRowHelper, VirtualGridRowHelperParams};
    
    let params = VirtualGridRowHelperParams {
        header_row_count: 1,
        row_count: 1000,
    };
    
    VirtualGridRowHelper::show(params, &mut tui, |tui, row| {
        // 1. Set the grid row position using the helper
        tui.id(row.id_gen()) // Use id_gen for unique cell IDs
           .mut_style(row.grid_row_setter());
    
        // 2. Draw your row content using row.idx to access your data
        // ...
    });
  9. Initialize a TUI layout with `tui()`

    main

    To start a Taffy-based UI layout within an egui::Ui, use the tui() helper function. This function returns a TuiInitializer which allows you to configure space allocation and styling before rendering the UI.

    Common configuration methods for TuiInitializer include:

    • reserve_space(space: egui::Vec2): Reserves a specific width and height.
    • reserve_width(width: f32) / reserve_height(height: f32): Reserves specific dimensions.
    • reserve_available_width() / reserve_available_height(): Uses the full available space in the current egui::Ui.
    • with_available_space(space: Size<AvailableSpace>): Sets custom sizing constraints for the Taffy layout algorithm.
    • style(style: taffy::Style): Sets the root container's Taffy style.

    Finally, call .show(f) where f is a closure that takes a &mut Tui to build your layout.

    let output = egui_taffy::tui(ui, "my_tui_id")
        .reserve_available_width()
        .reserve_height(200.0)
        .style(my_taffy_style)
        .show(|tui| {
            // Build your layout here using tui
        });
    
    // Access the result and the container layout
    let inner_value = output.inner;
    let layout_info = output.container;
  10. Check egui_taffy version compatibility

    main

    Ensure your versions of egui and taffy are compatible with the version of egui_taffy you are using.

    For example, egui_taffy v0.13 requires egui 0.35 and taffy 0.9.2, with a Minimum Supported Rust Version (MSRV) of 1.92.

    | egui_taffy | egui | taffy | MSRV |
    |------------|------| ---   |----------------|
    | 0.14       | 0.36 | 0.9.2 | 1.95 |
    | 0.13       | 0.35 | 0.9.2 | 1.92 |
    | 0.12       | 0.34 | 0.9.2 | 1.92 |
    | 0.11       | 0.34 | 0.7.7 | 1.92 |
    | 0.10       | 0.33 | 0.7.7 | 1.88 |
    | 0.9        | 0.32 | 0.7.7 | 1.85 |
    | 0.8        | 0.32 | 0.7.7 | 1.85 |
    | 0.7        | 0.31 | 0.7.3 | 1.81 |
    | 0.6        | 0.30 | 0.7.3 | 1.81 |
    | 0.5        | 0.30 | 0.7   | 1.83 |
    | 0.4        | 0.30 | 0.7   | 1.83 |
    | 0.3        | 0.29 | 0.7   | 1.83 |
    | 0.2        | 0.29 | 0.7   | 1.83 |
    | 0.1.2      | 0.29 | 0.6   | 1.83 |
    | 0.1        | 0.29 | 0.6   | 1.83 (nightly) |
  11. Add clickable nodes with bg_clickable

    main

    Use the bg_clickable method to add a child node that has a custom background and responds to user interaction (like clicks).

    Unlike bg_add, bg_clickable returns a TuiInnerResponse<T>. This allows you to access both the result of the closure (inner) and the egui::Response of the background itself (response), which is useful for detecting clicks or hover states on the container.

    let response = builder.bg_clickable(my_background, |tui| {
        // Define child nodes here
        tui.label("I am inside a clickable area")
    });
    
    if response.response.clicked() {
        println!("Background clicked!");
    }