ccf-gpui-widgets 0.1.0

Reusable GPUI widgets for building desktop applications
Documentation

ccf-gpui-widgets

Reusable GPUI widgets for building desktop applications.

Features

  • Themeable: All widgets support custom themes via global context or per-widget override
  • Accessible: Keyboard navigation support where applicable
  • Event-driven: All widgets emit events for state changes
  • Builder pattern: Fluent API for widget configuration

Installation

Add to your Cargo.toml:

[dependencies]
ccf-gpui-widgets = "0.1"

# For file/directory pickers (adds rfd and dirs dependencies)
# ccf-gpui-widgets = { version = "0.1", features = ["file-picker"] }

Quick Start

use gpui::*;
use ccf_gpui_widgets::{Theme, widgets::*};

Application::new().run(|cx: &mut App| {
    // Register keybindings for widgets that need them
    register_all_keybindings(cx);

    // Optionally set a global theme
    cx.set_global(Theme::dark());

    cx.open_window(WindowOptions::default(), |_window, cx| {
        cx.new(|cx| MyView::new(cx))
    }).unwrap();

    cx.activate(true);
});

Available Widgets

Input Widgets

Widget Description
TextInput Full-featured text input with cursor, selection, clipboard
PasswordInput Text input with visibility toggle
NumberStepper Numeric input with +/- buttons
Slider Horizontal slider for numeric ranges

Selection Widgets

Widget Description
Checkbox Simple checkbox with optional label
ToggleSwitch On/off toggle with configurable label position
Dropdown Select/dropdown with keyboard navigation
RadioGroup Single-selection from multiple choices
CheckboxGroup Multi-selection from multiple choices
ColorSwatch Color picker with hex input, HSV canvas

Display Widgets

Widget Description
Tooltip Hover tooltip
ProgressBar Progress indicator (determinate/indeterminate)
Spinner Loading spinner in multiple sizes

Layout & Navigation

Widget Description
Collapsible Expandable/collapsible section
TabBar Tab navigation with keyboard support
ConfirmationDialog Modal dialogs (Info/Default/Warning/Danger styles)

Repeatable Widgets

Widget Description
RepeatableTextInput Text input with add/remove for lists

File Widgets (requires file-picker feature)

Widget Description
FilePicker File selection with native dialog
DirectoryPicker Directory selection with native dialog
RepeatableFilePicker File picker with add/remove for lists
RepeatableDirectoryPicker Directory picker with add/remove for lists

Utilities

Function Description
primary_button() Blue/accent styled button
secondary_button() Gray styled button
danger_button() Red styled button
with_focus_actions() Add Tab/Shift-Tab focus navigation to elements

Theming

Widgets use a Theme struct for colors. You can:

  1. Set a global theme: cx.set_global(Theme::dark())
  2. Use per-widget themes: TextInput::new(cx).theme(my_theme)
  3. Use the default (dark theme) if nothing is set
use ccf_gpui_widgets::Theme;

// Built-in themes
let dark = Theme::dark();
let light = Theme::light();

// Customize with builder methods
let custom = Theme::dark()
    .with_accent(0x00ff00)
    .with_primary(0xff0000);

Widget Usage Examples

TextInput

let input = cx.new(|cx| {
    TextInput::new(cx)
        .placeholder("Enter text...")
        .select_on_focus(true)
});

cx.subscribe(&input, |this, _input, event: &TextInputEvent, cx| {
    match event {
        TextInputEvent::Change => { /* content changed */ }
        TextInputEvent::Enter => { /* enter pressed */ }
        _ => {}
    }
}).detach();

Checkbox

let checkbox = cx.new(|cx| {
    Checkbox::new(cx)
        .checked(true)
        .label("Enable feature")
});

cx.subscribe(&checkbox, |this, _cb, event: &CheckboxEvent, cx| {
    if let CheckboxEvent::Change(checked) = event {
        println!("Checkbox is now: {}", checked);
    }
}).detach();

Dropdown

let dropdown = cx.new(|cx| {
    Dropdown::new(cx)
        .choices(vec!["Option 1".into(), "Option 2".into()])
        .with_selected_index(0)
});

