MittensUi
A lightweight Ruby GUI toolkit built on top of GTK, inspired by Ruby Shoes. MittensUi wraps the complexity of GTK so you can build desktop apps in Ruby.
We always try to keep up with the latest GTK updates.
Installation
Add to your Gemfile:
gem 'mittens_ui'
Then run:
bundle install
Or install directly:
gem install mittens_ui
Quick Start
require 'mittens_ui'
MittensUi::Application.Window(name: "my_app", title: "Hello World", width: 400, height: 300) do
MittensUi::Label.new("What is your name?")
name_tb = MittensUi::Textbox.new(can_edit: true, placeholder: "Enter name...")
btn = MittensUi::Button.new(title: "Say Hello")
btn.click do
MittensUi::Alert.new("Hello, #{name_tb.text}!")
end
end
Core Concepts
Application Window
Every MittensUi app starts with Application.Window. All widgets are created inside the block.
MittensUi::Application.Window(
name: "my_app", # used as process name and store identifier
title: "My App", # window title bar text
width: 400, # window width in pixels
height: 600, # window height in pixels
can_resize: true, # whether the window is resizable
icon: "icon.png" # optional path to a window icon
) do
# widgets go here
end
Layout System
MittensUi uses a 12-unit grid layout. Every widget accepts a :width option that controls how many columns it occupies.
MittensUi::Label.new("Full width", width: :full) # 12 units
MittensUi::Label.new("Half width", width: :half) # 6 units
MittensUi::Label.new("Third width", width: :third) # 4 units
MittensUi::Label.new("Quarter width", width: :quarter) # 3 units
Two :half widgets placed back to back sit side by side automatically. A :full widget always gets its own row.
Horizontal Rows
Use HBox to place widgets side by side in a row:
MittensUi::HBox.new(spacing: 8) do
MittensUi::Label.new("Name:")
MittensUi::Textbox.new(can_edit: true)
MittensUi::Button.new(title: "Save")
end
HBox supports nesting:
MittensUi::HBox.new(spacing: 8) do
MittensUi::Label.new("Left")
MittensUi::HBox.new(spacing: 4) do
MittensUi::Button.new(title: "A")
MittensUi::Button.new(title: "B")
end
MittensUi::Label.new("Right")
end
Persistent Store
MittensUi includes a built-in key-value store backed by JSON, saved to ~/.local/share/mittens_ui/<app_name>.json. Data persists across app launches automatically.
MittensUi::Application.store.set(:theme, "dark")
MittensUi::Application.store.get(:theme) # => "dark"
MittensUi::Application.store.get(:missing, 42) # => 42 (default)
MittensUi::Application.store.delete(:theme)
MittensUi::Application.store.all # => { theme: "dark" }
MittensUi::Application.store.clear
Widget Reference
Knob
A rotary knob widget inspired by synthesizer hardware. Click and drag up or right to increase the value, down or left to decrease it. Scroll wheel also works.
knob = MittensUi::Knob.new(min: 0, max: 100, value: 50, label: "Volume")
knob.on_change { |v| puts "Volume: #{v}" }
# custom color and size
knob = MittensUi::Knob.new(
min: 0,
max: 127,
value: 64,
size: 80,
label: "Cutoff",
color: [0.2, 0.6, 1.0]
)
# programmatic control
knob.value = 75
puts knob.value # => 75.0
# row of synth knobs
MittensUi::HBox.new(spacing: 8) do
MittensUi::Knob.new(min: 0, max: 127, value: 64, label: "Cutoff", color: [0.2, 0.6, 1.0])
MittensUi::Knob.new(min: 0, max: 127, value: 32, label: "Resonance", color: [1.0, 0.4, 0.2])
MittensUi::Knob.new(min: 0, max: 127, value: 80, label: "Attack", color: [0.8, 0.8, 0.2])
MittensUi::Knob.new(min: 0, max: 127, value: 60, label: "Release", color: [0.6, 0.2, 1.0])
end
Label
MittensUi::Label.new("Hello World", top: 10)
Button
btn = MittensUi::Button.new(title: "Click Me")
btn.click { puts "clicked!" }
Buttons support a loading state that disables the button and shows a spinner while background work runs:
btn.click do
btn.loading do
sleep 2 # runs on a background thread
puts "done!"
end
end
Textbox
# single line
tb = MittensUi::Textbox.new(can_edit: true, placeholder: "Enter text...")
puts tb.text
tb.clear
# password field
tb = MittensUi::Textbox.new(password: true)
# multiline text area
tb = MittensUi::Textbox.new(multiline: true, height: 120)
# with autocomplete suggestions
tb = MittensUi::Textbox.new(can_edit: true)
tb.enable_text_completion(["Ruby", "Rails", "Rack"])
Checkbox
cb = MittensUi::Checkbox.new(label: "Enable notifications")
cb.value = "notifications"
cb.toggle { puts "toggled! value: #{cb.value}" }
RadioButton
A group of mutually exclusive options. Only one can be selected at a time.
rb = MittensUi::RadioButton.new(
options: ["Small", "Medium", "Large"],
default: "Medium",
layout: :horizontal # or :vertical
)
puts rb.selected # => "Medium"
rb.on_change { |v| puts v } # fires on selection change
rb.select("Large") # programmatic selection
Listbox
# basic dropdown
lb = MittensUi::Listbox.new(items: ["Ruby", "Python", "Elixir"])
puts lb.selected_value
# with search box
lb = MittensUi::Listbox.new(items: ["Ruby", "Python", "Elixir"], searchable: true)
lb.update_items(["Go", "Rust", "Zig"])
lb.clear_search
Slider
s = MittensUi::Slider.new(start_value: 0, stop_value: 100, initial_value: 50)
s. { |, value| puts "#{}: #{value}" }
Switch
sw = MittensUi::Switch.new
sw.on { puts "status: #{sw.status}" }
puts sw.status # => :on or :off
Image
img = MittensUi::Image.new("./assets/logo.png", width: 200, height: 200)
img.click { puts "image clicked!" }
# GIF support
img = MittensUi::Image.new("./assets/animation.gif")
TableView
table = MittensUi::TableView.new(
['Name', 'Email'],
[['John', 'john@example.com']]
)
table.add(['Jane', 'jane@example.com'])
table.row_clicked { |row| puts row.inspect }
table.row_double_clicked { |row| puts "Double: #{row.inspect}" }
Alert
MittensUi::Alert.new("Something happened!", title: "Notice")
Notify
MittensUi::Notify.new("Saved!", type: :info) # banner notification
# types: :info, :error, :question
Loader
loader = MittensUi::Loader.new
loader.start do
sleep 3 # runs on a background thread
end
HeaderBar
btn = MittensUi::Button.new(title: "New", defer_render: true)
MittensUi::HeaderBar.new([btn], title: "My App", position: :left)
# position: :left or :right
FileMenu
= {
"File": { sub_menus: ["New", "Open", "Exit"] },
"Edit": { sub_menus: ["Copy", "Paste"] }
}.freeze
fm = MittensUi::FileMenu.new()
fm.exit { MittensUi::Application.exit }
fm.new { puts "New file" }
FilePicker
picker = MittensUi::FilePicker.new
puts picker.path # => "/home/user/file.txt" or nil if cancelled
WebLink
MittensUi::WebLink.new("GitHub", "https://github.com")
Separator
MittensUi::Separator.new
MittensUi::Separator.new(:horizontal, top: 10, bottom: 10) # with margin
HBox
# block style
MittensUi::HBox.new(spacing: 8) do
MittensUi::Button.new(title: "OK")
MittensUi::Button.new(title: "Cancel")
end
# nested HBox
MittensUi::HBox.new(spacing: 8) do
MittensUi::Label.new("Tools:")
MittensUi::HBox.new(spacing: 4) do
MittensUi::Button.new(title: "Cut")
MittensUi::Button.new(title: "Copy")
MittensUi::Button.new(title: "Paste")
end
end
# array style — requires `defer_render: true` attribute to be set on each widget
MittensUi::HBox.new([
MittensUi::Button.new(title: "OK", defer_render: true),
MittensUi::Button.new(title: "Cancel", defer_render: true)
])
Development
MittensUi requires GTK native libraries to be installed on your system.
Install Ruby if you have not done so:
rbenv install 4.0.0
rbenv global 4.0.0
Linux
git clone https://github.com/tuttza/mittens_ui.git
cd mittens_ui
bundle install
macOS
brew install gtk4 cairo pkg-config rbenv
git clone https://codeberg.com/tuttza/mittens_ui.git
cd mittens_ui
bundle install
Run the test suite:
bundle exec rspec
Generate API docs:
bundle exec yard doc
License
Available under the MIT License.