quartz
Quartz
A Windows Forms-inspired, cross-platform GUI toolkit for Crystal
β¨ Overview
Quartz is a native, cross-platform GUI toolkit for the Crystal programming language. Its API is inspired by Windows Forms, making it instantly familiar to developers who have worked with desktop GUI frameworks before.
Quartz does not bundle a rendering engine or ship a UI library. Instead, it wraps each platform's native toolkit through a thin C bridge, giving you truly native look-and-feel on every OS β for free.
| Platform | Backend |
|---|---|
| AppKit (Cocoa) | |
| GTK 3 or Qt 5/6 | |
| Win32 API |
π§ͺ Tested On
Quartz has been verified to work on the following environments:
| OS | Backend | Status |
|---|---|---|
| AppKit | β Working | |
| GTK 3 | β Working | |
| Qt 6 | β Working |
π Quick Start
require "quartz"
Quartz::Application.run do |app|
window = Quartz::Window.new("Hello, Quartz!", 400, 250)
label = Quartz::Label.new("Welcome! π", x: 20, y: 20, width: 360, height: 30)
window.add_control(label)
button = Quartz::Button.new("Click Me", x: 20, y: 60, width: 120, height: 32)
button.on_click { label.text = "Button clicked! π" }
window.add_control(button)
window.show
end
π¦ Installation
-
Add the dependency to your
shard.yml:dependencies: quartz: github: umutuluer/quartz -
Install with Shards:
shards install
π¨ Building from Source
Quartz links against a compiled ext/quartz_helper.o object file. Use the provided Makefile to build it:
make # auto-detects your platform (macOS / Linux / Windows)
make mac # force macOS AppKit backend
make gtk # force Linux GTK 3 backend
make qt # force Linux Qt 5/6 backend (QT_VERSION=6 for Qt6)
make win # force Windows Win32 backend
make clean # remove build artifacts
Then compile your Crystal application as usual:
shards build
π API Reference
Application Lifecycle
| Method | Description |
|---|---|
Quartz::Application.run { } |
Starts the application loop |
Quartz::Application.exit |
Terminates the application |
Window
| Method | Description |
|---|---|
Window.new(title, width, height) |
Creates a new window |
window.title = "New Title" |
Changes the window title |
window.show |
Shows the window |
window.add_control(control) |
Adds a child control |
Button
| Method | Description |
|---|---|
Button.new(text, x, y, width, height) |
Creates a push button |
button.text = "Save" |
Updates the button label |
button.on_click { } |
Registers a click callback |
Label
| Method | Description |
|---|---|
Label.new(text, x, y, width, height) |
Creates a read-only label |
label.text |
Gets the current text |
label.text = "Updated" |
Updates the label text |
TextBox
| Method | Description |
|---|---|
TextBox.new(text, x, y, width, height) |
Creates a text input box |
textbox.text |
Gets the current text |
textbox.placeholder = "..." |
Sets placeholder text |
textbox.password_char = '*' |
Sets password masking |
textbox.on_text_changed { } |
Registers change event |
ListBox
| Method | Description |
|---|---|
ListBox.new(x, y, width, height) |
Creates a scrollable list |
listbox.add_item("Item") |
Adds an item to the list |
listbox.remove_item(index) |
Removes the item at index |
listbox.clear |
Removes all items |
listbox.selected_index |
Gets / sets selected index |
listbox.selected_text |
Gets selected item text or nil |
listbox.item_count |
Returns the number of items |
listbox.item_text(index) |
Gets the text at given index |
listbox.on_selection_changed { } |
Registers selection change |
ComboBox
| Method | Description |
|---|---|
ComboBox.new(x, y, w, h, editable: false) |
Creates a dropdown (editable = text input) |
combo.add_item("Item") |
Adds an item |
combo.remove_item(index) |
Removes the item at index |
combo.clear |
Removes all items |
combo.item_count |
Returns the number of items |
combo.item_text(index) |
Gets the text at given index |
combo.selected_index |
Gets / sets selected index |
combo.selected_text |
Gets selected item text or nil |
combo.text |
Gets the current text |
combo.text = "..." |
Sets the current text |
combo.dropped_down? |
Whether dropdown is open |
combo.dropped_down = true |
Opens / closes the dropdown |
combo.on_selection_changed { } |
Registers selection change |
combo.on_text_changed { } |
Registers edit-text change |
FileDialog
Abstract base for file dialogs. Concrete classes: OpenFileDialog, SaveFileDialog.
| Method | Description |
|---|---|
dialog.title |
Dialog window title |
dialog.filter |
WinForms-style filter ("Text (*.txt)|*.txt|All (*.*)|*.*") |
dialog.initial_directory |
Starting directory |
dialog.file_name |
Initial file name (SaveFileDialog) |
dialog.default_ext |
Default extension |
dialog.show_dialog(owner : Window?) |
Blocking modal; returns String? (nil = cancel) |
OpenFileDialog additionally has multiselect : Bool. SaveFileDialog additionally has overwrite_prompt : Bool (default true).
Multiselect MVP: only the first selected file is returned. Full
Array(String)?API coming later.
StackLayout
Linear layout: vertical ya da horizontal stack. Saf Crystal sΔ±nΔ±fΔ±dΔ±r (Control'den tΓΌremez); Γ§ocuklarΔ± add(control, width, height) ile ΓΆlΓ§ΓΌleriyle birlikte ekler, relayout ile yeniden konumlandΔ±rΔ±r.
| Method | Description |
|---|---|
layout = StackLayout.new(orientation, padding, spacing) |
orientation :vertical veya :horizontal, defaults: VBox, 0, 0 |
layout.add(control, width, height) : self |
Zincirleme ekleme |
layout.remove(control) : self |
ΓΔ±kar + relayout |
layout.clear : self |
TΓΌm Γ§ocuklarΔ± sil |
layout.size : Int32 |
Γocuk sayΔ±sΔ± |
layout.relayout : self |
TΓΌm Γ§ocuklarΔ± yeniden konumlandΔ±r (manuel tetikleme; auto-resize bir sonraki sΓΌrΓΌmde) |
Vertical: Γ§ocuklar dikey; her biri parent_width - 2*padding kadar geniΕlikte. Horizontal: Γ§ocuklar yatay; her biri belirtilen width kadar, parent_height - 2*padding yΓΌksekliΔinde.
Not: Resize'da otomatik reflay henΓΌz yok. Pencere yeniden boyutlandΔ±rΔ±lΔ±rsa
layout.relayoutmanuel Γ§aΔrΔ±lmalΔ±. Hook altyapΔ±sΔ± (Window#on_resize) bir sonraki sΓΌrΓΌmde eklenecek.
CheckBox
| Method | Description |
|---|---|
CheckBox.new(text, x, y, width, height) |
Creates a checkbox |
CheckBox.new(text, x, y, w, h, checked: true) |
Creates a pre-checked checkbox |
check_box.text = "Enable" |
Updates the label |
check_box.checked |
Returns whether checked |
check_box.checked = true |
Sets the checked state |
check_box.on_checked_changed { } |
Registers state-change callback |
RadioButton
| Method | Description |
|---|---|
RadioButton.new(text, x, y, width, height) |
Creates a radio button |
RadioButton.new(text, x, y, w, h, checked: true) |
Creates a pre-selected radio button |
radio_button.text = "Option" |
Updates the label |
radio_button.checked |
Returns whether selected |
radio_button.checked = true |
Selects (unchecks siblings) |
radio_button.on_checked_changed { } |
Registers state-change callback |
Radio buttons in the same window are mutually exclusive β selecting one automatically deselects others. Radio buttons in different windows are independent.
MenuBar / MenuItem / MenuSeparator / ContextMenu
| Class | Method |
|---|---|
MenuBar.new |
Create top-level menubar |
MenuBar#add_item(item) / add_item(text, &block) |
Add menu item |
MenuBar#add_separator |
Add visual separator |
MenuBar#items |
Returns list of added items |
Window#menu_bar= |
Attach menubar to window |
MenuItem.new(text) |
Create menu item with label |
MenuItem#on_click(&block) |
Register click callback |
MenuSeparator.new |
Create separator |
ContextMenu.new |
Create right-click menu |
Control#context_menu= |
Attach context menu to widget |
MVP limitations:
- Submenu (nested menus) not supported β flat menubar only
- Shortcuts, checked items β out of MVP scope
- macOS menubar is app-global (last assigned wins); per-window future work
- Qt menubar requires QMainWindow promotion (separate backend task)
β οΈ Submenu support is OUT of the MVP β flat menubar only. A menubar currently holds a single level of items; nested/submenus require an extra C API (
quartz_menu_item_set_submenuor similar) that is deferred to a later wave.MenuBarinstances are designed to be reused as submenu containers once that lands.
macOS limitation: the menubar is app-global (
[NSApp setMainMenu:]). With multiple windows the lastwindow.menu_bar=wins. This matches the documented Wave-1b risk inMENU_PLAN.md.
Qt limitation: Qt menubar atamasΔ±, QMainWindow terfisi ayrΔ± bir dalgada eklenecek. Context menu (saΔ tΔ±k) Qt'de Γ§alΔ±ΕΔ±r.
All controls inherit from
Control, which provides the low-levelhandleandparentproperties.
π§ͺ Examples
Run the included hello-world example:
make examples
./bin/hello_world
Example source: examples/hello_world.cr
Run the TextBox demo:
crystal build examples/textbox_example.cr -o bin/textbox_example
./bin/textbox_example
Example source: examples/textbox_example.cr
Run the ListBox demo:
crystal build examples/listbox_example.cr -o bin/listbox_example
./bin/listbox_example
Example source: examples/listbox_example.cr
Run the OpenFileDialog demo:
crystal build examples/openfiledialog_example.cr -o bin/openfiledialog_example
./bin/openfiledialog_example
Example source: examples/openfiledialog_example.cr
Run the SaveFileDialog demo:
crystal build examples/savefiledialog_example.cr -o bin/savefiledialog_example
./bin/savefiledialog_example
Example source: examples/savefiledialog_example.cr
Run the Layout demo:
crystal build examples/layout_example.cr -o bin/layout_example
./bin/layout_example
Example source: examples/layout_example.cr
Run the ComboBox demo:
crystal build examples/combobox_example.cr -o bin/combobox_example
./bin/combobox_example
Example source: examples/combobox_example.cr
Run the CheckBox demo:
crystal build examples/checkbox_example.cr -o bin/checkbox_example
./bin/checkbox_example
Example source: examples/checkbox_example.cr
Run the RadioButton demo:
crystal build examples/radiobutton_example.cr -o bin/radiobutton_example
./bin/radiobutton_example
Example source: examples/radiobutton_example.cr
Run the Menu demo:
crystal build examples/menu_example.cr -o bin/menu_example
./bin/menu_example
Example source: examples/menu_example.cr
ποΈ Architecture
ββββββββββββββββββββββββββββββββββββββββββββ
β Crystal (src/) β
β Application Β· Window Β· Button Β· Label β
β TextBox Β· ListBox Β· ComboBox β
β CheckBox Β· FileDialog Β· RadioButton Β· β
β MenuBar Β· MenuItem Β· MenuSeparator Β· β
β ContextMenu β
β StackLayout β
β β β
β lib_quartz.cr β
β (C bindings via @[Link]) β
ββββββββββββββββββββ¬ββββββββββββββββββββββββ
β quartz_helper.h
ββββββββββββββββββββ΄ββββββββββββββββββββββββ
β C bridge (ext/) β
β quartz_helper_mac.m ββ AppKit β
β quartz_helper_gtk.c ββ GTK 3 β
β quartz_helper_qt.cpp ββ Qt 5/6 β
β quartz_helper_win.c ββ Win32 API β
ββββββββββββββββββββββββββββββββββββββββββββ
πΊοΈ Roadmap
- macOS AppKit backend
- Linux GTK 3 backend
- Linux Qt 5/6 backend
- Windows Win32 backend
- TextBox / input controls
- ListBox
- CheckBox, RadioButton
- ComboBox / DropDown
- File dialogs
- Layout managers β Stack MVP
- Layout managers (Flow, Grid)
- Menu bar & context menus (C layer: AppKit/GTK/Win32/Qt items + context menus; Qt menubar attach deferred β QMainWindow promotion. Submenus out of MVP.)
- Comprehensive test suite
π€ Contributing
Contributions are welcome! Here's how you can help:
- Fork the repository
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Open a Pull Request
Please make sure your code follows the existing style and includes appropriate specs.
π License
This project is licensed under the MIT License β see the LICENSE file for details.
π€ Author
Umut ULUER β @umutuluer
Built with β€οΈ and Crystal
Testing
Quartz ships with 259 deterministic spec examples that run in under 500ms across 4 native backends.
make spec # run the full suite (default backend: mac/AppKit)
make lint # ameba static analysis
make format # crystal tool format --check
make examples # build the 10 example programs
The test suite covers:
- Crystal-level coverage β every public method on every widget
- Native callback routing β C-level trampoline helpers fire real callbacks through the 4 backends' callback tables
- Lifecycle β show/close, multi-window ID uniqueness, re-parenting, queryability after operations
- Edge cases β Unicode (emoji π, RTL ΨΉΨ±Ψ¨ΩΨ©, ZWJ π¨βπ©βπ§), 10k-char strings, zero/negative dimensions, empty strings
- Geometry β StackLayout pixel-position asserts via
quartz_widget_get_bounds - Modal dialogs β
OpenFileDialog/SaveFileDialogmocked through a C-level seam (quartz_test_dialog_set_mode+QUARTZ_TEST_DIALOG_PATH)
CI matrix runs the full suite on all 4 backends: macOS/AppKit, Linux/GTK3 (under xvfb), Linux/Qt5 (offscreen QPA), Windows/Win32.
Spec files live under spec/ (unit specs in spec/unit/, helpers in spec/support/). The C-level test helpers (quartz_test_fire_*, quartz_widget_get_bounds, quartz_test_dialog_set_mode) are declared in ext/quartz_helper.h and implemented in all 4 backends.
quartz
- 0
- 0
- 0
- 0
- 0
- 11 days ago
- August 1, 2026
MIT License
Thu, 13 Aug 2026 21:56:55 GMT