Glimmer DSL for LibUI

repository·master·Indexed 20 days ago

https://github.com/andyobtiva/glimmer-dsl-libui

A prerequisite-free Ruby gem for building cross-platform native GUI applications on Mac, Windows, and Linux. It provides a declarative, object-oriented DSL for LibUI, featuring bidirectional and explicit data-binding, a drawing API for shapes and gradients in area widgets, and a comprehensive CLI tool for scaffolding MVC applications, custom controls, windows, and shapes.

Tokens
45K
Snippets
137
Records
180
Agent score
69%

What's inside glimmer-dsl-libui

  1. Manage memory and resource lifecycle

    master

    Glimmer DSL for LibUI handles much of the low-level resource management automatically:

    • Automatic Cleanup: When a control is destroyed, it is automatically removed from its parent container (horizontal_box, vertical_box, form, window, or group).
    • Data Binding: When a control with a data-binding is destroyed, the observer registration is automatically deregistered.
    • Memory Management:
      • image instances and table model instances are automatically freed after the window is destroyed.
      • font_button and color_button controls automatically manage font descriptors and color value pointers.
    • Garbage Collection: All controls are protected from Ruby's garbage collection until they are explicitly destroyed, ensuring stability during the GUI lifecycle.
  2. Manage transformations in Area graphics

    master

    You can apply transformations to graphics within an area using transform blocks or by providing a matrix.

    • Declarative Nature: area, path, and nested shapes are declarative. The order of fill, stroke, and transform calls does not matter.
    • Automatic Cleanup: Any transform applied within a block is automatically reversed at the end of that block. This prevents transformations on one path from affecting subsequent paths.
    • Inheritance: You can set a matrix or transform directly on an area to apply it to all nested paths.
    • Matrix Reuse: For complex transformations, you can define a matrix block and reuse it across different elements.
    # Using a transform block
    transform {
      translate 100, 100
      rotate 100, 100, -9 * n
    }
    
    # Using a reusable matrix
    m1 = matrix {
      translate 100, 100
      rotate 100, 100, -9 * n
    }
    transform m1
  3. Use explicit vs implicit data-binding in tables

    master

    Glimmer DSL for LibUI supports two ways to bind data to a table:

    1. Implicit Data-Binding: Pass the data object (like an Array) directly to cell_rows. Any changes to the array (like delete_at or updating an index) are automatically reflected in the table.
    2. Explicit Data-Binding: Use the cell_rows <= [self, :attribute_name, ...] syntax. This binds the table to a specific attribute of an object. When the attribute is updated (e.g., self.animals = new_array), the table updates automatically.
    # Implicit
    table { 
      cell_rows data 
    }
    
    # Explicit
    table { 
      cell_rows <= [self, :animals, column_attributes: {'Animal' => :name}] 
    }
  4. Optimize redraw performance with multiple small areas

    master

    Instead of redrawing one large canvas for a complex game or dynamic UI, you can represent individual components (like Tetris blocks) as separate area widgets. This approach ensures that when a single component changes (e.g., a color change), only that specific small area is redrawn, significantly improving performance for high-frequency updates.

    # Conceptual pattern from the Tetris example:
    # Each block is its own area containing squares and polygons
    block[:area] = area {
      block[:background_square] = square(0, 0, block_size) { fill color }
      block[:top_bevel_edge] = polygon { 
        point_array 0, 0, block_size, 0, ... 
        fill color_variant
      }
      # ... other bevel edges and borders
    }
  5. Use dynamic content with Content Data-Binding

    master

    You can generate UI content dynamically based on a model's attributes using the content method within a block. This allows the UI to re-render automatically when the model changes.

    • Basic Dynamic Content: Use content(model, attribute) { ... } to wrap a block of controls. The block re-renders whenever the specified attribute changes.
    • Computed Re-rendering: If you need the content to rebuild when other attributes change (even if they aren't the primary model attribute), use the computed_by: option. This is useful for complex dependencies.
    • Attribute-less Re-rendering: You can use computed_by: without a primary attribute if you simply want the block to react to specific field changes.
    # Re-renders when @user.customizable_attributes changes
    content(@user, :customizable_attributes) {
      @user.customizable_attributes.each do |attribute|
        entry {
          label attribute.to_s.split('_').map(&:capitalize).join(' ')
          text <=> [@user, attribute]
        }
      end
    }
    
    # Re-renders when street, city, or zipcode changes
    content(@user, computed_by: [:street, :city, :zipcode]) {
      @user.address_attributes.each do |attribute|
        entry {
          label attribute.to_s.split('_').map(&:capitalize).join(' ')
          text <=> [@user, attribute]
        }
      end
    }
  6. How Glimmer proxy objects handle LibUI operations

    master

    Glimmer proxy objects automatically proxy method calls to the wrapped LibUI Fiddle pointer. For example, calling window_proxy.title automatically invokes the corresponding LibUI operation (e.g., LibUI.window_title(window_proxy.libui).to_s) without requiring you to manually manage the Fiddle pointer.

    w = window('hello world')
    w.title == LibUI.window_title(w.libui).to_s # => true
  7. Implement data-binding in Glimmer DSL for LibUI

    master

    The modern version of Glimmer DSL for LibUI supports data-binding, allowing you to link UI properties directly to model attributes. This reduces the need for manual observer registration for every individual UI element.

    In the Tic Tac Toe example, the string property of an area text element is bound to a specific cell in the board model using the <= operator:

    string <= [@tic_tac_toe_board[row + 1, column + 1], :sign]

    This syntax tells the UI to automatically update the text whenever the :sign attribute of the object at [@tic_tac_toe_board[row + 1, column + 1]] changes.

    # Example of data-binding a string property to a model attribute
    text(23, 19) {
      string {
        font family: 'Arial', size: 16
        string <= [@tic_tac_toe_board[row + 1, column + 1], :sign]
      }
    }
  8. Define properties, listeners, and nested controls in content blocks

    master

    Within a control's content block, you can define its attributes, event handlers, and child elements:

    • Properties: Set attributes using lower-case underscored names (e.g., title 'hello world'). These map to LibUI.control_set_property.
    • Listeners: Handle events using names starting with on_ (e.g., on_clicked do ... end). These map to LibUI.control_on_event.
    • Nested Controls: Simply call another keyword inside the block to create a hierarchy.
    • Re-opening Content: If you need to add more elements to an existing control later, use the control.content { ... } method.
    window {
      title 'hello world' # property
      
      on_closing do # listener
        puts 'Bye'
      end
      
      button('greet') { # nested control
        on_clicked do
          puts 'hello world'
        end
      }
    }
    
    # Re-opening content
    box1 = vertical_box {
      label('First Name')
    }
    box1.content {
      entry {
        text 'fill in your first name'
      }
    }
  9. Implement Bidirectional (Two-Way) Data-Binding

    master

    Bidirectional data-binding synchronizes View properties with Model attributes in both directions using the <=> operator. This is useful for keeping UI controls and data models in sync without manual imperative updates.

    Supported controls and properties for bidirectional binding include:

    • checkbox: checked
    • check_menu_item: checked
    • color_button: color
    • combobox: selected, selected_item
    • date_picker: time
    • date_time_picker: time
    • editable_combobox: text
    • entry: text
    • font_button: font
    • multiline_entry: text
    • non_wrapping_multiline_entry: text
    • radio_buttons: selected
    • radio_menu_item: checked
    • search_entry: text
    • slider: value
    • spinbox: value
    • table: cell_rows, selection
    • table columns (e.g. text_column): sort_indicator
    • time_picker: time
    entry {
      text <=> [contract, :legal_text]
    }
  10. Create area-based custom controls

    master

    You can define custom graphical components from scratch by building them on top of the area control. This allows you to use vector graphics (rectangles, text, etc.) to create unique UI elements while still leveraging keyboard and mouse listeners (like on_mouse_down or on_mouse_up).

    To implement a custom control, you typically wrap an area block and use its drawing methods (like rectangle and text) to render the component's visual state. You can also access the area's children to modify their properties dynamically in response to events.

    # Example of a custom control structure
    def my_custom_control(text, width: 80, height: 30, **options, &content)
      area { |the_area|
        # 1. Draw the background
        rectangle(1, 1, width, height) { fill :white }
        
        # 2. Draw the text
        text(10, 10, width) { 
          string(text) { color :black }
        }
    
        # 3. Add interaction
        on_mouse_down do
          # Access children to change appearance
          the_area.children[0].fill = :blue
        end
    
        # 4. Allow user to extend the area
        content&.call(the_area)
      }
    end
  11. Use the Table control with automatic data-binding

    master

    The table control simplifies data management by automatically constructing the required TableModelHandler, TableModel, and TableParams based on your cell_rows and nested columns (like text_column).

    Automatic Synchronization: The cell_rows data has implicit data-binding. When you modify the cell_rows array, the table is automatically updated:

    • Deleting a row from cell_rows deletes the corresponding row in the UI.
    • Inserting a row into cell_rows inserts a row in the UI.
    • Updating a value in cell_rows updates the cell in the UI.
  12. Implement bidirectional data-binding with `after_write`

    master

    In Glimmer DSL for LibUI, you can achieve bidirectional data-binding for widget values (like spinbox or color_button) using the <=> operator. To ensure the UI updates (e.g., redrawing an area) when the data changes, use the after_write option within the binding.

    For example, when a spinbox value changes, you can trigger a redraw of an @area component:

    spinbox(0, 100) { |sb|
      stretchy false
      value <=> [self, "datapoints[#{i}]", after_write: -> { @area.queue_redraw_all }]
    }