cx.subscribe(&dropdown, |this, _dd, event: &DropdownEvent, cx| {
    if let DropdownEvent::Change(value) = event {
        println!("Selected: {}", value);
    }
}).detach();

NumberStepper

let stepper = cx.new(|cx| {
    NumberStepper::new(cx)
        .with_value(50.0)
        .min(0.0)
        .max(100.0)
        .step(5.0)
});

ColorSwatch

let swatch = cx.new(|cx| {
    ColorSwatch::new(cx)
        .with_value("#3b82f6")  // Initial color (hex or CSS name like "coral")
        .with_alpha(true)       // Enable alpha channel
});

cx.subscribe(&swatch, |this, _swatch, event: &ColorSwatchEvent, cx| {
    if let ColorSwatchEvent::Change(hex) = event {
        println!("Color changed: {}", hex);  // e.g., "#3B82F6" or "#3B82F680"
    }
}).detach();

The color picker popup includes:

  • 2D saturation/brightness canvas (HSV model)
  • Hue slider (0-359°)
  • Alpha slider (when enabled)
  • RGB component sliders
  • Old/New color comparison
  • Hex value display

Supports hex input (#RGB, #RRGGBB, #RRGGBBAA) and all 140 CSS named colors.

Slider

let slider = cx.new(|cx| {
    Slider::new(cx)
        .with_value(50.0)
        .min(0.0)
        .max(100.0)
        .step(1.0)
        .show_value(true)
});

cx.subscribe(&slider, |this, _slider, event: &SliderEvent, cx| {
    match event {
        SliderEvent::Change(value) => { /* value changing */ }
        SliderEvent::ChangeComplete(value) => { /* drag ended */ }
    }
}).detach();

ToggleSwitch

let toggle = cx.new(|cx| {
    ToggleSwitch::new(cx)
        .with_on(true)
        .label("Dark mode")
        .label_position(LabelPosition::Left)
});

cx.subscribe(&toggle, |this, _toggle, event: &ToggleSwitchEvent, cx| {
    if let ToggleSwitchEvent::Change(is_on) = event {
        println!("Toggle is now: {}", is_on);
    }
}).detach();

TabBar

let tabs = cx.new(|cx| {
    TabBar::new(cx)
        .tabs(vec![
            TabItem::new("general", "General"),
            TabItem::new("advanced", "Advanced"),
            TabItem::new("about", "About"),
        ])
        .with_selected_index(0)
});

cx.subscribe(&tabs, |this, _tabs, event: &TabBarEvent, cx| {
    if let TabBarEvent::Change(index) = event {
        println!("Selected tab: {}", index);
    }
}).detach();

ConfirmationDialog

let dialog = cx.new(|cx| {
    ConfirmationDialog::new(cx)
        .style(DialogStyle::Warning)
        .title("Delete Item")
        .message("Are you sure you want to delete this item? This action cannot be undone.")
        .primary_label("Delete")
        .secondary_label("Cancel")
});

cx.subscribe(&dialog, |this, _dialog, event: &ConfirmationDialogEvent, cx| {
    match event {
        ConfirmationDialogEvent::Primary => { /* confirmed */ }
        ConfirmationDialogEvent::Secondary => { /* cancelled */ }
        ConfirmationDialogEvent::Tertiary => { /* third option */ }
    }
}).detach();

FilePicker (requires file-picker feature)

let picker = cx.new(|cx| {
    FilePicker::new(cx)
        .mode(FileMode::Save)
        .extensions(vec!["json".into(), "yaml".into()])
        .placeholder("Select output file...")
});

Keybindings

Some widgets require keybindings to be registered at startup. Call register_all_keybindings(cx) once during app initialization:

Application::new().run(|cx: &mut App| {
    ccf_gpui_widgets::register_all_keybindings(cx);
    // ...
});

Or register individual widgets:

ccf_gpui_widgets::widgets::text_input::register_keybindings(cx);
ccf_gpui_widgets::widgets::dropdown::register_keybindings(cx);

License

MIT OR Apache-2.0