├── comctl32.rc
├── examples
├── zigzag
│ ├── .gitignore
│ ├── shard.yml
│ ├── README.md
│ └── zigzag.cr
├── gallery
│ ├── crys.png
│ ├── basic_msg_box.cr
│ ├── basic_msg_box_error.cr
│ ├── basic_window.cr
│ ├── basic_label.cr
│ ├── basic_separator.cr
│ ├── basic_progressbar.cr
│ ├── basic_multiline_entry.cr
│ ├── basic_button.cr
│ ├── basic_entry.cr
│ ├── basic_checkbox.cr
│ ├── basic_slider.cr
│ ├── basic_spinbox.cr
│ ├── basic_date_time_picker.cr
│ ├── basic_combobox.cr
│ ├── basic_color_button.cr
│ ├── basic_font_button.cr
│ ├── basic_editable_combobox.cr
│ ├── basic_tab.cr
│ ├── basic_radio_buttons.cr
│ ├── basic_area.cr
│ ├── basic_box_vertical.cr
│ ├── basic_box_horizontal.cr
│ ├── basic_table.cr
│ ├── basic_image_view.cr
│ ├── basic_menu.cr
│ ├── basic_form.cr
│ ├── basic_group.cr
│ ├── basic_grid.cr
│ ├── area_basic_shapes.cr
│ ├── area_draw_image.cr
│ ├── basic_draw_text.cr
│ └── area_analog_clock.cr
├── md5_checker
│ ├── resources
│ │ └── app_icon.png
│ ├── shard.yml
│ ├── .env
│ ├── .gitignore
│ ├── src
│ │ ├── md5_checker.cr
│ │ ├── table_handler.cr
│ │ ├── app.cr
│ │ └── checker.cr
│ ├── README.md
│ ├── build-deb.sh
│ ├── build-mac.sh
│ └── build-win.ps1
├── video_player
│ ├── .gitignore
│ ├── shard.yml
│ ├── README.md
│ └── src
│ │ ├── platform_embedding.cr
│ │ └── mpv_bindings.cr
├── basic_button_dsl.cr
├── basic_button.cr
├── basic_window.cr
├── basic_entry.cr
├── basic_area.cr
├── basic_table.cr
└── notepad.cr
├── spec
├── spec_helper.cr
└── libui_spec.cr
├── src
└── uing
│ ├── grid
│ ├── align.cr
│ └── at.cr
│ ├── text_italic.cr
│ ├── lib_ui
│ ├── init_options.cr
│ ├── table_selection.cr
│ ├── table_text_column_optional_params.cr
│ ├── table_params.cr
│ ├── draw_brush_gradient_stop.cr
│ ├── draw_matrix.cr
│ ├── area_key_event.cr
│ ├── draw_text_layout_params.cr
│ ├── area_draw_params.cr
│ ├── draw_stroke_params.cr
│ ├── area_mouse_event.cr
│ ├── font_descriptor.cr
│ ├── draw_brush.cr
│ ├── control.cr
│ ├── tm.cr
│ ├── table_model_handler.cr
│ └── area_handler.cr
│ ├── version.cr
│ ├── table
│ └── table
│ │ ├── sort_indicator.cr
│ │ ├── value
│ │ └── type.cr
│ │ ├── selection
│ │ └── mode.cr
│ │ ├── text_column_optional_params.cr
│ │ ├── params.cr
│ │ ├── model.cr
│ │ ├── selection.cr
│ │ └── value.cr
│ ├── area
│ ├── area
│ │ ├── draw
│ │ │ ├── fill_mode.cr
│ │ │ ├── line_cap.cr
│ │ │ ├── line_join.cr
│ │ │ ├── text_align.cr
│ │ │ ├── brush
│ │ │ │ ├── type.cr
│ │ │ │ └── gradient_stop.cr
│ │ │ ├── text_layout.cr
│ │ │ ├── text_layout
│ │ │ │ └── params.cr
│ │ │ ├── matrix.cr
│ │ │ ├── stroke_params.cr
│ │ │ ├── path.cr
│ │ │ └── brush.cr
│ │ ├── modifiers.cr
│ │ ├── attribute
│ │ │ ├── underline.cr
│ │ │ ├── underline_color.cr
│ │ │ ├── type.cr
│ │ │ └── open_type_features.cr
│ │ ├── window_resize_edge.cr
│ │ ├── key_event.cr
│ │ ├── ext_key.cr
│ │ ├── mouse_event.cr
│ │ ├── draw_params.cr
│ │ ├── attributed_string.cr
│ │ └── attribute.cr
│ └── area.cr
│ ├── text_stretch.cr
│ ├── text_weight.cr
│ ├── block_constructor.cr
│ ├── progress_bar.cr
│ ├── label.cr
│ ├── separator.cr
│ ├── image.cr
│ ├── image_view.cr
│ ├── button.cr
│ ├── spinbox.cr
│ ├── color_button.cr
│ ├── checkbox.cr
│ ├── radio_buttons.cr
│ ├── form.cr
│ ├── menu_item.cr
│ ├── grid.cr
│ ├── editable_combobox.cr
│ ├── font_button.cr
│ ├── group.cr
│ ├── entry.cr
│ ├── combobox.cr
│ ├── box.cr
│ ├── menu.cr
│ ├── date_time_picker.cr
│ ├── font_descriptor.cr
│ ├── slider.cr
│ ├── multiline_entry.cr
│ ├── tab.cr
│ ├── control.cr
│ └── tm.cr
├── renovate.json
├── .editorconfig
├── shard.yml
├── .gitignore
├── comctl32.manifest
├── .github
├── workflows
│ ├── docs.yml
│ ├── ci.yml
│ └── screenshot.yml
└── actions
│ └── screenshot
│ ├── macos-screenshot.sh
│ ├── action.yml
│ └── ubuntu-screenshot.sh
└── LICENSE
/comctl32.rc:
--------------------------------------------------------------------------------
1 | 1 24 "comctl32.manifest"
2 |
3 |
--------------------------------------------------------------------------------
/examples/zigzag/.gitignore:
--------------------------------------------------------------------------------
1 | /bin/
2 | /lib/
3 | *.lock
--------------------------------------------------------------------------------
/spec/spec_helper.cr:
--------------------------------------------------------------------------------
1 | require "spec"
2 | require "../src/uing"
3 |
--------------------------------------------------------------------------------
/examples/gallery/crys.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kojix2/uing/HEAD/examples/gallery/crys.png
--------------------------------------------------------------------------------
/src/uing/grid/align.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | enum Align
3 | Fill
4 | Start
5 | Center
6 | End
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/src/uing/text_italic.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | enum TextItalic
3 | Normal
4 | Oblique
5 | Italic
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/examples/md5_checker/resources/app_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kojix2/uing/HEAD/examples/md5_checker/resources/app_icon.png
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:recommended"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/src/uing/lib_ui/init_options.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | lib LibUI
3 | struct InitOptions
4 | size : LibC::SizeT
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/src/uing/version.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | VERSION = {{ `shards version #{__DIR__}`.chomp.stringify }}
3 | SOURCE = "https://github.com/kojix2/uing"
4 | end
5 |
--------------------------------------------------------------------------------
/examples/gallery/basic_msg_box.cr:
--------------------------------------------------------------------------------
1 | require "../../src/uing"
2 |
3 | UIng.init
4 |
5 | UIng.msg_box("Message", "Hello Crystal World!")
6 |
7 | UIng.uninit
8 |
--------------------------------------------------------------------------------
/examples/gallery/basic_msg_box_error.cr:
--------------------------------------------------------------------------------
1 | require "../../src/uing"
2 |
3 | UIng.init
4 |
5 | UIng.msg_box_error("Error", "An error occurred.")
6 |
7 | UIng.uninit
8 |
--------------------------------------------------------------------------------
/examples/video_player/.gitignore:
--------------------------------------------------------------------------------
1 | # Build artifacts
2 | /bin/
3 | /lib/
4 | /.shards/
5 | *.dwarf
6 | *.lock
7 |
8 | # macOS system files
9 | .DS_Store
10 |
--------------------------------------------------------------------------------
/spec/libui_spec.cr:
--------------------------------------------------------------------------------
1 | require "./spec_helper"
2 |
3 | describe UIng do
4 | it "has a version number" do
5 | UIng::VERSION.should be_a(String)
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/src/uing/grid/at.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | class Grid
3 | enum At
4 | Leading
5 | Top
6 | Trailing
7 | Bottom
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/src/uing/lib_ui/table_selection.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | lib LibUI
3 | struct TableSelection
4 | num_rows : LibC::Int
5 | rows : Pointer(LibC::Int)
6 | end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/src/uing/lib_ui/table_text_column_optional_params.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | lib LibUI
3 | struct TableTextColumnOptionalParams
4 | color_model_column : LibC::Int
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/src/uing/table/table/sort_indicator.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | class Table < Control
3 | enum SortIndicator
4 | None
5 | Ascending
6 | Descending
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*.cr]
4 | charset = utf-8
5 | end_of_line = lf
6 | insert_final_newline = true
7 | indent_style = space
8 | indent_size = 2
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/src/uing/lib_ui/table_params.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | lib LibUI
3 | struct TableParams
4 | model : Pointer(TableModel)
5 | row_background_color_model_column : LibC::Int
6 | end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/src/uing/area/area/draw/fill_mode.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | class Area < Control
3 | module Draw
4 | enum FillMode
5 | Winding
6 | Alternate
7 | end
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/src/uing/area/area/draw/line_cap.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | class Area < Control
3 | module Draw
4 | enum LineCap
5 | Flat
6 | Round
7 | Square
8 | end
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/src/uing/area/area/draw/line_join.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | class Area < Control
3 | module Draw
4 | enum LineJoin
5 | Miter
6 | Round
7 | Bevel
8 | end
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/src/uing/area/area/draw/text_align.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | class Area < Control
3 | module Draw
4 | enum TextAlign
5 | Left
6 | Center
7 | Right
8 | end
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/src/uing/table/table/value/type.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | class Table < Control
3 | class Value
4 | enum Type
5 | String
6 | Image
7 | Int
8 | Color
9 | end
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/src/uing/area/area/modifiers.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | class Area < Control
3 | @[Flags]
4 | enum Modifiers
5 | Ctrl = 1 << 0
6 | Alt = 1 << 1
7 | Shift = 1 << 2
8 | Super = 1 << 3
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/src/uing/table/table/selection/mode.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | class Table < Control
3 | class Selection
4 | enum Mode
5 | None
6 | ZeroOrOne
7 | One
8 | ZeroOrMany
9 | end
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/src/uing/area/area/attribute/underline.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | class Area < Control
3 | class Attribute
4 | enum Underline
5 | None
6 | Single
7 | Double
8 | Suggestion
9 | end
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/src/uing/text_stretch.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | enum TextStretch
3 | UltraCondensed
4 | ExtraCondensed
5 | Condensed
6 | SemiCondensed
7 | Normal
8 | SemiExpanded
9 | Expanded
10 | ExtraExpanded
11 | UltraExpanded
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/src/uing/area/area/attribute/underline_color.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | class Area < Control
3 | class Attribute
4 | enum UnderlineColor
5 | Custom
6 | Spelling
7 | Grammar
8 | Auxiliary
9 | end
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/examples/gallery/basic_window.cr:
--------------------------------------------------------------------------------
1 | require "../../src/uing"
2 |
3 | UIng.init
4 |
5 | window = UIng::Window.new("Window Example", 300, 100, margined: true)
6 | window.on_closing do
7 | UIng.quit
8 | true
9 | end
10 |
11 | window.show
12 |
13 | UIng.main
14 | UIng.uninit
15 |
--------------------------------------------------------------------------------
/examples/md5_checker/shard.yml:
--------------------------------------------------------------------------------
1 | name: md5checker
2 | version: 0.1.0
3 |
4 | dependencies:
5 | uing:
6 | github: kojix2/uing
7 | branch: main
8 |
9 | targets:
10 | md5checker:
11 | main: src/md5_checker.cr
12 |
13 | crystal: ">= 1.0.0"
14 |
15 | license: MIT
16 |
--------------------------------------------------------------------------------
/src/uing/lib_ui/draw_brush_gradient_stop.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | lib LibUI
3 | struct DrawBrushGradientStop
4 | pos : LibC::Double
5 | r : LibC::Double
6 | g : LibC::Double
7 | b : LibC::Double
8 | a : LibC::Double
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/examples/zigzag/shard.yml:
--------------------------------------------------------------------------------
1 | name: chipmunk
2 | version: 0.1.0
3 |
4 | dependencies:
5 | uing:
6 | github: kojix2/uing
7 | branch: main
8 | chipmunk:
9 | github: oprypin/crystal-chipmunk
10 | branch: master
11 |
12 | targets:
13 | zigzag:
14 | main: zigzag.cr
15 |
--------------------------------------------------------------------------------
/src/uing/area/area/window_resize_edge.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | class Area < Control
3 | enum WindowResizeEdge
4 | Left
5 | Top
6 | Right
7 | Bottom
8 | TopLeft
9 | TopRight
10 | BottomLeft
11 | BottomRight
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/src/uing/lib_ui/draw_matrix.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | lib LibUI
3 | struct DrawMatrix
4 | m11 : LibC::Double
5 | m12 : LibC::Double
6 | m21 : LibC::Double
7 | m22 : LibC::Double
8 | m31 : LibC::Double
9 | m32 : LibC::Double
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/src/uing/lib_ui/area_key_event.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | lib LibUI
3 | struct AreaKeyEvent
4 | key : LibC::Char
5 | ext_key : UIng::Area::ExtKey
6 | modifier : UIng::Area::Modifiers
7 | modifiers : UIng::Area::Modifiers
8 | up : LibC::Int
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/examples/zigzag/README.md:
--------------------------------------------------------------------------------
1 | # ZigZag
2 |
3 | Requirements:
4 |
5 | - [Chipmunk](https://github.com/slembcke/Chipmunk2D)
6 |
7 | ```sh
8 | sudo apt install libchipmunk-dev
9 | ```
10 |
11 | ## build
12 |
13 | ```sh
14 | shards build
15 | ```
16 |
17 | ## Run
18 |
19 | ```sh
20 | bin/zigzag
21 | ```
22 |
--------------------------------------------------------------------------------
/shard.yml:
--------------------------------------------------------------------------------
1 | name: uing
2 | version: 0.0.4
3 |
4 | authors:
5 | - kojix2 <2xijok@gmail.com>
6 |
7 | scripts:
8 | postinstall: crystal run download.cr
9 |
10 |
11 | development_dependencies:
12 | stumpy_png:
13 | github: stumpycr/stumpy_png
14 | version: "~> 5.0"
15 |
16 | license: MIT
17 |
--------------------------------------------------------------------------------
/src/uing/area/area/draw/brush/type.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | class Area < Control
3 | module Draw
4 | class Brush
5 | enum Type
6 | Solid
7 | LinearGradient
8 | RadialGradient
9 | Image
10 | end
11 | end
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/src/uing/lib_ui/draw_text_layout_params.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | lib LibUI
3 | struct DrawTextLayoutParams
4 | string : Pointer(AttributedString)
5 | default_font : Pointer(FontDescriptor)
6 | width : LibC::Double
7 | align : UIng::Area::Draw::TextAlign
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/examples/gallery/basic_label.cr:
--------------------------------------------------------------------------------
1 | require "../../src/uing"
2 |
3 | UIng.init
4 |
5 | window = UIng::Window.new("Label Example", 300, 100, margined: true)
6 | window.on_closing do
7 | UIng.quit
8 | true
9 | end
10 |
11 | label = UIng::Label.new("This is a label.")
12 |
13 | window.child = label
14 | window.show
15 |
16 | UIng.main
17 | UIng.uninit
18 |
--------------------------------------------------------------------------------
/examples/gallery/basic_separator.cr:
--------------------------------------------------------------------------------
1 | require "../../src/uing"
2 |
3 | UIng.init
4 |
5 | window = UIng::Window.new("Separator Example", 300, 100, margined: true)
6 | window.on_closing do
7 | UIng.quit
8 | true
9 | end
10 |
11 | separator = UIng::Separator.new(:horizontal)
12 |
13 | window.child = separator
14 | window.show
15 |
16 | UIng.main
17 | UIng.uninit
18 |
--------------------------------------------------------------------------------
/examples/md5_checker/.env:
--------------------------------------------------------------------------------
1 | # Build configuration for md5checker
2 | APP_NAME=md5checker
3 | VERSION=0.1.0
4 | APP_NAME_CAPITALIZED=Md5checker
5 | DESCRIPTION="A simple MD5 checksum verification tool built with Crystal and UIng"
6 | MAINTAINER=kojix2
7 | LICENSE=MIT
8 | URL=https://github.com/kojix2/uing
9 |
10 | # Windows-specific configuration
11 | INNO_SETUP_PATH=ISCC.exe
12 |
--------------------------------------------------------------------------------
/src/uing/area/area/attribute/type.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | class Area < Control
3 | class Attribute
4 | enum Type
5 | Family
6 | Size
7 | Weight
8 | Italic
9 | Stretch
10 | Color
11 | Background
12 | Underline
13 | UnderlineColor
14 | Features
15 | end
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/src/uing/lib_ui/area_draw_params.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | lib LibUI
3 | struct AreaDrawParams
4 | context : Pointer(DrawContext)
5 | area_width : LibC::Double
6 | area_height : LibC::Double
7 | clip_x : LibC::Double
8 | clip_y : LibC::Double
9 | clip_width : LibC::Double
10 | clip_height : LibC::Double
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/examples/gallery/basic_progressbar.cr:
--------------------------------------------------------------------------------
1 | require "../../src/uing"
2 |
3 | UIng.init
4 |
5 | window = UIng::Window.new("ProgressBar Example", 300, 100, margined: true)
6 | window.on_closing do
7 | UIng.quit
8 | true
9 | end
10 |
11 | progressbar = UIng::ProgressBar.new
12 | progressbar.value = 42
13 |
14 | window.child = progressbar
15 | window.show
16 |
17 | UIng.main
18 | UIng.uninit
19 |
--------------------------------------------------------------------------------
/examples/md5_checker/.gitignore:
--------------------------------------------------------------------------------
1 | # Build artifacts
2 | /bin/
3 | /lib/
4 | /.shards/
5 | *.dwarf
6 | *.lock
7 |
8 | # macOS app bundles
9 | *.app/
10 | /Md5checker.app/
11 | /MyApp.app/
12 |
13 | /resources/app_icon.icns
14 | /resources/app_icon.ico
15 |
16 | # Distribution files
17 | /dist/
18 | *.dmg
19 |
20 | # Staging directory
21 | /dmg_stage/
22 |
23 | # macOS system files
24 | .DS_Store
25 |
--------------------------------------------------------------------------------
/src/uing/lib_ui/draw_stroke_params.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | lib LibUI
3 | struct DrawStrokeParams
4 | cap : UIng::Area::Draw::LineCap
5 | join : UIng::Area::Draw::LineJoin
6 | thickness : LibC::Double
7 | miter_limit : LibC::Double
8 | dashes : Pointer(LibC::Double)
9 | num_dashes : LibC::SizeT
10 | dash_phase : LibC::Double
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/examples/basic_button_dsl.cr:
--------------------------------------------------------------------------------
1 | require "../src/uing"
2 |
3 | UIng.init do
4 | UIng::Window.new("Hello World", 300, 200) { |win|
5 | on_closing { UIng.quit; true }
6 | set_child {
7 | UIng::Button.new("Click me") {
8 | on_clicked {
9 | win.msg_box("Information", "You clicked the button")
10 | }
11 | }
12 | }
13 | show
14 | }
15 |
16 | UIng.main
17 | end
18 |
--------------------------------------------------------------------------------
/examples/video_player/shard.yml:
--------------------------------------------------------------------------------
1 | name: video_player
2 | version: 0.1.0
3 |
4 | authors:
5 | - kojix2 <2xijok@gmail.com>
6 |
7 | description: |
8 | Video player example using libmpv and UIng
9 |
10 | dependencies:
11 | uing:
12 | path: ../../ # Reference to parent uing library
13 |
14 | targets:
15 | video_player:
16 | main: video_player.cr
17 |
18 | crystal: 1.0.0
19 |
20 | license: MIT
21 |
--------------------------------------------------------------------------------
/src/uing/lib_ui/area_mouse_event.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | lib LibUI
3 | struct AreaMouseEvent
4 | x : LibC::Double
5 | y : LibC::Double
6 | area_width : LibC::Double
7 | area_height : LibC::Double
8 | down : LibC::Int
9 | up : LibC::Int
10 | count : LibC::Int
11 | modifiers : UIng::Area::Modifiers
12 | held1_to64 : UInt64
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/examples/basic_button.cr:
--------------------------------------------------------------------------------
1 | require "../src/uing"
2 |
3 | UIng.init
4 |
5 | window = UIng::Window.new("hello world", 300, 200)
6 | window.on_closing do
7 | UIng.quit
8 | true
9 | end
10 |
11 | button = UIng::Button.new("Button")
12 | button.on_clicked do
13 | window.msg_box("Information", "You clicked the button")
14 | end
15 |
16 | window.child = button
17 | window.show
18 |
19 | UIng.main
20 | UIng.uninit
21 |
--------------------------------------------------------------------------------
/src/uing/text_weight.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | enum TextWeight
3 | Minimum = 0
4 | Thin = 100
5 | UltraLight = 200
6 | Light = 300
7 | Book = 350
8 | Normal = 400
9 | Medium = 500
10 | SemiBold = 600
11 | Bold = 700
12 | UltraBold = 800
13 | Heavy = 900
14 | UltraHeavy = 950
15 | Maximum = 1000
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/examples/gallery/basic_multiline_entry.cr:
--------------------------------------------------------------------------------
1 | require "../../src/uing"
2 |
3 | UIng.init
4 |
5 | window = UIng::Window.new("MultilineEntry Example", 300, 100, margined: true)
6 | window.on_closing do
7 | UIng.quit
8 | true
9 | end
10 |
11 | multiline_entry = UIng::MultilineEntry.new
12 | multiline_entry.text = "Type here\nThis is a multiline entry."
13 |
14 | window.child = multiline_entry
15 | window.show
16 |
17 | UIng.main
18 | UIng.uninit
19 |
--------------------------------------------------------------------------------
/src/uing/lib_ui/font_descriptor.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | lib LibUI
3 | # Use UIng.free_font_descriptor to free the LibC::Char memory.
4 | # Use UIng.free_font_button_font to free the memory if the font is from a font button.
5 | struct FontDescriptor
6 | family : Pointer(LibC::Char)
7 | size : LibC::Double
8 | weight : TextWeight
9 | italic : TextItalic
10 | stretch : TextStretch
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/examples/gallery/basic_button.cr:
--------------------------------------------------------------------------------
1 | require "../../src/uing"
2 |
3 | UIng.init
4 |
5 | window = UIng::Window.new("Button Examples", 300, 100, margined: true)
6 | window.on_closing do
7 | UIng.quit
8 | true
9 | end
10 |
11 | button = UIng::Button.new("Button") do
12 | on_clicked do
13 | window.msg_box("Information", "You clicked the button")
14 | end
15 | end
16 |
17 | window.child = button
18 | window.show
19 |
20 | UIng.main
21 | UIng.uninit
22 |
--------------------------------------------------------------------------------
/examples/gallery/basic_entry.cr:
--------------------------------------------------------------------------------
1 | require "../../src/uing"
2 |
3 | UIng.init
4 |
5 | window = UIng::Window.new("Entry Example", 300, 100, margined: true)
6 | window.on_closing do
7 | UIng.quit
8 | true
9 | end
10 |
11 | entry = UIng::Entry.new
12 | entry.text = "Type here"
13 | entry.on_changed do
14 | window.msg_box("Entry Changed", "Text: #{entry.text}")
15 | end
16 |
17 | window.child = entry
18 | window.show
19 |
20 | UIng.main
21 | UIng.uninit
22 |
--------------------------------------------------------------------------------
/examples/gallery/basic_checkbox.cr:
--------------------------------------------------------------------------------
1 | require "../../src/uing"
2 |
3 | UIng.init
4 |
5 | window = UIng::Window.new("Checkbox Example", 300, 100, margined: true)
6 | window.on_closing do
7 | UIng.quit
8 | true
9 | end
10 |
11 | checkbox = UIng::Checkbox.new("Check me")
12 | checkbox.on_toggled do |checked|
13 | window.msg_box("Checkbox", "Checked: #{checked}")
14 | end
15 |
16 | window.child = checkbox
17 | window.show
18 |
19 | UIng.main
20 | UIng.uninit
21 |
--------------------------------------------------------------------------------
/examples/gallery/basic_slider.cr:
--------------------------------------------------------------------------------
1 | require "../../src/uing"
2 |
3 | UIng.init
4 |
5 | window = UIng::Window.new("Slider Example", 300, 100, margined: true)
6 | window.on_closing do
7 | UIng.quit
8 | true
9 | end
10 |
11 | slider = UIng::Slider.new(0, 100)
12 | slider.value = 42
13 | slider.on_changed do |v|
14 | window.msg_box("Slider Changed", "Value: #{v}")
15 | end
16 |
17 | window.child = slider
18 | window.show
19 |
20 | UIng.main
21 | UIng.uninit
22 |
--------------------------------------------------------------------------------
/examples/gallery/basic_spinbox.cr:
--------------------------------------------------------------------------------
1 | require "../../src/uing"
2 |
3 | UIng.init
4 |
5 | window = UIng::Window.new("Spinbox Example", 300, 100, margined: true)
6 | window.on_closing do
7 | UIng.quit
8 | true
9 | end
10 |
11 | spinbox = UIng::Spinbox.new(0, 100, value: 42) do
12 | on_changed do |v|
13 | window.msg_box("Spinbox Changed", "Value: #{v}")
14 | end
15 | end
16 |
17 | window.child = spinbox
18 | window.show
19 |
20 | UIng.main
21 | UIng.uninit
22 |
--------------------------------------------------------------------------------
/src/uing/block_constructor.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | private module BlockConstructor
3 | macro block_constructor
4 | def self.new(*args, &block)
5 | instance = new(*args)
6 | with instance yield(instance)
7 | instance
8 | end
9 |
10 | def self.new(*args, **kwargs, &block)
11 | instance = new(*args, **kwargs)
12 | with instance yield(instance)
13 | instance
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/examples/gallery/basic_date_time_picker.cr:
--------------------------------------------------------------------------------
1 | require "../../src/uing"
2 |
3 | UIng.init
4 |
5 | window = UIng::Window.new("DateTimePicker Example", 300, 100, margined: true)
6 | window.on_closing do
7 | UIng.quit
8 | true
9 | end
10 |
11 | date_picker = UIng::DateTimePicker.new(:date) do
12 | on_changed do |tm|
13 | window.msg_box("Date Changed", tm.to_s)
14 | end
15 | end
16 |
17 | window.child = date_picker
18 | window.show
19 |
20 | UIng.main
21 | UIng.uninit
22 |
--------------------------------------------------------------------------------
/examples/basic_window.cr:
--------------------------------------------------------------------------------
1 | require "../src/uing"
2 |
3 | UIng.init do
4 | UIng::Window.new("Hello", 300, 200) do
5 | on_closing do
6 | UIng.quit; true
7 | end
8 | on_position_changed do |a, b|
9 | p "x: #{a}, y: #{b}"
10 | end
11 | on_content_size_changed do |w, h|
12 | p "width: #{w}, height: #{h}"
13 | end
14 | on_focus_changed do |focused|
15 | p "focused: #{focused}"
16 | end
17 | show
18 | end
19 | UIng.main
20 | end
21 |
--------------------------------------------------------------------------------
/examples/gallery/basic_combobox.cr:
--------------------------------------------------------------------------------
1 | require "../../src/uing"
2 |
3 | UIng.init
4 |
5 | window = UIng::Window.new("Combobox Example", 300, 100, margined: true)
6 | window.on_closing do
7 | UIng.quit
8 | true
9 | end
10 |
11 | combobox = UIng::Combobox.new(["Item 1", "Item 2", "Item 3"])
12 | combobox.on_selected do |idx|
13 | window.msg_box("Combobox Changed", "Selected index: #{idx}")
14 | end
15 |
16 | window.child = combobox
17 | window.show
18 |
19 | UIng.main
20 | UIng.uninit
21 |
--------------------------------------------------------------------------------
/src/uing/lib_ui/draw_brush.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | lib LibUI
3 | struct DrawBrush
4 | type : UIng::Area::Draw::Brush::Type
5 | r : LibC::Double
6 | g : LibC::Double
7 | b : LibC::Double
8 | a : LibC::Double
9 | x0 : LibC::Double
10 | y0 : LibC::Double
11 | x1 : LibC::Double
12 | y1 : LibC::Double
13 | outer_radius : LibC::Double
14 | stops : Pointer(DrawBrushGradientStop)
15 | num_stops : LibC::SizeT
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/examples/gallery/basic_color_button.cr:
--------------------------------------------------------------------------------
1 | require "../../src/uing"
2 |
3 | UIng.init
4 |
5 | window = UIng::Window.new("ColorButton Example", 300, 100, margined: true)
6 | window.on_closing do
7 | UIng.quit
8 | true
9 | end
10 |
11 | color_button = UIng::ColorButton.new do
12 | set_color(255, 0, 0, 255)
13 | on_changed do |r, g, b, a|
14 | window.msg_box("Color Changed", "R=#{r}, G=#{g}, B=#{b}, A=#{a}")
15 | end
16 | end
17 |
18 | window.child = color_button
19 | window.show
20 |
21 | UIng.main
22 | UIng.uninit
23 |
--------------------------------------------------------------------------------
/examples/gallery/basic_font_button.cr:
--------------------------------------------------------------------------------
1 | require "../../src/uing"
2 |
3 | UIng.init
4 |
5 | window = UIng::Window.new("FontButton Example", 300, 100, margined: true)
6 | window.on_closing do
7 | UIng.quit
8 | true
9 | end
10 |
11 | font_button = UIng::FontButton.new do
12 | on_changed do |font_descriptor|
13 | window.msg_box("Font Changed", "Family: #{font_descriptor.family}\nSize: #{font_descriptor.size}")
14 | end
15 | end
16 |
17 | window.child = font_button
18 | window.show
19 |
20 | UIng.main
21 | UIng.uninit
22 |
--------------------------------------------------------------------------------
/examples/gallery/basic_editable_combobox.cr:
--------------------------------------------------------------------------------
1 | require "../../src/uing"
2 |
3 | UIng.init
4 |
5 | window = UIng::Window.new("EditableCombobox Example", 300, 100, margined: true)
6 | window.on_closing do
7 | UIng.quit
8 | true
9 | end
10 |
11 | editable_combobox = UIng::EditableCombobox.new(["Item 1", "Item 2", "Item 3"])
12 | editable_combobox.on_changed do |text|
13 | window.msg_box("EditableCombobox Changed", "Text: #{text}")
14 | end
15 |
16 | window.child = editable_combobox
17 | window.show
18 |
19 | UIng.main
20 | UIng.uninit
21 |
--------------------------------------------------------------------------------
/examples/gallery/basic_tab.cr:
--------------------------------------------------------------------------------
1 | require "../../src/uing"
2 |
3 | UIng.init
4 |
5 | window = UIng::Window.new("Tab Example", 300, 100, margined: true)
6 | window.on_closing do
7 | UIng.quit
8 | true
9 | end
10 |
11 | tab = UIng::Tab.new
12 | tab.append("Tab 1", UIng::Label.new("This is Tab 1"))
13 | tab.append("Tab 2", UIng::Label.new("This is Tab 2"))
14 | tab.on_selected do |idx|
15 | window.msg_box("Tab Changed", "Selected index: #{idx}")
16 | end
17 |
18 | window.child = tab
19 | window.show
20 |
21 | UIng.main
22 | UIng.uninit
23 |
--------------------------------------------------------------------------------
/src/uing/progress_bar.cr:
--------------------------------------------------------------------------------
1 | require "./control"
2 |
3 | module UIng
4 | class ProgressBar < Control
5 | block_constructor
6 |
7 | def initialize
8 | @ref_ptr = LibUI.new_progress_bar
9 | end
10 |
11 | def destroy
12 | super
13 | end
14 |
15 | def value : Int32
16 | LibUI.progress_bar_value(@ref_ptr)
17 | end
18 |
19 | def value=(n : Int32) : Nil
20 | LibUI.progress_bar_set_value(@ref_ptr, n)
21 | end
22 |
23 | def to_unsafe
24 | @ref_ptr
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /docs/
2 | /lib/
3 | /bin/
4 | /.shards/
5 | *.dwarf
6 |
7 | # Libraries don't need dependency lock
8 | # Dependencies will be locked in applications that use them
9 | /shard.lock
10 |
11 | *.a
12 | *.so
13 | *.dylib
14 | *.dll
15 | *.lib
16 |
17 | *.exe
18 | *.pdb
19 | *.exp
20 |
21 | *.res
22 |
23 | # Generated by download.cr
24 | /libui/
25 |
26 | # executables
27 | basic_area
28 | basic_button
29 | basic_date_time_picker
30 | basic_draw_text
31 | basic_entry
32 | basic_font_button
33 | basic_multiline_entry
34 | basic_table
35 | basic_window
36 | control_gallery
37 |
--------------------------------------------------------------------------------
/comctl32.manifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/uing/label.cr:
--------------------------------------------------------------------------------
1 | require "./control"
2 |
3 | module UIng
4 | class Label < Control
5 | block_constructor
6 |
7 | def initialize(text : String)
8 | @ref_ptr = LibUI.new_label(text)
9 | end
10 |
11 | def destroy
12 | super
13 | end
14 |
15 | def text : String?
16 | str_ptr = LibUI.label_text(@ref_ptr)
17 | UIng.string_from_pointer(str_ptr)
18 | end
19 |
20 | def text=(text : String) : Nil
21 | LibUI.label_set_text(@ref_ptr, text)
22 | end
23 |
24 | def to_unsafe
25 | @ref_ptr
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/examples/gallery/basic_radio_buttons.cr:
--------------------------------------------------------------------------------
1 | require "../../src/uing"
2 |
3 | UIng.init
4 |
5 | window = UIng::Window.new("RadioButtons Example", 300, 100, margined: true)
6 | window.on_closing do
7 | UIng.quit
8 | true
9 | end
10 |
11 | group = UIng::Group.new("Options")
12 |
13 | radio_buttons = UIng::RadioButtons.new(["Option 1", "Option 2", "Option 3"])
14 | radio_buttons.on_selected do |idx|
15 | window.msg_box("RadioButtons Changed", "Selected index: #{idx}")
16 | end
17 |
18 | group.child = radio_buttons
19 | window.child = group
20 | window.show
21 |
22 | UIng.main
23 | UIng.uninit
24 |
--------------------------------------------------------------------------------
/.github/workflows/docs.yml:
--------------------------------------------------------------------------------
1 | name: docs
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - uses: actions/checkout@v6
14 | - uses: crystal-lang/install-crystal@v1
15 | - name: Install dependencies
16 | run: shards install
17 | - name: Generate document
18 | run: crystal docs
19 | - name: Deploy to GitHub Pages
20 | uses: peaceiris/actions-gh-pages@v4
21 | with:
22 | github_token: ${{ secrets.GITHUB_TOKEN }}
23 | publish_dir: ./docs
24 |
--------------------------------------------------------------------------------
/src/uing/separator.cr:
--------------------------------------------------------------------------------
1 | require "./control"
2 |
3 | module UIng
4 | class Separator < Control
5 | block_constructor
6 |
7 | def initialize(orientation : (Symbol | String))
8 | case orientation.to_s
9 | when "horizontal"
10 | @ref_ptr = LibUI.new_horizontal_separator
11 | when "vertical"
12 | @ref_ptr = LibUI.new_vertical_separator
13 | else
14 | raise "Invalid orientation: #{orientation}"
15 | end
16 | end
17 |
18 | def destroy
19 | super
20 | end
21 |
22 | def to_unsafe
23 | @ref_ptr
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/examples/basic_entry.cr:
--------------------------------------------------------------------------------
1 | require "../src/uing"
2 |
3 | UIng.init
4 |
5 | window = UIng::Window.new("Basic Entry", 300, 50) do
6 | on_closing do
7 | UIng.quit; true
8 | end
9 | show
10 | end
11 |
12 | box = UIng::Box.new(:horizontal) do
13 | entry = UIng::Entry.new(:password) do
14 | on_changed do |text|
15 | puts text
16 | end
17 | end
18 | append(entry, stretchy: true)
19 |
20 | button = UIng::Button.new("Button") do
21 | on_clicked do
22 | window.msg_box("You entered", entry.text || "")
23 | end
24 | end
25 | append(button)
26 | end
27 | window.child = box
28 |
29 | UIng.main
30 | UIng.uninit
31 |
--------------------------------------------------------------------------------
/examples/gallery/basic_area.cr:
--------------------------------------------------------------------------------
1 | require "../../src/uing"
2 |
3 | UIng.init
4 |
5 | window = UIng::Window.new("Area Example", 300, 100)
6 | window.on_closing do
7 | UIng.quit
8 | true
9 | end
10 |
11 | handler = UIng::Area::Handler.new
12 | handler.draw do |area, params|
13 | brush = UIng::Area::Draw::Brush.new(:solid, 0.4, 0.4, 0.8, 1.0)
14 | params.context.fill_path(brush) do |path|
15 | path.add_rectangle(0, 0, 100, 100)
16 | end
17 | end
18 |
19 | area = UIng::Area.new(handler)
20 | box = UIng::Box.new(:vertical, padded: true)
21 | box.append(area, stretchy: true)
22 | window.child = box
23 |
24 | window.show
25 |
26 | UIng.main
27 | UIng.uninit
28 |
--------------------------------------------------------------------------------
/examples/md5_checker/src/md5_checker.cr:
--------------------------------------------------------------------------------
1 | require "./checker"
2 | require "./table_handler"
3 | require "./app"
4 |
5 | # Redirect stdout and stderr to temp file when not in debug mode
6 | # This is needed for Windows builds with -mwindows or /SUBSYSTEM:WINDOWS
7 | {% unless flag?(:debug) %}
8 | temp_output = File.tempfile("md5checker_output")
9 | STDOUT.reopen(temp_output)
10 | STDERR.reopen(temp_output)
11 |
12 | begin
13 | # Run the application
14 | app = MD5CheckerApp.new
15 | app.run
16 | ensure
17 | temp_output.delete if temp_output
18 | end
19 | {% else %}
20 | # Run the application
21 | app = MD5CheckerApp.new
22 | app.run
23 | {% end %}
24 |
--------------------------------------------------------------------------------
/src/uing/image.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | class Image
3 | @released : Bool = false
4 |
5 | def initialize(@ref_ptr : Pointer(LibUI::Image))
6 | end
7 |
8 | def initialize(width : Int32, height : Int32)
9 | @ref_ptr = LibUI.new_image(width, height)
10 | end
11 |
12 | def append(pixels, pixel_width : Int32, pixel_height : Int32, byte_stride : Int32) : Nil
13 | LibUI.image_append(@ref_ptr, pixels, pixel_width, pixel_height, byte_stride)
14 | end
15 |
16 | def free : Nil
17 | return if @released
18 | LibUI.free_image(@ref_ptr)
19 | @released = true
20 | end
21 |
22 | def to_unsafe
23 | @ref_ptr
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/src/uing/table/table/text_column_optional_params.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | class Table < Control
3 | class TextColumnOptionalParams
4 | include BlockConstructor; block_constructor
5 |
6 | def initialize(color_model_column : LibC::Int)
7 | @cstruct = LibUI::TableTextColumnOptionalParams.new
8 | @cstruct.color_model_column = color_model_column
9 | end
10 |
11 | def color_model_column
12 | @cstruct.color_model_column
13 | end
14 |
15 | def color_model_column=(value : LibC::Int)
16 | @cstruct.color_model_column = value
17 | end
18 |
19 | def to_unsafe
20 | pointerof(@cstruct)
21 | end
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/src/uing/lib_ui/control.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | lib LibUI
3 | struct Control
4 | signature : UInt32
5 | os_signature : UInt32
6 | type_signature : UInt32
7 | destroy : (Pointer(Control) -> Void)
8 | handle : (Pointer(Control) -> Pointer(Void))
9 | parent : (Pointer(Control) -> Pointer(Control))
10 | set_parent : (Pointer(Control), Pointer(Control) -> Void)
11 | toplevel : (Pointer(Control) -> LibC::Int)
12 | visible : (Pointer(Control) -> LibC::Int)
13 | show : (Pointer(Control) -> Void)
14 | hide : (Pointer(Control) -> Void)
15 | enabled : (Pointer(Control) -> LibC::Int)
16 | enable : (Pointer(Control) -> Void)
17 | disable : (Pointer(Control) -> Void)
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/src/uing/area/area/key_event.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | class Area < Control
3 | # This class provides read-only access to key event properties.
4 | class KeyEvent
5 | def initialize(ref_ptr : LibUI::AreaKeyEvent*)
6 | @cstruct = ref_ptr.value
7 | end
8 |
9 | def key : Char
10 | @cstruct.key.chr
11 | end
12 |
13 | def ext_key : ExtKey
14 | @cstruct.ext_key
15 | end
16 |
17 | def modifier : Modifiers
18 | @cstruct.modifier
19 | end
20 |
21 | def modifiers : Modifiers
22 | @cstruct.modifiers
23 | end
24 |
25 | def up : Int32
26 | @cstruct.up
27 | end
28 |
29 | def to_unsafe
30 | pointerof(@cstruct)
31 | end
32 | end
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/src/uing/lib_ui/tm.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | lib LibUI
3 | {% if flag?(:windows) %}
4 | struct TM
5 | sec : LibC::Int
6 | min : LibC::Int
7 | hour : LibC::Int
8 | mday : LibC::Int
9 | mon : LibC::Int
10 | year : LibC::Int
11 | wday : LibC::Int
12 | yday : LibC::Int
13 | isdst : LibC::Int
14 | end
15 | {% else %}
16 | struct TM
17 | sec : LibC::Int
18 | min : LibC::Int
19 | hour : LibC::Int
20 | mday : LibC::Int
21 | mon : LibC::Int
22 | year : LibC::Int
23 | wday : LibC::Int
24 | yday : LibC::Int
25 | isdst : LibC::Int
26 | gmtoff : LibC::Long
27 | zone : Pointer(LibC::Char)
28 | end
29 | {% end %}
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/examples/md5_checker/src/table_handler.cr:
--------------------------------------------------------------------------------
1 | require "uing"
2 | require "./checker"
3 |
4 | # Table model handler for MD5 checker
5 | class MD5TableHandler
6 | def initialize
7 | @handler = UIng::Table::Model::Handler.new do
8 | num_columns { 3 }
9 | column_type { |i| UIng::Table::Value::Type::String }
10 | num_rows { MD5Checker.instance.result_count }
11 | cell_value { |row, column|
12 | value = MD5Checker.instance.result_at(row, column)
13 | UIng::Table::Value.new(value)
14 | }
15 | set_cell_value { |row, column, value| }
16 | end
17 | end
18 |
19 | # Get the underlying handler
20 | def handler
21 | @handler
22 | end
23 |
24 | # Create a table model
25 | def create_model
26 | UIng::Table::Model.new(@handler)
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/src/uing/area/area/ext_key.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | class Area < Control
3 | enum ExtKey
4 | Escape = 1
5 | Insert # equivalent to "Help" on Apple keyboards
6 | Delete
7 | Home
8 | End
9 | PageUp
10 | PageDown
11 | Up
12 | Down
13 | Left
14 | Right
15 | F1 # F1..F12 are guaranteed to be consecutive
16 | F2
17 | F3
18 | F4
19 | F5
20 | F6
21 | F7
22 | F8
23 | F9
24 | F10
25 | F11
26 | F12
27 | N0 # numpad keys; independent of Num Lock state
28 | N1 # N0..N9 are guaranteed to be consecutive
29 | N2
30 | N3
31 | N4
32 | N5
33 | N6
34 | N7
35 | N8
36 | N9
37 | NDot
38 | NEnter
39 | NAdd
40 | NSubtract
41 | NMultiply
42 | NDivide
43 | end
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/examples/md5_checker/README.md:
--------------------------------------------------------------------------------
1 | # MD5 Checker
2 |
3 | A simple MD5 checksum verification tool built with Crystal and UIng.
4 |
5 | ## Usage
6 |
7 | ```bash
8 | shards install
9 | shards build
10 | bin/md5checker
11 | ```
12 |
13 | ## Packaging
14 |
15 | ### macOS
16 |
17 | Create .app bundle and .dmg:
18 | ```bash
19 | ./build-mac.sh
20 | ```
21 |
22 | Create .pkg installer (requires fpm):
23 | ```bash
24 | ./build-mac-pkg.sh
25 | ```
26 |
27 | ### Linux/Debian
28 |
29 | Create .deb package (requires fpm):
30 | ```bash
31 | ./build-deb.sh
32 | ```
33 |
34 | ### Windows
35 |
36 | Create installer (requires Inno Setup):
37 | ```cmd
38 | build-win.bat
39 | ```
40 |
41 | ## MD5 File Format
42 |
43 | ```
44 | d41d8cd98f00b204e9800998ecf8427e file1.txt
45 | 900150983cd24fb0d6963f7d28e17f72 file2.txt
46 | ```
47 |
48 | Each line contains an MD5 hash followed by a space and the filename.
49 |
--------------------------------------------------------------------------------
/examples/gallery/basic_box_vertical.cr:
--------------------------------------------------------------------------------
1 | require "../../src/uing"
2 |
3 | UIng.init
4 |
5 | window = UIng::Window.new("Box Vertical Example", 200, 150, margined: true)
6 | window.on_closing do
7 | UIng.quit
8 | true
9 | end
10 |
11 | box = UIng::Box.new(:vertical, padded: true)
12 |
13 | button1 = UIng::Button.new("Button1") do
14 | on_clicked do
15 | window.msg_box("Information", "You clicked Button1")
16 | end
17 | end
18 |
19 | button2 = UIng::Button.new("Button2") do
20 | on_clicked do
21 | window.msg_box("Information", "You clicked Button2")
22 | end
23 | end
24 |
25 | button3 = UIng::Button.new("Button3") do
26 | on_clicked do
27 | window.msg_box("Information", "You clicked Button3")
28 | end
29 | end
30 |
31 | box.append(button1)
32 | box.append(button2)
33 | box.append(button3)
34 |
35 | window.child = box
36 | window.show
37 |
38 | UIng.main
39 | UIng.uninit
40 |
--------------------------------------------------------------------------------
/examples/gallery/basic_box_horizontal.cr:
--------------------------------------------------------------------------------
1 | require "../../src/uing"
2 |
3 | UIng.init
4 |
5 | window = UIng::Window.new("Box Horizontal Example", 400, 100, margined: true)
6 | window.on_closing do
7 | UIng.quit
8 | true
9 | end
10 |
11 | box = UIng::Box.new(:horizontal, padded: true)
12 |
13 | button1 = UIng::Button.new("Button1") do
14 | on_clicked do
15 | window.msg_box("Information", "You clicked Button1")
16 | end
17 | end
18 |
19 | button2 = UIng::Button.new("Button2") do
20 | on_clicked do
21 | window.msg_box("Information", "You clicked Button2")
22 | end
23 | end
24 |
25 | button3 = UIng::Button.new("Button3") do
26 | on_clicked do
27 | window.msg_box("Information", "You clicked Button3")
28 | end
29 | end
30 |
31 | box.append(button1)
32 | box.append(button2)
33 | box.append(button3)
34 |
35 | window.child = box
36 | window.show
37 |
38 | UIng.main
39 | UIng.uninit
40 |
--------------------------------------------------------------------------------
/src/uing/table/table/params.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | class Table < Control
3 | class Params
4 | include BlockConstructor; block_constructor
5 |
6 | def initialize(model : Model, row_background_color_model_column : LibC::Int = -1)
7 | @cstruct = LibUI::TableParams.new
8 | @cstruct.model = model.to_unsafe
9 | @cstruct.row_background_color_model_column = row_background_color_model_column
10 | end
11 |
12 | def model
13 | TableModel.new(@cstruct.model)
14 | end
15 |
16 | def model=(value : Model)
17 | @cstruct.model = value.to_unsafe
18 | end
19 |
20 | def row_background_color_model_column
21 | @cstruct.row_background_color_model_column
22 | end
23 |
24 | def row_background_color_model_column=(value : LibC::Int)
25 | @cstruct.row_background_color_model_column = value
26 | end
27 |
28 | def to_unsafe
29 | pointerof(@cstruct)
30 | end
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/src/uing/image_view.cr:
--------------------------------------------------------------------------------
1 | require "./control"
2 |
3 | module UIng
4 | class ImageView < Control
5 | enum ContentMode
6 | Center = 0
7 | Fit = 1
8 | end
9 |
10 | block_constructor
11 |
12 | def initialize
13 | @ref_ptr = LibUI.new_image_view
14 | end
15 |
16 | def initialize(image : Image, mode : ContentMode = ContentMode::Fit)
17 | @ref_ptr = LibUI.new_image_view
18 | LibUI.image_view_set_image(@ref_ptr, image.to_unsafe)
19 | LibUI.image_view_set_content_mode(@ref_ptr, mode)
20 | end
21 |
22 | def image=(image : Image?)
23 | if image
24 | LibUI.image_view_set_image(@ref_ptr, image.to_unsafe)
25 | else
26 | LibUI.image_view_set_image(@ref_ptr, Pointer(LibUI::Image).null)
27 | end
28 | end
29 |
30 | def content_mode=(mode : ContentMode)
31 | LibUI.image_view_set_content_mode(@ref_ptr, LibUI::ImageViewContentMode.new(mode.value))
32 | end
33 |
34 | def to_unsafe
35 | @ref_ptr
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/src/uing/area/area/mouse_event.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | # This class provides read-only access to key event properties.
3 | class Area < Control
4 | class MouseEvent
5 | def initialize(@ref_ptr : LibUI::AreaMouseEvent*)
6 | @cstruct = @ref_ptr.value
7 | end
8 |
9 | def x : Float64
10 | @cstruct.x
11 | end
12 |
13 | def y : Float64
14 | @cstruct.y
15 | end
16 |
17 | def area_width : Float64
18 | @cstruct.area_width
19 | end
20 |
21 | def area_height : Float64
22 | @cstruct.area_height
23 | end
24 |
25 | def down : Int32
26 | @cstruct.down
27 | end
28 |
29 | def up : Int32
30 | @cstruct.up
31 | end
32 |
33 | def count : Int32
34 | @cstruct.count
35 | end
36 |
37 | def modifiers : Modifiers
38 | @cstruct.modifiers
39 | end
40 |
41 | def held1_to64 : UInt64
42 | @cstruct.held1_to64
43 | end
44 |
45 | def to_unsafe
46 | pointerof(@cstruct)
47 | end
48 | end
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/src/uing/lib_ui/table_model_handler.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | lib LibUI
3 | struct TableModelHandler
4 | num_columns : (Pointer(TableModelHandler), Pointer(TableModel) -> LibC::Int)
5 | column_type : (Pointer(TableModelHandler), Pointer(TableModel), LibC::Int -> UIng::Table::Value::Type)
6 | num_rows : (Pointer(TableModelHandler), Pointer(TableModel) -> LibC::Int)
7 | cell_value : (Pointer(TableModelHandler), Pointer(TableModel), LibC::Int, LibC::Int -> Pointer(TableValue))
8 | set_cell_value : (Pointer(TableModelHandler), Pointer(TableModel), LibC::Int, LibC::Int, Pointer(TableValue) -> Void)
9 | end
10 |
11 | # Extended handler structure that contains the base TableModelHandler
12 | # and individual boxes for each callback
13 | @[Packed]
14 | struct TableModelHandlerExtended
15 | base_handler : TableModelHandler
16 | num_columns_box : Pointer(Void)
17 | column_type_box : Pointer(Void)
18 | num_rows_box : Pointer(Void)
19 | cell_value_box : Pointer(Void)
20 | set_cell_value_box : Pointer(Void)
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/src/uing/area/area/draw_params.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | class Area < Control
3 | module Draw
4 | # This class provides read-only access to area draw parameters.
5 | class Params
6 | include BlockConstructor; block_constructor
7 |
8 | def initialize(ptr_ref : LibUI::AreaDrawParams*)
9 | @cstruct = ptr_ref.value
10 | end
11 |
12 | def context : Context
13 | Context.new(@cstruct.context)
14 | end
15 |
16 | def area_width : Float64
17 | @cstruct.area_width
18 | end
19 |
20 | def area_height : Float64
21 | @cstruct.area_height
22 | end
23 |
24 | def clip_x : Float64
25 | @cstruct.clip_x
26 | end
27 |
28 | def clip_y : Float64
29 | @cstruct.clip_y
30 | end
31 |
32 | def clip_width : Float64
33 | @cstruct.clip_width
34 | end
35 |
36 | def clip_height : Float64
37 | @cstruct.clip_height
38 | end
39 |
40 | def to_unsafe
41 | pointerof(@cstruct)
42 | end
43 | end
44 | end
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 kojix2 <2xijok@gmail.com>
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/examples/gallery/basic_table.cr:
--------------------------------------------------------------------------------
1 | require "../../src/uing"
2 |
3 | UIng.init
4 |
5 | main_window = UIng::Window.new("Table Example", 300, 100)
6 |
7 | hbox = UIng::Box.new :horizontal
8 | main_window.child = hbox
9 |
10 | data = [
11 | %w[Windows Microsoft],
12 | %w[macOS Apple],
13 | %w[Ubuntu Canonical],
14 | ]
15 |
16 | model_handler = UIng::Table::Model::Handler.new do
17 | num_columns { 2 }
18 | column_type { |column| UIng::Table::Value::Type::String }
19 | num_rows { data.size }
20 | cell_value { |row, column| UIng::Table::Value.new(data[row][column]) }
21 | set_cell_value { |row, column, value| }
22 | end
23 |
24 | table_model = UIng::Table::Model.new(model_handler)
25 |
26 | table = UIng::Table.new(table_model) do
27 | append_text_column("OS", 0, -1)
28 | append_text_column("Vendor", 1, -1)
29 | end
30 |
31 | hbox.append(table, true)
32 | main_window.show
33 |
34 | main_window.on_closing do
35 | # FIXME: https://github.com/kojix2/uing/issues/6
36 | hbox.delete(0)
37 | table.destroy # Destroy table firs
38 | table_model.free # Then free model
39 |
40 | UIng.quit
41 | true
42 | end
43 |
44 | UIng.main
45 | UIng.uninit
46 |
--------------------------------------------------------------------------------
/examples/gallery/basic_image_view.cr:
--------------------------------------------------------------------------------
1 | require "../../src/uing"
2 | require "http/client"
3 | require "stumpy_png"
4 |
5 | fname = File.join(__DIR__, "crys.png")
6 | canvas = StumpyPNG.read(fname)
7 | width = canvas.width.to_i32
8 | height = canvas.height.to_i32
9 |
10 | pixels = Bytes.new(width * height * 4)
11 | (0...height).each do |y|
12 | (0...width).each do |x|
13 | offset = (y * width + x) * 4
14 | r, g, b, a = canvas[x, y].to_rgba
15 | pixels[offset] = r
16 | pixels[offset + 1] = g
17 | pixels[offset + 2] = b
18 | pixels[offset + 3] = a || 255_u8
19 | end
20 | end
21 |
22 | UIng.init
23 |
24 | window = UIng::Window.new("ImageView Example", 300, 200, margined: true)
25 | window.on_closing do
26 | UIng.quit
27 | true
28 | end
29 |
30 | vbox = UIng::Box.new(:vertical)
31 |
32 | image = UIng::Image.new(width, height)
33 | image.append(pixels, width, height, width * 4)
34 | image_view = UIng::ImageView.new(image, :fit)
35 | image.free
36 |
37 | label = UIng::Label.new(fname)
38 |
39 | vbox.append(image_view, stretchy: true)
40 | vbox.append(label)
41 |
42 | window.set_child(vbox)
43 | window.show
44 |
45 | UIng.main
46 | UIng.uninit
47 |
--------------------------------------------------------------------------------
/examples/gallery/basic_menu.cr:
--------------------------------------------------------------------------------
1 | require "../../src/uing"
2 |
3 | UIng.init
4 |
5 | UIng::Menu.new("File") do
6 | append_item("Open").on_clicked do |w|
7 | Window.open_file
8 | end
9 | append_separator
10 | append_preferences_item.on_clicked do |w|
11 | w.msg_box("Preferences", "Preferences clicked")
12 | end
13 | append_separator
14 | append_quit_item
15 | end
16 |
17 | UIng::Menu.new("Edit") do
18 | append_check_item("Check", checked: false).on_clicked do |w|
19 | # No-op
20 | end
21 | append_separator
22 | append_item("Click").on_clicked do |w|
23 | w.msg_box("Click", "Click menu clicked")
24 | end
25 | end
26 |
27 | UIng::Menu.new("Help") do
28 | append_about_item.on_clicked do |w|
29 | w.msg_box("About", "Menu example")
30 | end
31 | end
32 |
33 | # Window must be created after menu finalized.
34 | Window = UIng::Window.new("Menu Example", 300, 50, menubar: true) do
35 | on_closing do
36 | UIng.quit
37 | true
38 | end
39 |
40 | show
41 | end
42 |
43 | {% if flag?(:darwin) %}
44 | label = UIng::Label.new("The Mac menu bar is at the top of the screen.")
45 | Window.set_child(label)
46 | {% end %}
47 |
48 | UIng.main
49 |
50 | UIng.uninit
51 |
--------------------------------------------------------------------------------
/examples/gallery/basic_form.cr:
--------------------------------------------------------------------------------
1 | require "../../src/uing"
2 |
3 | UIng.init
4 |
5 | window = UIng::Window.new("Form Example", 300, 200, margined: true)
6 | window.on_closing do
7 | UIng.quit
8 | true
9 | end
10 |
11 | form = UIng::Form.new(padded: true)
12 |
13 | username_entry = UIng::Entry.new
14 | username_entry.text = "user123"
15 | form.append("Username:", username_entry)
16 |
17 | password_entry = UIng::Entry.new(:password)
18 | password_entry.text = "password123" # For demonstration
19 | form.append("Password:", password_entry)
20 |
21 | age_spinbox = UIng::Spinbox.new(0, 120)
22 | age_spinbox.value = 25
23 | form.append("Age:", age_spinbox)
24 |
25 | volume_slider = UIng::Slider.new(0, 100)
26 | volume_slider.value = 50
27 | form.append("Volume:", volume_slider)
28 |
29 | submit_button = UIng::Button.new("Submit")
30 | submit_button.on_clicked do
31 | username = username_entry.text || ""
32 | age = age_spinbox.value
33 | volume = volume_slider.value
34 | message = "Username: #{username}\nAge: #{age}\nVolume: #{volume}"
35 | window.msg_box("Form Submitted", message)
36 | end
37 | form.append("", submit_button)
38 |
39 | window.child = form
40 | window.show
41 |
42 | UIng.main
43 | UIng.uninit
44 |
--------------------------------------------------------------------------------
/examples/gallery/basic_group.cr:
--------------------------------------------------------------------------------
1 | require "../../src/uing"
2 |
3 | UIng.init
4 |
5 | window = UIng::Window.new("Group Example", 300, 200, margined: true)
6 | window.on_closing do
7 | UIng.quit
8 | true
9 | end
10 |
11 | group = UIng::Group.new("User Settings", margined: true)
12 |
13 | box = UIng::Box.new(:vertical, padded: true)
14 |
15 | # Enable notifications checkbox
16 | notifications_checkbox = UIng::Checkbox.new("Enable notifications")
17 | notifications_checkbox.checked = true
18 | box.append(notifications_checkbox)
19 |
20 | # Theme selection radio buttons
21 | theme_radio = UIng::RadioButtons.new
22 | theme_radio.append("Light")
23 | theme_radio.append("Dark")
24 | theme_radio.append("Auto")
25 | theme_radio.selected = 0
26 | box.append(theme_radio)
27 |
28 | # Save button
29 | save_button = UIng::Button.new("Save Settings")
30 | save_button.on_clicked do
31 | notifications = notifications_checkbox.checked? ? "enabled" : "disabled"
32 | theme_options = ["Light", "Dark", "Auto"]
33 | theme = theme_options[theme_radio.selected]
34 | window.msg_box("Settings Saved", "Notifications: #{notifications}\nTheme: #{theme}")
35 | end
36 | box.append(save_button)
37 |
38 | group.child = box
39 | window.child = group
40 | window.show
41 |
42 | UIng.main
43 | UIng.uninit
44 |
--------------------------------------------------------------------------------
/src/uing/button.cr:
--------------------------------------------------------------------------------
1 | require "./control"
2 |
3 | module UIng
4 | class Button < Control
5 | block_constructor
6 |
7 | # Store callback box to prevent GC collection
8 | @on_clicked_box : Pointer(Void)?
9 |
10 | def initialize(text : String)
11 | @ref_ptr = LibUI.new_button(text)
12 | end
13 |
14 | def destroy
15 | @on_clicked_box = nil
16 | super
17 | end
18 |
19 | def text : String?
20 | str_ptr = LibUI.button_text(@ref_ptr)
21 | UIng.string_from_pointer(str_ptr)
22 | end
23 |
24 | def text=(text : String) : Nil
25 | LibUI.button_set_text(@ref_ptr, text)
26 | end
27 |
28 | def on_clicked(&block : -> Nil) : Nil
29 | @on_clicked_box = ::Box.box(block)
30 | if boxed_data = @on_clicked_box
31 | LibUI.button_on_clicked(
32 | @ref_ptr,
33 | ->(_sender, data) : Nil {
34 | begin
35 | data_as_callback = ::Box(typeof(block)).unbox(data)
36 | data_as_callback.call
37 | rescue e
38 | UIng.handle_callback_error(e, "Button on_clicked")
39 | end
40 | },
41 | boxed_data
42 | )
43 | end
44 | end
45 |
46 | def to_unsafe
47 | @ref_ptr
48 | end
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/examples/gallery/basic_grid.cr:
--------------------------------------------------------------------------------
1 | require "../../src/uing"
2 |
3 | UIng.init
4 |
5 | window = UIng::Window.new("Grid Example", 300, 200, margined: true)
6 | window.on_closing do
7 | UIng.quit
8 | true
9 | end
10 |
11 | grid = UIng::Grid.new(padded: true)
12 |
13 | # Create a simple 3x3 grid layout
14 | labels = [
15 | "Top Left", "Top Center", "Top Right",
16 | "Middle Left", "Center", "Middle Right",
17 | "Bottom Left", "Bottom Center", "Bottom Right",
18 | ]
19 |
20 | labels.each_with_index do |text, index|
21 | row = index // 3
22 | col = index % 3
23 |
24 | label = UIng::Label.new(text)
25 | grid.append(label, col, row, 1, 1, true, :fill, true, :fill)
26 | end
27 |
28 | # Add a button that spans 2 columns at the bottom
29 | button = UIng::Button.new("Span Button")
30 | button.on_clicked do
31 | window.msg_box("Grid Demo", "This button spans 2 columns!")
32 | end
33 | grid.append(button, 0, 3, 2, 1, true, :fill, false, :fill)
34 |
35 | # Add another button in the remaining space
36 | another_button = UIng::Button.new("Single")
37 | another_button.on_clicked do
38 | window.msg_box("Grid Demo", "Single column button!")
39 | end
40 | grid.append(another_button, 2, 3, 1, 1, true, :fill, false, :fill)
41 |
42 | window.child = grid
43 | window.show
44 |
45 | UIng.main
46 | UIng.uninit
47 |
--------------------------------------------------------------------------------
/examples/basic_area.cr:
--------------------------------------------------------------------------------
1 | require "../src/uing"
2 |
3 | UIng.init
4 |
5 | handler = UIng::Area::Handler.new do
6 | draw { |area, params|
7 | brush = UIng::Area::Draw::Brush.new(:solid, 0.4, 0.4, 0.8, 1.0)
8 | params.context.fill_path(brush) do |path|
9 | path.add_rectangle(0, 0, 400, 400)
10 | end
11 | }
12 |
13 | mouse_event { |area, event|
14 | puts "Mouse event:"
15 | p! event.x
16 | p! event.y
17 | p! event.area_width
18 | p! event.area_height
19 | p! event.down
20 | p! event.up
21 | p! event.count
22 | p! event.modifiers
23 | p! event.held1_to64
24 | nil
25 | }
26 |
27 | mouse_crossed { |area, left|
28 | puts "Mouse crossed: #{left}"
29 | nil
30 | }
31 |
32 | drag_broken { |area|
33 | puts "Drag broken"
34 | nil
35 | }
36 |
37 | key_event { |area, event|
38 | puts "Key event:"
39 | p! event.key
40 | p! event.ext_key
41 | p! event.modifier
42 | p! event.modifiers
43 | p! event.up
44 | false
45 | }
46 | end
47 |
48 | UIng::Window.new("Basic Area", 400, 400, margined: true) do
49 | set_child(
50 | UIng::Box.new(:vertical, padded: true) {
51 | append(
52 | UIng::Area.new(handler), stretchy: true
53 | )
54 | }
55 | )
56 | on_closing {
57 | UIng.quit
58 | true
59 | }
60 | show
61 | end
62 |
63 | UIng.main
64 | UIng.uninit
65 |
--------------------------------------------------------------------------------
/src/uing/spinbox.cr:
--------------------------------------------------------------------------------
1 | require "./control"
2 |
3 | module UIng
4 | class Spinbox < Control
5 | block_constructor
6 |
7 | # Store callback box to prevent GC collection
8 | @on_changed_box : Pointer(Void)?
9 |
10 | def initialize(min, max)
11 | @ref_ptr = LibUI.new_spinbox(min, max)
12 | end
13 |
14 | def destroy
15 | @on_changed_box = nil
16 | super
17 | end
18 |
19 | def initialize(min, max, value)
20 | @ref_ptr = LibUI.new_spinbox(min, max)
21 | self.value = value
22 | end
23 |
24 | def value : Int32
25 | LibUI.spinbox_value(@ref_ptr)
26 | end
27 |
28 | def value=(value : Int32) : Nil
29 | LibUI.spinbox_set_value(@ref_ptr, value)
30 | end
31 |
32 | def on_changed(&block : Int32 -> Nil) : Nil
33 | wrapper = -> : Nil {
34 | v = value
35 | block.call(v)
36 | }
37 | @on_changed_box = ::Box.box(wrapper)
38 | if boxed_data = @on_changed_box
39 | LibUI.spinbox_on_changed(
40 | @ref_ptr,
41 | ->(_sender, data) : Nil {
42 | begin
43 | data_as_callback = ::Box(typeof(wrapper)).unbox(data)
44 | data_as_callback.call
45 | rescue e
46 | UIng.handle_callback_error(e, "Spinbox on_changed")
47 | end
48 | },
49 | boxed_data
50 | )
51 | end
52 | end
53 |
54 | def to_unsafe
55 | @ref_ptr
56 | end
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/src/uing/color_button.cr:
--------------------------------------------------------------------------------
1 | require "./control"
2 |
3 | module UIng
4 | class ColorButton < Control
5 | block_constructor
6 |
7 | # Store callback box to prevent GC collection
8 | @on_changed_box : Pointer(Void)?
9 |
10 | def initialize
11 | @ref_ptr = LibUI.new_color_button
12 | end
13 |
14 | def destroy
15 | @on_changed_box = nil
16 | super
17 | end
18 |
19 | def color : {Float64, Float64, Float64, Float64}
20 | LibUI.color_button_color(@ref_ptr, out r, out g, out b, out a)
21 | {r, g, b, a}
22 | end
23 |
24 | def set_color(r : Float64, g : Float64, b : Float64, a : Float64) : Nil
25 | LibUI.color_button_set_color(@ref_ptr, r, g, b, a)
26 | end
27 |
28 | def on_changed(&block : Float64, Float64, Float64, Float64 -> Nil) : Nil
29 | wrapper = -> : Nil {
30 | r, g, b, a = color
31 | block.call(r, g, b, a)
32 | }
33 | @on_changed_box = ::Box.box(wrapper)
34 | if boxed_data = @on_changed_box
35 | LibUI.color_button_on_changed(
36 | @ref_ptr,
37 | ->(_sender, data) : Nil {
38 | begin
39 | data_as_callback = ::Box(typeof(wrapper)).unbox(data)
40 | data_as_callback.call
41 | rescue e
42 | UIng.handle_callback_error(e, "ColorButton on_changed")
43 | end
44 | },
45 | boxed_data
46 | )
47 | end
48 | end
49 |
50 | def to_unsafe
51 | @ref_ptr
52 | end
53 | end
54 | end
55 |
--------------------------------------------------------------------------------
/src/uing/lib_ui/area_handler.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | lib LibUI
3 | struct AreaHandler
4 | draw : (Pointer(AreaHandler), Pointer(Area), Pointer(AreaDrawParams) -> Void)
5 | mouse_event : (Pointer(AreaHandler), Pointer(Area), Pointer(AreaMouseEvent) -> Void)
6 | mouse_crossed : (Pointer(AreaHandler), Pointer(Area), LibC::Int -> Void)
7 | drag_broken : (Pointer(AreaHandler), Pointer(Area) -> Void)
8 | key_event : (Pointer(AreaHandler), Pointer(Area), Pointer(AreaKeyEvent) -> LibC::Int)
9 | end
10 |
11 | # Extended handler structure that contains the base AreaHandler
12 | # and individual boxes for each callback
13 | #
14 | # NOTE:
15 | # - We intentionally DO NOT use @[Packed] here.
16 | # - This struct embeds AreaHandler as the first field so that a pointer to
17 | # AreaHandlerExtended can be safely cast to AreaHandler* for C (libui).
18 | # - Crystal lib structs follow the C ABI (alignment/padding). There is no
19 | # padding before the first field, so the base_handler will be correctly
20 | # aligned at offset 0.
21 | # - Adding @[Packed] would reduce alignment to 1-byte and may yield an
22 | # unaligned AreaHandler*, which can crash on some architectures (e.g. ARM).
23 | struct AreaHandlerExtended
24 | base_handler : AreaHandler
25 | draw_box : Pointer(Void)
26 | mouse_event_box : Pointer(Void)
27 | mouse_crossed_box : Pointer(Void)
28 | drag_broken_box : Pointer(Void)
29 | key_event_box : Pointer(Void)
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/src/uing/checkbox.cr:
--------------------------------------------------------------------------------
1 | require "./control"
2 |
3 | module UIng
4 | class Checkbox < Control
5 | block_constructor
6 |
7 | # Store callback box to prevent GC collection
8 | @on_toggled_box : Pointer(Void)?
9 |
10 | def initialize(text : String)
11 | @ref_ptr = LibUI.new_checkbox(text)
12 | end
13 |
14 | def destroy
15 | @on_toggled_box = nil
16 | super
17 | end
18 |
19 | def text : String?
20 | str_ptr = LibUI.checkbox_text(@ref_ptr)
21 | UIng.string_from_pointer(str_ptr)
22 | end
23 |
24 | def text=(text : String) : Nil
25 | LibUI.checkbox_set_text(@ref_ptr, text)
26 | end
27 |
28 | def checked? : Bool
29 | LibUI.checkbox_checked(@ref_ptr)
30 | end
31 |
32 | def checked=(checked : Bool) : Nil
33 | LibUI.checkbox_set_checked(@ref_ptr, checked)
34 | end
35 |
36 | def on_toggled(&block : Bool -> Nil) : Nil
37 | wrapper = -> : Nil {
38 | checked = checked?
39 | block.call(checked)
40 | }
41 | @on_toggled_box = ::Box.box(wrapper)
42 | if boxed_data = @on_toggled_box
43 | LibUI.checkbox_on_toggled(
44 | @ref_ptr,
45 | ->(_sender, data) : Nil {
46 | begin
47 | data_as_callback = ::Box(typeof(wrapper)).unbox(data)
48 | data_as_callback.call
49 | rescue e
50 | UIng.handle_callback_error(e, "Checkbox on_toggled")
51 | end
52 | },
53 | boxed_data
54 | )
55 | end
56 | end
57 |
58 | def to_unsafe
59 | @ref_ptr
60 | end
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/src/uing/radio_buttons.cr:
--------------------------------------------------------------------------------
1 | require "./control"
2 |
3 | module UIng
4 | class RadioButtons < Control
5 | block_constructor
6 |
7 | # Store callback box to prevent GC collection
8 | @on_selected_box : Pointer(Void)?
9 |
10 | def initialize
11 | @ref_ptr = LibUI.new_radio_buttons
12 | end
13 |
14 | def destroy
15 | @on_selected_box = nil
16 | super
17 | end
18 |
19 | def initialize(items : Array(String))
20 | initialize()
21 | items.each do |item|
22 | append(item)
23 | end
24 | end
25 |
26 | def append(text : String) : Nil
27 | LibUI.radio_buttons_append(@ref_ptr, text)
28 | end
29 |
30 | def selected : Int32
31 | LibUI.radio_buttons_selected(@ref_ptr)
32 | end
33 |
34 | def selected=(index : Int32) : Nil
35 | LibUI.radio_buttons_set_selected(@ref_ptr, index)
36 | end
37 |
38 | def on_selected(&block : Int32 -> Nil) : Nil
39 | wrapper = -> : Nil {
40 | idx = selected
41 | block.call(idx)
42 | }
43 | @on_selected_box = ::Box.box(wrapper)
44 | if boxed_data = @on_selected_box
45 | LibUI.radio_buttons_on_selected(
46 | @ref_ptr,
47 | ->(_sender, data) : Nil {
48 | begin
49 | data_as_callback = ::Box(typeof(wrapper)).unbox(data)
50 | data_as_callback.call
51 | rescue e
52 | UIng.handle_callback_error(e, "RadioButtons on_selected")
53 | end
54 | },
55 | boxed_data
56 | )
57 | end
58 | end
59 |
60 | def to_unsafe
61 | @ref_ptr
62 | end
63 | end
64 | end
65 |
--------------------------------------------------------------------------------
/src/uing/form.cr:
--------------------------------------------------------------------------------
1 | require "./control"
2 |
3 | module UIng
4 | class Form < Control
5 | block_constructor
6 |
7 | @children_refs : Array(Control) = [] of Control
8 |
9 | def initialize(padded : Bool = false)
10 | @ref_ptr = LibUI.new_form
11 | self.padded = true if padded
12 | end
13 |
14 | def destroy
15 | @children_refs.each do |child|
16 | child.release_ownership
17 | end
18 | super
19 | end
20 |
21 | def delete(child : Control)
22 | if index = @children_refs.index(child)
23 | delete(index)
24 | end
25 | end
26 |
27 | def append(label : String, control, stretchy : Bool = false) : Nil
28 | control.check_can_move
29 | LibUI.form_append(@ref_ptr, label, UIng.to_control(control), stretchy)
30 | @children_refs << control
31 | control.take_ownership(self)
32 | end
33 |
34 | # For DSL style
35 | def append(label : String, stretchy : Bool = false, &block : -> Control) : Nil
36 | control = block.call
37 | append(label, control, stretchy)
38 | end
39 |
40 | def num_children : Int32
41 | LibUI.form_num_children(@ref_ptr)
42 | end
43 |
44 | def delete(index : Int32) : Nil
45 | child = @children_refs[index]
46 | LibUI.form_delete(@ref_ptr, index)
47 | @children_refs.delete_at(index)
48 | child.release_ownership
49 | end
50 |
51 | def padded? : Bool
52 | LibUI.form_padded(@ref_ptr)
53 | end
54 |
55 | def padded=(padded : Bool) : Nil
56 | LibUI.form_set_padded(@ref_ptr, padded)
57 | end
58 |
59 | def to_unsafe
60 | @ref_ptr
61 | end
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/src/uing/menu_item.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | class MenuItem
3 | @released : Bool = false
4 |
5 | # Store callback box to prevent GC collection
6 | @on_clicked_box : Pointer(Void)?
7 |
8 | def initialize(@ref_ptr : Pointer(LibUI::MenuItem))
9 | end
10 |
11 | def destroy
12 | return if @released
13 | @on_clicked_box = nil
14 | @released = true
15 | end
16 |
17 | # no new_menu_item function in libui
18 |
19 | def enable : Nil
20 | LibUI.menu_item_enable(@ref_ptr)
21 | end
22 |
23 | def disable : Nil
24 | LibUI.menu_item_disable(@ref_ptr)
25 | end
26 |
27 | def checked? : Bool
28 | LibUI.menu_item_checked(@ref_ptr)
29 | end
30 |
31 | def checked=(checked : Bool) : Nil
32 | LibUI.menu_item_set_checked(@ref_ptr, checked)
33 | end
34 |
35 | def on_clicked(&block : UIng::Window -> Nil) : Nil
36 | # Convert to the internal callback format that matches LibUI expectation
37 | callback2 = ->(w : Pointer(LibUI::Window)) : Nil {
38 | block.call(UIng::Window.new(w))
39 | }
40 | @on_clicked_box = ::Box.box(callback2)
41 | if boxed_data = @on_clicked_box
42 | LibUI.menu_item_on_clicked(
43 | @ref_ptr,
44 | ->(_sender, window, data) : Nil {
45 | begin
46 | data_as_callback = ::Box(typeof(callback2)).unbox(data)
47 | data_as_callback.call(window)
48 | rescue e
49 | UIng.handle_callback_error(e, "MenuItem on_clicked")
50 | end
51 | },
52 | boxed_data
53 | )
54 | end
55 | end
56 |
57 | def to_unsafe
58 | @ref_ptr
59 | end
60 | end
61 | end
62 |
--------------------------------------------------------------------------------
/src/uing/grid.cr:
--------------------------------------------------------------------------------
1 | require "./control"
2 |
3 | module UIng
4 | class Grid < Control
5 | block_constructor
6 |
7 | @children_refs : Array(Control) = [] of Control
8 |
9 | def initialize(padded : Bool = false)
10 | @ref_ptr = LibUI.new_grid
11 | self.padded = true if padded
12 | end
13 |
14 | def destroy
15 | @children_refs.each do |child|
16 | child.release_ownership
17 | end
18 | super
19 | end
20 |
21 | # Raises: Not supported for this container.
22 | def delete(child : Control)
23 | raise "Grid does not support delete(child : Control)"
24 | end
25 |
26 | def append(control, left : Int32, top : Int32, xspan : Int32, yspan : Int32, hexpand : Bool, halign : UIng::Align, vexpand : Bool, valign : UIng::Align) : Nil
27 | control.check_can_move
28 | LibUI.grid_append(@ref_ptr, UIng.to_control(control), left, top, xspan, yspan, hexpand, halign, vexpand, valign)
29 | @children_refs << control
30 | control.take_ownership(self)
31 | end
32 |
33 | def insert_at(control, existing, at : At, xspan : Int32, yspan : Int32, hexpand : Bool, halign : UIng::Align, vexpand : Bool, valign : UIng::Align) : Nil
34 | control.check_can_move
35 | LibUI.grid_insert_at(@ref_ptr, UIng.to_control(control), UIng.to_control(existing), at, xspan, yspan, hexpand, halign, vexpand, valign)
36 | @children_refs << control
37 | control.take_ownership(self)
38 | end
39 |
40 | def padded? : Bool
41 | LibUI.grid_padded(@ref_ptr)
42 | end
43 |
44 | def padded=(padded : Bool) : Nil
45 | LibUI.grid_set_padded(@ref_ptr, padded)
46 | end
47 |
48 | def to_unsafe
49 | @ref_ptr
50 | end
51 | end
52 | end
53 |
--------------------------------------------------------------------------------
/src/uing/editable_combobox.cr:
--------------------------------------------------------------------------------
1 | require "./control"
2 |
3 | module UIng
4 | class EditableCombobox < Control
5 | block_constructor
6 |
7 | # Store callback box to prevent GC collection
8 | @on_changed_box : Pointer(Void)?
9 |
10 | def initialize
11 | @ref_ptr = LibUI.new_editable_combobox
12 | end
13 |
14 | def destroy
15 | @on_changed_box = nil
16 | super
17 | end
18 |
19 | def initialize(items : Array(String))
20 | initialize()
21 | items.each do |item|
22 | append(item)
23 | end
24 | end
25 |
26 | def append(text : String) : Nil
27 | LibUI.editable_combobox_append(@ref_ptr, text)
28 | end
29 |
30 | def text : String?
31 | str_ptr = LibUI.editable_combobox_text(@ref_ptr)
32 | UIng.string_from_pointer(str_ptr)
33 | end
34 |
35 | def text=(text : String) : Nil
36 | LibUI.editable_combobox_set_text(@ref_ptr, text)
37 | end
38 |
39 | def on_changed(&block : String -> Nil) : Nil
40 | wrapper = -> : Nil {
41 | current_text = text || ""
42 | block.call(current_text)
43 | }
44 | @on_changed_box = ::Box.box(wrapper)
45 | if boxed_data = @on_changed_box
46 | LibUI.editable_combobox_on_changed(
47 | @ref_ptr,
48 | ->(_sender, data) : Nil {
49 | begin
50 | data_as_callback = ::Box(typeof(wrapper)).unbox(data)
51 | data_as_callback.call
52 | rescue e
53 | UIng.handle_callback_error(e, "EditableCombobox on_changed")
54 | end
55 | },
56 | boxed_data
57 | )
58 | end
59 | end
60 |
61 | def to_unsafe
62 | @ref_ptr
63 | end
64 | end
65 | end
66 |
--------------------------------------------------------------------------------
/src/uing/area/area/draw/brush/gradient_stop.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | class Area < Control
3 | module Draw
4 | class Brush
5 | class GradientStop
6 | include BlockConstructor; block_constructor
7 |
8 | def initialize(pos : Number = 0.0,
9 | r : Number = 0.0,
10 | g : Number = 0.0,
11 | b : Number = 0.0,
12 | a : Number = 1.0)
13 | @cstruct = LibUI::DrawBrushGradientStop.new
14 | self.pos = pos.to_f64
15 | self.r = r.to_f64
16 | self.g = g.to_f64
17 | self.b = b.to_f64
18 | self.a = a.to_f64
19 | end
20 |
21 | def pos : Float64
22 | @cstruct.pos
23 | end
24 |
25 | def pos=(value : Float64)
26 | @cstruct.pos = value
27 | end
28 |
29 | def r : Float64
30 | @cstruct.r
31 | end
32 |
33 | def r=(value : Float64)
34 | @cstruct.r = value
35 | end
36 |
37 | def g : Float64
38 | @cstruct.g
39 | end
40 |
41 | def g=(value : Float64)
42 | @cstruct.g = value
43 | end
44 |
45 | def b : Float64
46 | @cstruct.b
47 | end
48 |
49 | def b=(value : Float64)
50 | @cstruct.b = value
51 | end
52 |
53 | def a : Float64
54 | @cstruct.a
55 | end
56 |
57 | def a=(value : Float64)
58 | @cstruct.a = value
59 | end
60 |
61 | def to_unsafe
62 | pointerof(@cstruct)
63 | end
64 | end
65 | end
66 | end
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/src/uing/font_button.cr:
--------------------------------------------------------------------------------
1 | require "./control"
2 |
3 | module UIng
4 | class FontButton < Control
5 | block_constructor
6 |
7 | # Store callback box to prevent GC collection
8 | @on_changed_box : Pointer(Void)?
9 |
10 | def initialize
11 | @ref_ptr = LibUI.new_font_button
12 | end
13 |
14 | def destroy
15 | @on_changed_box = nil
16 | super
17 | end
18 |
19 | def on_changed(&block : FontDescriptor -> Nil) : Nil
20 | wrapper = -> : Nil {
21 | font_descriptor = FontDescriptor.new
22 | font(font_descriptor)
23 | block.call(font_descriptor)
24 | free_font(font_descriptor)
25 | }
26 | @on_changed_box = ::Box.box(wrapper)
27 | if boxed_data = @on_changed_box
28 | LibUI.font_button_on_changed(
29 | @ref_ptr,
30 | ->(_sender, data) : Nil {
31 | begin
32 | data_as_callback = ::Box(typeof(wrapper)).unbox(data)
33 | data_as_callback.call
34 | rescue e
35 | UIng.handle_callback_error(e, "FontButton on_changed")
36 | end
37 | },
38 | boxed_data
39 | )
40 | end
41 | end
42 |
43 | def font(&block : FontDescriptor -> Nil)
44 | font_descriptor = FontDescriptor.new
45 | font(font_descriptor)
46 | block.call(font_descriptor)
47 | free_font(font_descriptor)
48 | end
49 |
50 | def font(descriptor : FontDescriptor)
51 | LibUI.font_button_font(@ref_ptr, descriptor)
52 | end
53 |
54 | def free_font(font_descriptor : FontDescriptor) : Nil
55 | LibUI.free_font_button_font(font_descriptor.to_unsafe)
56 | end
57 |
58 | def to_unsafe
59 | @ref_ptr
60 | end
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/examples/md5_checker/build-deb.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | # Load configuration from .env
5 | set -a
6 | source .env
7 | set +a
8 |
9 | DIST_DIR="dist"
10 |
11 | echo "Building $APP_NAME v$VERSION..."
12 | shards build --release
13 |
14 | rm -rf "$DIST_DIR"
15 | mkdir -p "$DIST_DIR"
16 |
17 | # Create temporary directory structure
18 | TEMP_DIR=$(mktemp -d)
19 | mkdir -p "$TEMP_DIR/usr/bin"
20 | mkdir -p "$TEMP_DIR/usr/share/applications"
21 | mkdir -p "$TEMP_DIR/usr/share/icons/hicolor/256x256/apps"
22 |
23 | # Copy binary
24 | cp "bin/$APP_NAME" "$TEMP_DIR/usr/bin/"
25 |
26 | # Generate desktop entry
27 | cat > "$TEMP_DIR/usr/share/applications/$APP_NAME.desktop" < Control)
52 | control = block.call
53 | self.child = control
54 | end
55 |
56 | def margined? : Bool
57 | LibUI.group_margined(@ref_ptr)
58 | end
59 |
60 | def margined=(margined : Bool) : Nil
61 | LibUI.group_set_margined(@ref_ptr, margined)
62 | end
63 |
64 | def to_unsafe
65 | @ref_ptr
66 | end
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/src/uing/area/area.cr:
--------------------------------------------------------------------------------
1 | require "../control"
2 | require "./area/*"
3 | require "./area/draw/*"
4 | require "./area/attribute/*"
5 |
6 | module UIng
7 | class Area < Control
8 | block_constructor
9 |
10 | # Keep a reference to the Area::Handler to prevent GC
11 | @area_handler : Handler?
12 |
13 | def initialize(@ref_ptr : Pointer(LibUI::Area))
14 | end
15 |
16 | def initialize(area_handler : Handler)
17 | @area_handler = area_handler # Keep reference to prevent GC
18 | @ref_ptr = LibUI.new_area(area_handler.to_unsafe)
19 | end
20 |
21 | # scrolling area
22 |
23 | def initialize(area_handler : Pointer(LibUI::AreaHandler), width : Int32, height : Int32)
24 | @ref_ptr = LibUI.new_scrolling_area(area_handler, width, height)
25 | end
26 |
27 | def initialize(area_handler : Handler, width : Int32, height : Int32)
28 | @area_handler = area_handler # Keep reference to prevent GC
29 | @ref_ptr = LibUI.new_scrolling_area(area_handler.to_unsafe, width, height)
30 | end
31 |
32 | def destroy
33 | @area_handler = nil
34 | super
35 | end
36 |
37 | def set_size(width : Int32, height : Int32) : Nil
38 | LibUI.area_set_size(@ref_ptr, width, height)
39 | end
40 |
41 | def queue_redraw_all : Nil
42 | LibUI.area_queue_redraw_all(@ref_ptr)
43 | end
44 |
45 | def scroll_to(x : Float64, y : Float64, width : Float64, height : Float64) : Nil
46 | LibUI.area_scroll_to(@ref_ptr, x, y, width, height)
47 | end
48 |
49 | def begin_user_window_move : Nil
50 | LibUI.area_begin_user_window_move(@ref_ptr)
51 | end
52 |
53 | def begin_user_window_resize(edge : WindowResizeEdge) : Nil
54 | LibUI.area_begin_user_window_resize(@ref_ptr, edge)
55 | end
56 |
57 | def to_unsafe
58 | @ref_ptr
59 | end
60 | end
61 | end
62 |
--------------------------------------------------------------------------------
/src/uing/entry.cr:
--------------------------------------------------------------------------------
1 | require "./control"
2 |
3 | module UIng
4 | class Entry < Control
5 | block_constructor
6 |
7 | # Store callback box to prevent GC collection
8 | @on_changed_box : Pointer(Void)?
9 |
10 | def initialize(type : Symbol = :default, read_only = false)
11 | case type
12 | when :password
13 | @ref_ptr = LibUI.new_password_entry
14 | when :search
15 | @ref_ptr = LibUI.new_search_entry
16 | else
17 | @ref_ptr = LibUI.new_entry
18 | end
19 | if read_only
20 | self.read_only = true
21 | end
22 | end
23 |
24 | def destroy
25 | @on_changed_box = nil
26 | super
27 | end
28 |
29 | def text : String?
30 | str_ptr = LibUI.entry_text(@ref_ptr)
31 | UIng.string_from_pointer(str_ptr)
32 | end
33 |
34 | def text=(text : String) : Nil
35 | LibUI.entry_set_text(@ref_ptr, text)
36 | end
37 |
38 | def read_only? : Bool
39 | LibUI.entry_read_only(@ref_ptr)
40 | end
41 |
42 | def read_only=(readonly : Bool) : Nil
43 | LibUI.entry_set_read_only(@ref_ptr, readonly)
44 | end
45 |
46 | def on_changed(&block : String -> Nil) : Nil
47 | wrapper = -> : Nil {
48 | current_text = text || ""
49 | block.call(current_text)
50 | }
51 | @on_changed_box = ::Box.box(wrapper)
52 | if boxed_data = @on_changed_box
53 | LibUI.entry_on_changed(
54 | @ref_ptr,
55 | ->(_sender, data) : Nil {
56 | begin
57 | data_as_callback = ::Box(typeof(wrapper)).unbox(data)
58 | data_as_callback.call
59 | rescue e
60 | UIng.handle_callback_error(e, "Entry on_changed")
61 | end
62 | },
63 | boxed_data
64 | )
65 | end
66 | end
67 |
68 | def to_unsafe
69 | @ref_ptr
70 | end
71 | end
72 | end
73 |
--------------------------------------------------------------------------------
/examples/basic_table.cr:
--------------------------------------------------------------------------------
1 | require "../src/uing"
2 |
3 | UIng.init
4 |
5 | main_window = UIng::Window.new("Animal sounds", 300, 200)
6 |
7 | hbox = UIng::Box.new :horizontal
8 | main_window.child = hbox
9 |
10 | data = [
11 | %w[cat meow],
12 | %w[dog woof],
13 | %w[chicken cock-a-doodle-doo],
14 | %w[horse neigh],
15 | %w[cow moo],
16 | ]
17 |
18 | model_handler = UIng::Table::Model::Handler.new do
19 | num_columns do
20 | 2
21 | end
22 |
23 | column_type do |column|
24 | UIng::Table::Value::Type::String
25 | end
26 |
27 | num_rows do
28 | data.size
29 | end
30 |
31 | cell_value do |row, column|
32 | UIng::Table::Value.new(data[row][column])
33 | end
34 |
35 | set_cell_value do |row, column, value|
36 | # This example doesn't support editing, so we do nothing
37 | end
38 | end
39 |
40 | table_model = UIng::Table::Model.new(model_handler)
41 |
42 | table = UIng::Table.new(table_model) do
43 | append_text_column("Animal", 0, -1)
44 | append_text_column("Description", 1, -1)
45 | end
46 |
47 | table.on_selection_changed do |selection|
48 | if selection.num_rows > 0
49 | selected_row = selection.rows[0]
50 | animal = data[selected_row][0]
51 | sound = data[selected_row][1]
52 | puts "Selected: #{animal} says #{sound}"
53 | else
54 | puts "No selection"
55 | end
56 | # Table::Selection is automatically freed after this block
57 | end
58 |
59 | table.on_header_clicked do |idx|
60 | puts "Header clicked: #{idx}"
61 | end
62 |
63 | table.on_row_double_clicked do |row|
64 | animal = data[row][0]
65 | sound = data[row][1]
66 | puts "Double-clicked: #{animal} goes #{sound}!"
67 | end
68 |
69 | hbox.append(table, true)
70 | main_window.show
71 |
72 | main_window.on_closing do
73 | puts "Bye Bye"
74 |
75 | # FIXME: https://github.com/kojix2/uing/issues/6
76 | hbox.delete(0)
77 | table.destroy # Destroy table firs
78 | table_model.free # Then free model
79 |
80 | UIng.quit
81 | true
82 | end
83 |
84 | UIng.main
85 | UIng.uninit
86 |
--------------------------------------------------------------------------------
/examples/video_player/README.md:
--------------------------------------------------------------------------------
1 | # Crystal Video Player
2 |
3 | A video player example using libmpv and UIng (Crystal bindings for libui-ng).
4 |
5 | ## Features
6 |
7 | - Video playback using libmpv
8 | - Play/Pause controls
9 | - Video title and dimensions display
10 | - Support for local files and URLs
11 | - Cross-platform GUI using libui-ng
12 |
13 | ## Requirements
14 |
15 | - Crystal language
16 | - libmpv library
17 |
18 | ## Setup
19 |
20 | ### macOS
21 |
22 | ```bash
23 | brew install mpv
24 | cd examples/video_player
25 | shards install
26 | shards build
27 | ```
28 |
29 | **Note**: On macOS, the video player uses mpv's "gpu" output driver for hardware-accelerated rendering with Vulkan/Metal backend.
30 |
31 | ### Windows (MSVC/RIDK)
32 |
33 | ```bash
34 | ridk enable
35 | ridk exec pacman -S mingw-w64-x86_64-mpv
36 | cd examples/video_player
37 | ridk exec shards install
38 | ridk exec shards build
39 | ```
40 |
41 | ### Windows (MinGW/MSYS2)
42 |
43 | ```bash
44 | # Open MSYS2 MinGW 64-bit shell
45 | pacman -Syu
46 | pacman -S mingw-w64-x86_64-crystal mingw-w64-x86_64-mpv
47 | cd /path/to/your/clone/examples/video_player
48 | shards install
49 | shards build
50 | ```
51 |
52 | ## Usage
53 |
54 | ```bash
55 | # Play default video (Big Buck Bunny)
56 | ./bin/video_player
57 |
58 | # Play a local file
59 | ./bin/video_player "path/to/your/video.mp4"
60 |
61 | # Play a URL
62 | ./bin/video_player "https://example.com/video.mp4"
63 | ```
64 |
65 | ## Controls
66 |
67 | - **Play / Pause button**: Toggle video playback
68 | - **Title label**: Shows the media title (if available)
69 | - **Size label**: Shows video dimensions (width x height)
70 |
71 | ## Architecture
72 |
73 | The video player consists of three main components:
74 |
75 | 1. **MPV Bindings** (`src/mpv_bindings.cr`): Low-level Crystal bindings for libmpv
76 | 2. **MPV Player** (`src/mpv_player.cr`): High-level wrapper class for mpv functionality
77 | 3. **Video Player** (`video_player.cr`): Main application with GUI
78 |
79 | ## License
80 |
81 | MIT License
82 |
--------------------------------------------------------------------------------
/src/uing/combobox.cr:
--------------------------------------------------------------------------------
1 | require "./control"
2 |
3 | module UIng
4 | class Combobox < Control
5 | block_constructor
6 |
7 | # Store callback box to prevent GC collection
8 | @on_selected_box : Pointer(Void)?
9 |
10 | def initialize
11 | @ref_ptr = LibUI.new_combobox
12 | end
13 |
14 | def destroy
15 | @on_selected_box = nil
16 | super
17 | end
18 |
19 | def initialize(items : Array(String))
20 | initialize()
21 | items.each do |item|
22 | append(item)
23 | end
24 | end
25 |
26 | def append(text : String) : Nil
27 | LibUI.combobox_append(@ref_ptr, text)
28 | end
29 |
30 | def insert_at(index : Int32, text : String) : Nil
31 | LibUI.combobox_insert_at(@ref_ptr, index, text)
32 | end
33 |
34 | def delete(index : Int32) : Nil
35 | LibUI.combobox_delete(@ref_ptr, index)
36 | end
37 |
38 | def clear : Nil
39 | LibUI.combobox_clear(@ref_ptr)
40 | end
41 |
42 | def num_items : Int32
43 | LibUI.combobox_num_items(@ref_ptr)
44 | end
45 |
46 | def selected : Int32
47 | LibUI.combobox_selected(@ref_ptr)
48 | end
49 |
50 | def selected=(index : Int32) : Nil
51 | LibUI.combobox_set_selected(@ref_ptr, index)
52 | end
53 |
54 | def on_selected(&block : Int32 -> Nil) : Nil
55 | wrapper = -> : Nil {
56 | idx = selected
57 | block.call(idx)
58 | }
59 | @on_selected_box = ::Box.box(wrapper)
60 | if boxed_data = @on_selected_box
61 | LibUI.combobox_on_selected(
62 | @ref_ptr,
63 | ->(_sender, data) : Nil {
64 | begin
65 | data_as_callback = ::Box(typeof(wrapper)).unbox(data)
66 | data_as_callback.call
67 | rescue e
68 | UIng.handle_callback_error(e, "Combobox on_selected")
69 | end
70 | },
71 | boxed_data
72 | )
73 | end
74 | end
75 |
76 | def to_unsafe
77 | @ref_ptr
78 | end
79 | end
80 | end
81 |
--------------------------------------------------------------------------------
/src/uing/area/area/draw/text_layout.cr:
--------------------------------------------------------------------------------
1 | require "./text_layout/*"
2 |
3 | module UIng
4 | class Area < Control
5 | module Draw
6 | class TextLayout
7 | include BlockConstructor; block_constructor
8 |
9 | @released = false
10 |
11 | # NOTE: Strong references to AttributedString/FontDescriptor are NOT needed.
12 | # libui-ng uses "copy on creation" pattern - TextLayout internally copies
13 | # all content from source objects, making it safe to free them immediately.
14 |
15 | def initialize(string : AttributedString,
16 | default_font : FontDescriptor,
17 | width : Float64,
18 | align : UIng::Area::Draw::TextAlign = UIng::Area::Draw::TextAlign::Left)
19 | draw_text_layout_params = Draw::TextLayout::Params.new(
20 | string: string,
21 | default_font: default_font,
22 | width: width,
23 | align: align
24 | )
25 | @ref_ptr = LibUI.draw_new_text_layout(draw_text_layout_params)
26 | end
27 |
28 | def self.open(string : AttributedString,
29 | default_font : FontDescriptor,
30 | width : Float64,
31 | align : UIng::Area::Draw::TextAlign = UIng::Area::Draw::TextAlign::Left,
32 | &block : TextLayout -> Nil) : Nil
33 | text_layout = TextLayout.new(string, default_font, width, align)
34 | begin
35 | block.call(text_layout)
36 | ensure
37 | text_layout.free
38 | end
39 | end
40 |
41 | def free : Nil
42 | return if @released
43 | LibUI.draw_free_text_layout(@ref_ptr)
44 | @released = true
45 | end
46 |
47 | def extents : {Float64, Float64}
48 | LibUI.draw_text_layout_extents(@ref_ptr, out width, out height)
49 | {width, height}
50 | end
51 |
52 | def to_unsafe
53 | @ref_ptr
54 | end
55 | end
56 | end
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/src/uing/area/area/draw/text_layout/params.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | class Area < Control
3 | module Draw
4 | class TextLayout
5 | class Params
6 | include BlockConstructor; block_constructor
7 |
8 | # Store references to prevent garbage collection
9 | @attributed_string : AttributedString
10 | @font_descriptor : FontDescriptor
11 |
12 | def initialize(string : AttributedString, default_font : FontDescriptor, width : Float64, align : UIng::Area::Draw::TextAlign)
13 | @cstruct = LibUI::DrawTextLayoutParams.new
14 | @attributed_string = string
15 | @font_descriptor = default_font
16 | @cstruct.string = string.to_unsafe
17 | @cstruct.default_font = default_font.to_unsafe
18 | @cstruct.width = width
19 | @cstruct.align = align
20 | end
21 |
22 | # Explicit property accessors for better type safety and documentation
23 | def string : AttributedString
24 | @attributed_string
25 | end
26 |
27 | def string=(value : AttributedString)
28 | @attributed_string = value
29 | @cstruct.string = value.to_unsafe
30 | end
31 |
32 | def default_font : FontDescriptor
33 | @font_descriptor
34 | end
35 |
36 | def default_font=(value : FontDescriptor)
37 | @font_descriptor = value
38 | @cstruct.default_font = value.to_unsafe
39 | end
40 |
41 | def width : Float64
42 | @cstruct.width
43 | end
44 |
45 | def width=(value : Float64)
46 | @cstruct.width = value
47 | end
48 |
49 | def align : UIng::Area::Draw::TextAlign
50 | @cstruct.align
51 | end
52 |
53 | def align=(value : UIng::Area::Draw::TextAlign)
54 | @cstruct.align = value
55 | end
56 |
57 | def to_unsafe
58 | pointerof(@cstruct)
59 | end
60 | end
61 | end
62 | end
63 | end
64 | end
65 |
--------------------------------------------------------------------------------
/examples/gallery/area_basic_shapes.cr:
--------------------------------------------------------------------------------
1 | require "../../src/uing"
2 |
3 | UIng.init
4 |
5 | # Basic shapes drawing example
6 | # Demonstrates: Area setup, basic drawing operations, solid brushes, simple paths
7 |
8 | handler = UIng::Area::Handler.new do
9 | draw { |area, params|
10 | ctx = params.context
11 |
12 | # Draw a blue rectangle
13 | blue_brush = UIng::Area::Draw::Brush.new(:solid, 0.2, 0.4, 0.8, 1.0)
14 | ctx.fill_path(blue_brush) do |path|
15 | # x, y, width, height
16 | path.add_rectangle(30, 30, 70, 45)
17 | end
18 |
19 | # Draw a red circle (using arc)
20 | red_brush = UIng::Area::Draw::Brush.new(:solid, 0.8, 0.2, 0.2, 1.0)
21 | ctx.fill_path(red_brush) do |path|
22 | # x_center, y_center, radius, start_angle, sweep, negative
23 | path.new_figure_with_arc(200, 50, 30, 0, Math::PI * 2, false)
24 | end
25 |
26 | # Draw a green triangle (using lines)
27 | green_brush = UIng::Area::Draw::Brush.new(:solid, 0.2, 0.8, 0.2, 1.0)
28 | ctx.fill_path(green_brush) do |path|
29 | # x, y (starting point)
30 | path.new_figure(75, 110)
31 | # x, y (line to point)
32 | path.line_to(110, 160)
33 | # x, y (line to point)
34 | path.line_to(40, 160)
35 | path.close_figure
36 | end
37 |
38 | # Draw a purple outlined rectangle (stroke only)
39 | purple_brush = UIng::Area::Draw::Brush.new(:solid, 0.6, 0.2, 0.8, 1.0)
40 | stroke_params = UIng::Area::Draw::StrokeParams.new(
41 | cap: :flat,
42 | join: :miter,
43 | thickness: 2.5,
44 | miter_limit: 10.0
45 | )
46 | ctx.stroke_path(purple_brush, stroke_params) do |path|
47 | # x, y, width, height
48 | path.add_rectangle(190, 110, 60, 50)
49 | end
50 | }
51 | end
52 |
53 | window = UIng::Window.new("Area - Basic Shapes", 300, 200)
54 | window.on_closing do
55 | UIng.quit
56 | true
57 | end
58 |
59 | area = UIng::Area.new(handler)
60 | box = UIng::Box.new(:vertical, padded: true)
61 | box.append(area, stretchy: true)
62 | window.child = box
63 |
64 | window.show
65 |
66 | UIng.main
67 | UIng.uninit
68 |
--------------------------------------------------------------------------------
/examples/gallery/area_draw_image.cr:
--------------------------------------------------------------------------------
1 | require "../../src/uing"
2 | require "stumpy_png"
3 |
4 | fname = File.join(__DIR__, "crys.png")
5 | canvas = StumpyPNG.read(fname)
6 | width = canvas.width.to_i32
7 | height = canvas.height.to_i32
8 |
9 | # Create pixel buffer for premultiplied RGBA
10 | pixels = Bytes.new(width * height * 4)
11 | (0...height).each do |y|
12 | (0...width).each do |x|
13 | offset = (y * width + x) * 4
14 | r, g, b, a = canvas[x, y].to_rgba
15 |
16 | # Handle alpha properly - default to 255 if nil
17 | alpha = a || 255_u8
18 |
19 | # For premultiplied alpha, multiply RGB by alpha/255
20 | if alpha < 255
21 | alpha_factor = alpha.to_f / 255.0
22 | r = (r.to_f * alpha_factor).to_u8
23 | g = (g.to_f * alpha_factor).to_u8
24 | b = (b.to_f * alpha_factor).to_u8
25 | end
26 |
27 | pixels[offset] = r
28 | pixels[offset + 1] = g
29 | pixels[offset + 2] = b
30 | pixels[offset + 3] = alpha
31 | end
32 | end
33 |
34 | UIng.init
35 |
36 | image = UIng::Image.new(width, height)
37 | image.append(pixels, width, height, width * 4)
38 |
39 | window = UIng::Window.new("Draw Image Example", 400, 400, margined: true)
40 | window.on_closing do
41 | UIng.quit
42 | true
43 | end
44 |
45 | area_handler = UIng::Area::Handler.new do |header|
46 | draw do |area, params|
47 | ctx = params.context
48 | white_brush = UIng::Area::Draw::Brush.new(:solid, 1.0, 1.0, 1.0, 1.0)
49 | ctx.fill_path(white_brush) do |path|
50 | path.add_rectangle(0, 0, params.area_width, params.area_height)
51 | end
52 | begin
53 | ctx.draw_image(image, 10, 10, 100, 100)
54 | ctx.draw_image(image, 160, 10, 200, 100)
55 | ctx.draw_image(image, 10, 160, 100, 200)
56 | ctx.draw_image(image, 160, 160, 200, 200)
57 | rescue ex
58 | UIng.handle_callback_error(ex, "draw_image")
59 | end
60 | end
61 | end
62 |
63 | area = UIng::Area.new(area_handler)
64 | box = UIng::Box.new(:horizontal)
65 | box.append(area, stretchy: true)
66 | window.set_child(box)
67 | window.show
68 |
69 | UIng.main
70 |
71 | image.free
72 | UIng.uninit
73 |
--------------------------------------------------------------------------------
/src/uing/area/area/draw/matrix.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | class Area < Control
3 | module Draw
4 | class Matrix
5 | include BlockConstructor; block_constructor
6 |
7 | def initialize
8 | @cstruct = LibUI::DrawMatrix.new
9 | end
10 |
11 | def set_identity : self
12 | LibUI.draw_matrix_set_identity(self.to_unsafe)
13 | self
14 | end
15 |
16 | def translate(x : Float64, y : Float64) : self
17 | LibUI.draw_matrix_translate(self.to_unsafe, x, y)
18 | self
19 | end
20 |
21 | def scale(x_center : Float64, y_center : Float64, x : Float64, y : Float64) : self
22 | LibUI.draw_matrix_scale(self.to_unsafe, x_center, y_center, x, y)
23 | self
24 | end
25 |
26 | def rotate(x : Float64, y : Float64, amount : Float64) : self
27 | LibUI.draw_matrix_rotate(self.to_unsafe, x, y, amount)
28 | self
29 | end
30 |
31 | def skew(x : Float64, y : Float64, x_amount : Float64, y_amount : Float64) : self
32 | LibUI.draw_matrix_skew(self.to_unsafe, x, y, x_amount, y_amount)
33 | self
34 | end
35 |
36 | def multiply(src : Matrix) : self
37 | LibUI.draw_matrix_multiply(self.to_unsafe, src.to_unsafe)
38 | self
39 | end
40 |
41 | def invertible? : Bool
42 | LibUI.draw_matrix_invertible(self.to_unsafe)
43 | end
44 |
45 | def invert : Bool
46 | LibUI.draw_matrix_invert(self.to_unsafe)
47 | end
48 |
49 | def transform_point(x : Float64, y : Float64) : {Float64, Float64}
50 | x2 = x
51 | y2 = y
52 | LibUI.draw_matrix_transform_point(self.to_unsafe, pointerof(x2), pointerof(y2))
53 | {x2, y2}
54 | end
55 |
56 | def transform_size(x : Float64, y : Float64) : {Float64, Float64}
57 | x2 = x
58 | y2 = y
59 | LibUI.draw_matrix_transform_size(self.to_unsafe, pointerof(x2), pointerof(y2))
60 | {x2, y2}
61 | end
62 |
63 | def to_unsafe
64 | pointerof(@cstruct)
65 | end
66 | end
67 | end
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/src/uing/box.cr:
--------------------------------------------------------------------------------
1 | require "./control"
2 |
3 | module UIng
4 | # Note: The name Box is already taken by Crystal's built-in class Box.
5 |
6 | # Why not use HorizontalBox and VerticalBox?
7 | # For consistency with the libui C API naming convention,
8 | # we use a single Box class with orientation parameter.
9 | # This matches the libui functions like `uiBoxAppend`, `uiBoxSetPadded`, etc.
10 |
11 | class Box < Control
12 | block_constructor
13 |
14 | @ref_ptr : Pointer(LibUI::Box)
15 | @children_refs : Array(Control) = [] of Control
16 |
17 | def initialize(orientation : Symbol, padded : Bool = false)
18 | case orientation
19 | when :horizontal
20 | @ref_ptr = LibUI.new_horizontal_box
21 | when :vertical
22 | @ref_ptr = LibUI.new_vertical_box
23 | else
24 | raise "Invalid orientation: #{orientation}"
25 | end
26 | self.padded = true if padded
27 | end
28 |
29 | def destroy
30 | @children_refs.each do |child|
31 | child.release_ownership
32 | end
33 | super
34 | end
35 |
36 | def delete(child : Control)
37 | if index = @children_refs.index(child)
38 | delete(index)
39 | end
40 | end
41 |
42 | def append(control, stretchy : Bool = false) : Nil
43 | control.check_can_move
44 | LibUI.box_append(@ref_ptr, UIng.to_control(control), stretchy)
45 | @children_refs << control
46 | control.take_ownership(self)
47 | end
48 |
49 | # For DSL style
50 | def append(stretchy : Bool = false, &block : -> Control) : Nil
51 | control = block.call
52 | append(control)
53 | end
54 |
55 | def num_children : Int32
56 | LibUI.box_num_children(@ref_ptr)
57 | end
58 |
59 | def delete(index : Int32) : Nil
60 | child = @children_refs[index]
61 | LibUI.box_delete(@ref_ptr, index)
62 | @children_refs.delete_at(index)
63 | child.release_ownership
64 | end
65 |
66 | def padded? : Bool
67 | LibUI.box_padded(@ref_ptr)
68 | end
69 |
70 | def padded=(padded : Bool) : Nil
71 | LibUI.box_set_padded(@ref_ptr, padded)
72 | end
73 |
74 | def to_unsafe
75 | @ref_ptr
76 | end
77 | end
78 | end
79 |
--------------------------------------------------------------------------------
/src/uing/table/table/model.cr:
--------------------------------------------------------------------------------
1 | require "./model/handler"
2 |
3 | module UIng
4 | # Table::Model manages data for Table controls.
5 | #
6 | # CRITICAL MEMORY MANAGEMENT WARNINGS:
7 | # 1. Table::Model MUST be freed AFTER all Tables using it are destroyed
8 | # 2. Table::Model::Handler callbacks become invalid after Table::Model is freed
9 | # 3. DO NOT rely on GC/finalize for automatic cleanup - use explicit free()
10 | # 4. Avoid circular references in Table::Model::Handler callbacks
11 | #
12 | # Safe usage pattern:
13 | # model_handler = Table::Model::Handler.new
14 | # model = Table::Model.new(model_handler)
15 | # table = Table.new(Table::Params.new(model))
16 | # # ... use table ...
17 | # table.destroy # Destroy table first
18 | # model.free # Then free model
19 | class Table < Control
20 | class Model
21 | @released : Bool = false
22 |
23 | # Store Table::Model::Handler reference to prevent GC collection
24 | # IMPORTANT: This prevents GC of handler while model is alive
25 | @model_handler_ref : Handler?
26 |
27 | def initialize(@ref_ptr : Pointer(LibUI::TableModel))
28 | end
29 |
30 | def initialize(model_handler : Handler)
31 | @ref_ptr = LibUI.new_table_model(model_handler)
32 | @model_handler_ref = model_handler
33 | end
34 |
35 | # Explicitly free the Table::Model.
36 | # WARNING: Only call this AFTER all Tables using this model are destroyed.
37 | # Calling this while Tables are still active will cause crashes.
38 | def free : Nil
39 | return if @released
40 | LibUI.free_table_model(@ref_ptr)
41 | @released = true
42 | # Clear handler reference to allow GC
43 | @model_handler_ref = nil
44 | end
45 |
46 | def row_inserted(new_index : Int32) : Nil
47 | LibUI.table_model_row_inserted(@ref_ptr, new_index)
48 | end
49 |
50 | def row_changed(index : Int32) : Nil
51 | LibUI.table_model_row_changed(@ref_ptr, index)
52 | end
53 |
54 | def row_deleted(old_index : Int32) : Nil
55 | LibUI.table_model_row_deleted(@ref_ptr, old_index)
56 | end
57 |
58 | def to_unsafe
59 | @ref_ptr
60 | end
61 | end
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/src/uing/menu.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | class Menu
3 | include BlockConstructor; block_constructor
4 |
5 | # Mutex
6 | @@mutex = Mutex.new
7 |
8 | # Store references to Menu to prevent GC collection
9 | @@menu : Array(Menu) = [] of Menu
10 |
11 | @@has_quit_item = false
12 | @@has_preferences_item = false
13 | @@has_about_item = false
14 |
15 | # Store references to MenuItems to prevent GC collection
16 | @menu_items : Array(MenuItem) = [] of MenuItem
17 |
18 | def initialize(name : String)
19 | @ref_ptr = LibUI.new_menu(name)
20 | @@mutex.synchronize do
21 | @@menu << self
22 | end
23 | end
24 |
25 | def append_item(name : String) : MenuItem
26 | ref_ptr = LibUI.menu_append_item(@ref_ptr, name)
27 | item = MenuItem.new(ref_ptr)
28 | @menu_items << item
29 | item
30 | end
31 |
32 | def append_check_item(name : String) : MenuItem
33 | ref_ptr = LibUI.menu_append_check_item(@ref_ptr, name)
34 | item = MenuItem.new(ref_ptr)
35 | @menu_items << item
36 | item
37 | end
38 |
39 | def append_check_item(name : String, checked : Bool) : MenuItem
40 | item = append_check_item(name)
41 | item.checked = checked
42 | item
43 | end
44 |
45 | def append_quit_item : MenuItem
46 | raise "Quit item already exists" if @@has_quit_item
47 | ref_ptr = LibUI.menu_append_quit_item(@ref_ptr)
48 | item = MenuItem.new(ref_ptr)
49 | @menu_items << item
50 | @@has_quit_item = true
51 | item
52 | end
53 |
54 | def append_preferences_item : MenuItem
55 | raise "Preferences item already exists" if @@has_preferences_item
56 | ref_ptr = LibUI.menu_append_preferences_item(@ref_ptr)
57 | item = MenuItem.new(ref_ptr)
58 | @menu_items << item
59 | @@has_preferences_item = true
60 | item
61 | end
62 |
63 | def append_about_item : MenuItem
64 | raise "About item already exists" if @@has_about_item
65 | ref_ptr = LibUI.menu_append_about_item(@ref_ptr)
66 | item = MenuItem.new(ref_ptr)
67 | @menu_items << item
68 | @@has_about_item = true
69 | item
70 | end
71 |
72 | def append_separator : Nil
73 | LibUI.menu_append_separator(@ref_ptr)
74 | end
75 |
76 | def to_unsafe
77 | @ref_ptr
78 | end
79 | end
80 | end
81 |
--------------------------------------------------------------------------------
/examples/video_player/src/platform_embedding.cr:
--------------------------------------------------------------------------------
1 | # Platform-specific embedding implementations for MPV player
2 |
3 | module PlatformEmbedding
4 | # Common interface for all platforms
5 | abstract def setup_platform_embedding(raw_handle : UInt64)
6 | abstract def apply_platform_settings
7 |
8 | # macOS-specific embedding module
9 | module MacOS
10 | def setup_macos_embedding(raw_handle : UInt64)
11 | handle = raw_handle.to_i64
12 |
13 | set_window_id(handle)
14 | set_video_output("gpu")
15 | apply_platform_settings
16 |
17 | puts "Set macOS NSView handle: #{handle} with gpu mode"
18 | end
19 |
20 | def apply_macos_settings
21 | set_property("hwdec", "auto")
22 | set_property("fullscreen", "no")
23 | end
24 | end
25 |
26 | # Windows-specific embedding module
27 | module Windows
28 | def setup_windows_embedding(raw_handle : UInt64)
29 | handle = raw_handle.to_i64
30 | set_window_id(handle)
31 | set_video_output("direct3d")
32 | apply_platform_settings
33 | puts "Set Windows HWND handle: #{handle} with direct3d mode"
34 | end
35 |
36 | def apply_windows_settings
37 | set_property("hwdec", "auto")
38 | set_property("fullscreen", "no")
39 | set_property("d3d11-adapter", "auto")
40 | end
41 | end
42 |
43 | # Linux-specific embedding module
44 | module Linux
45 | def setup_linux_embedding(raw_handle : UInt64)
46 | widget = Pointer(Void).new(raw_handle).as(LibGTK::GtkWidget)
47 | LibGTK.gtk_widget_realize(widget)
48 | gdk_window = LibGTK.gtk_widget_get_window(widget)
49 |
50 | type_name_ptr = LibGTK.g_type_name_from_instance(gdk_window.as(LibGTK::GTypeInstance))
51 | type_name = String.new(type_name_ptr)
52 | puts "Window type: #{type_name}"
53 |
54 | case type_name
55 | when "GdkX11Window"
56 | setup_x11_window(gdk_window)
57 | else
58 | raise "Unsupported window type: #{type_name}"
59 | end
60 | end
61 |
62 | def setup_x11_window(gdk_window)
63 | x11_window_id = LibGDK.gdk_x11_window_get_xid(gdk_window)
64 | handle = x11_window_id.to_i64
65 |
66 | set_video_output("x11")
67 | set_window_id(handle)
68 |
69 | puts "Set X11 window ID: #{handle}"
70 | end
71 |
72 | def apply_linux_settings
73 | # Linux-specific settings can be added here
74 | end
75 | end
76 | end
77 |
--------------------------------------------------------------------------------
/src/uing/date_time_picker.cr:
--------------------------------------------------------------------------------
1 | require "./control"
2 |
3 | module UIng
4 | class DateTimePicker < Control
5 | block_constructor
6 |
7 | # Store callback box to prevent GC collection
8 | @on_changed_box : Pointer(Void)?
9 | # Keep TM instance to avoid repeated allocation and ensure memory safety
10 | @tm : UIng::TM
11 |
12 | def initialize(type : Symbol)
13 | case type
14 | when :date
15 | @ref_ptr = LibUI.new_date_picker
16 | when :time
17 | @ref_ptr = LibUI.new_time_picker
18 | when :date_time
19 | @ref_ptr = LibUI.new_date_time_picker
20 | else
21 | raise "Invalid type: #{type}"
22 | end
23 | @tm = UIng::TM.new
24 | end
25 |
26 | def initialize
27 | @ref_ptr = LibUI.new_date_time_picker
28 | @tm = UIng::TM.new
29 | end
30 |
31 | def destroy
32 | @on_changed_box = nil
33 | super
34 | end
35 |
36 | def time : Time
37 | return Time.local unless @ref_ptr
38 |
39 | begin
40 | LibUI.date_time_picker_time(@ref_ptr, @tm)
41 | @tm.to_time
42 | rescue e
43 | UIng.handle_callback_error(e, "DateTimePicker time retrieval")
44 | Time.local
45 | end
46 | end
47 |
48 | def time=(time : Time) : Nil
49 | return unless @ref_ptr
50 |
51 | begin
52 | # Update our persistent @tm instance with new time
53 | temp_tm = UIng::TM.new(time)
54 | LibUI.date_time_picker_set_time(@ref_ptr, temp_tm)
55 | # Sync @tm with the actual widget state
56 | LibUI.date_time_picker_time(@ref_ptr, @tm)
57 | rescue e
58 | UIng.handle_callback_error(e, "DateTimePicker time setting")
59 | end
60 | end
61 |
62 | def on_changed(&block : Time -> Nil) : Nil
63 | wrapper = -> : Nil {
64 | LibUI.date_time_picker_time(@ref_ptr, @tm)
65 | current_time = @tm.to_time
66 | block.call(current_time)
67 | }
68 | @on_changed_box = ::Box.box(wrapper)
69 | if boxed_data = @on_changed_box
70 | LibUI.date_time_picker_on_changed(
71 | @ref_ptr,
72 | ->(_sender, data) : Nil {
73 | data_as_callback = ::Box(typeof(wrapper)).unbox(data)
74 | data_as_callback.call
75 | },
76 | boxed_data
77 | )
78 | end
79 | end
80 |
81 | def to_unsafe
82 | @ref_ptr
83 | end
84 | end
85 | end
86 |
--------------------------------------------------------------------------------
/src/uing/table/table/selection.cr:
--------------------------------------------------------------------------------
1 | require "./selection/mode"
2 |
3 | module UIng
4 | # Table::Selection represents selected rows in a Table.
5 | #
6 | # AUTOMATIC MEMORY MANAGEMENT:
7 | # Table::Selection is automatically managed by the UIng library.
8 | # Users do NOT need to manually free Table::Selection objects.
9 | #
10 | # Recommended usage patterns:
11 | #
12 | # 1. Using on_selection_changed callback (RECOMMENDED):
13 | # table.on_selection_changed do |selection|
14 | # if selection.num_rows > 0
15 | # selected_row = selection.rows[0]
16 | # # ... use selection data ...
17 | # end
18 | # # Table::Selection is automatically freed after this block
19 | # end
20 | #
21 | # 2. Manual selection access (use with caution):
22 | # selection = table.selection # Get selection
23 | # rows = selection.num_rows # Extract data immediately
24 | # # ... use selection data ...
25 | # selection.free # MUST free manually when using this pattern
26 | #
27 | # 3. Setting a custom Table::Selection object via `table.selection =`:
28 | # You can create a Table::Selection manually using `Table::Selection.new(...)`
29 | # and assign it to a table.
30 | # The data will be immediately copied or consumed by libui-ng,
31 | # so the object does **not** need to be freed manually.
32 | # Memory is automatically managed by Crystal's garbage collector.
33 | class Table < Control
34 | class Selection
35 | @rows : Array(Int32)?
36 | @released : Bool = false
37 |
38 | def initialize(@ptr : Pointer(LibUI::TableSelection))
39 | @rows = nil
40 | @cstruct = nil
41 | end
42 |
43 | def initialize(rows : Array(Int32))
44 | # Create a new Table::Selection with the given rows
45 | @rows = rows
46 | @cstruct = LibUI::TableSelection.new(@rows.to_unsafe, @rows.size)
47 | @ptr = Pointer(LibUI::TableSelection).new(@cstruct)
48 | end
49 |
50 | def num_rows : Int32
51 | @ptr.value.num_rows
52 | end
53 |
54 | def rows : Pointer(Int32)
55 | @ptr.value.rows
56 | end
57 |
58 | def free : Nil
59 | return if @rows
60 | return if @released # Prevent double-free
61 | LibUI.free_table_selection(@ptr)
62 | @released = true
63 | end
64 |
65 | def to_unsafe
66 | @ptr
67 | end
68 |
69 | # Note: No finalize method needed for Table::Selection
70 | end
71 | end
72 | end
73 |
--------------------------------------------------------------------------------
/src/uing/area/area/attribute/open_type_features.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | class OpenTypeFeatures
3 | @released : Bool = false
4 | @for_each_box : Pointer(Void)?
5 |
6 | def initialize
7 | @ref_ptr = LibUI.new_open_type_features
8 | end
9 |
10 | # Used in clone method
11 | def initialize(ref_ptr : Pointer(LibUI::OpenTypeFeatures))
12 | @ref_ptr = ref_ptr
13 | end
14 |
15 | def free : Nil
16 | return if @released
17 | LibUI.free_open_type_features(@ref_ptr)
18 | @released = true
19 | end
20 |
21 | def clone : OpenTypeFeatures
22 | ref_ptr = LibUI.open_type_features_clone(@ref_ptr)
23 | OpenTypeFeatures.new(ref_ptr)
24 | end
25 |
26 | def add(tag : String, value : Int32 = 1) : Nil
27 | raise ArgumentError.new("OpenType tag must be exactly 4 characters") unless tag.size == 4
28 | bytes = tag.bytes
29 | LibUI.open_type_features_add(@ref_ptr, bytes[0], bytes[1], bytes[2], bytes[3], value.to_u32)
30 | end
31 |
32 | def remove(tag : String) : Nil
33 | raise ArgumentError.new("OpenType tag must be exactly 4 characters") unless tag.size == 4
34 | bytes = tag.bytes
35 | LibUI.open_type_features_remove(@ref_ptr, bytes[0], bytes[1], bytes[2], bytes[3])
36 | end
37 |
38 | def get(tag : String) : {Bool, Int32}
39 | raise ArgumentError.new("OpenType tag must be exactly 4 characters") unless tag.size == 4
40 | bytes = tag.bytes
41 | result = LibUI.open_type_features_get(@ref_ptr, bytes[0], bytes[1], bytes[2], bytes[3], out value)
42 | {result, value.to_i32}
43 | end
44 |
45 | # FIXME : Is this appropriate for OpenTypeFeatures?
46 |
47 | def for_each(&callback : (String, Int32) -> _) : Nil
48 | @for_each_box = ::Box.box(callback)
49 | proc = ->(_otf : Pointer(LibUI::OpenTypeFeatures), a : LibC::Char, b : LibC::Char, c : LibC::Char, d : LibC::Char, value : UInt32, data : Pointer(Void)) : LibC::Int do
50 | data_as_callback = ::Box(typeof(callback)).unbox(data)
51 | tag = "#{a.chr}#{b.chr}#{c.chr}#{d.chr}"
52 | data_as_callback.call(tag, value.to_i32)
53 | 0 # uiForEachContinue
54 | end
55 | LibUI.open_type_features_for_each(@ref_ptr, proc, @for_each_box.not_nil!)
56 |
57 | # Clear the box reference after enumeration completes
58 | @for_each_box = nil
59 | end
60 |
61 | def to_unsafe
62 | @ref_ptr
63 | end
64 |
65 | def finalize
66 | # Releasing timing is not critical for this class
67 | free
68 | end
69 | end
70 | end
71 |
--------------------------------------------------------------------------------
/src/uing/font_descriptor.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | class FontDescriptor
3 | # Store reference to family string to prevent garbage collection
4 | @family_string : String = ""
5 | @family_borrowed = false # Getting a FontDescriptor from FontButton.
6 |
7 | @released = false
8 |
9 | def initialize(@cstruct : LibUI::FontDescriptor = LibUI::FontDescriptor.new)
10 | end
11 |
12 | def initialize(
13 | family : String? = nil, size : Int32? = nil, weight : TextWeight? = nil,
14 | italic : TextItalic? = nil, stretch : TextStretch? = nil,
15 | )
16 | @cstruct = LibUI::FontDescriptor.new
17 | load_control_font unless family && size && weight && italic && stretch
18 | self.family = family if family
19 | self.size = size if size
20 | self.weight = weight if weight
21 | self.italic = italic if italic
22 | self.stretch = stretch if stretch
23 | end
24 |
25 | # Auto convert to and from String
26 | def family
27 | if @cstruct.family.null?
28 | ""
29 | else
30 | # This copies the string from the C struct to a Crystal String
31 | String.new(@cstruct.family)
32 | end
33 | end
34 |
35 | def family=(value : String)
36 | @family_string = value
37 | @family_borrowed = false # manage memory on crystal side.
38 | @cstruct.family = @family_string.to_unsafe
39 | end
40 |
41 | def size
42 | @cstruct.size
43 | end
44 |
45 | def size=(value)
46 | @cstruct.size = value
47 | end
48 |
49 | def weight
50 | @cstruct.weight
51 | end
52 |
53 | def weight=(value)
54 | @cstruct.weight = value
55 | end
56 |
57 | def italic
58 | @cstruct.italic
59 | end
60 |
61 | def italic=(value)
62 | @cstruct.italic = value
63 | end
64 |
65 | def stretch
66 | @cstruct.stretch
67 | end
68 |
69 | def stretch=(value)
70 | @cstruct.stretch = value
71 | end
72 |
73 | def free : Nil
74 | return if @released
75 | @cstruct.family = Pointer(UInt8).null unless @family_borrowed
76 | LibUI.free_font_descriptor(to_unsafe)
77 | @released = true
78 | end
79 |
80 | def load_control_font : Nil
81 | LibUI.load_control_font(to_unsafe)
82 | @family_string = String.new(@cstruct.family)
83 | @family_borrowed = true # The family string is borrowed from the control font.
84 | end
85 |
86 | def to_unsafe
87 | pointerof(@cstruct)
88 | end
89 |
90 | def finalize
91 | free
92 | end
93 | end
94 | end
95 |
--------------------------------------------------------------------------------
/src/uing/slider.cr:
--------------------------------------------------------------------------------
1 | require "./control"
2 |
3 | module UIng
4 | class Slider < Control
5 | block_constructor
6 |
7 | # Store callback boxes to prevent GC collection
8 | @on_changed_box : Pointer(Void)?
9 | @on_released_box : Pointer(Void)?
10 |
11 | def initialize(min, max)
12 | @ref_ptr = LibUI.new_slider(min, max)
13 | end
14 |
15 | def destroy
16 | @on_changed_box = nil
17 | @on_released_box = nil
18 | super
19 | end
20 |
21 | def initialize(min, max, value)
22 | @ref_ptr = LibUI.new_slider(min, max)
23 | self.value = value
24 | end
25 |
26 | def value : Int32
27 | LibUI.slider_value(@ref_ptr)
28 | end
29 |
30 | def value=(value : Int32) : Nil
31 | LibUI.slider_set_value(@ref_ptr, value)
32 | end
33 |
34 | def has_tool_tip? : Bool
35 | LibUI.slider_has_tool_tip(@ref_ptr)
36 | end
37 |
38 | def has_tool_tip=(has_tool_tip : Bool) : Nil
39 | LibUI.slider_set_has_tool_tip(@ref_ptr, has_tool_tip)
40 | end
41 |
42 | def set_range(min : Int32, max : Int32) : Nil
43 | LibUI.slider_set_range(@ref_ptr, min, max)
44 | end
45 |
46 | def set_range(range : Range(Int32, Int32)) : Nil
47 | LibUI.slider_set_range(@ref_ptr, range.min, range.max)
48 | end
49 |
50 | def on_changed(&block : Int32 -> _) : Nil
51 | wrapper = -> : Nil {
52 | v = value
53 | block.call(v)
54 | }
55 | @on_changed_box = ::Box.box(wrapper)
56 | if boxed_data = @on_changed_box
57 | LibUI.slider_on_changed(
58 | @ref_ptr,
59 | ->(_sender, data) : Nil {
60 | begin
61 | data_as_callback = ::Box(typeof(wrapper)).unbox(data)
62 | data_as_callback.call
63 | rescue e
64 | UIng.handle_callback_error(e, "Slider on_changed")
65 | end
66 | },
67 | boxed_data
68 | )
69 | end
70 | end
71 |
72 | def on_released(&block : Int32 -> Nil) : Nil
73 | wrapper = -> : Nil {
74 | v = value
75 | block.call(v)
76 | }
77 | @on_released_box = ::Box.box(wrapper)
78 | if boxed_data = @on_released_box
79 | LibUI.slider_on_released(
80 | @ref_ptr,
81 | ->(_sender, data) : Nil {
82 | begin
83 | data_as_callback = ::Box(typeof(wrapper)).unbox(data)
84 | data_as_callback.call
85 | rescue e
86 | UIng.handle_callback_error(e, "Slider on_released")
87 | end
88 | },
89 | boxed_data
90 | )
91 | end
92 | end
93 |
94 | def to_unsafe
95 | @ref_ptr
96 | end
97 | end
98 | end
99 |
--------------------------------------------------------------------------------
/src/uing/multiline_entry.cr:
--------------------------------------------------------------------------------
1 | require "./control"
2 |
3 | module UIng
4 | class MultilineEntry < Control
5 | block_constructor
6 |
7 | # Store callback box to prevent GC collection
8 | @on_changed_box : Pointer(Void)?
9 |
10 | def initialize(wrapping = true, read_only = false)
11 | if wrapping
12 | @ref_ptr = LibUI.new_multiline_entry
13 | else
14 | @ref_ptr = LibUI.new_non_wrapping_multiline_entry
15 | end
16 | if read_only
17 | self.read_only = true
18 | end
19 | end
20 |
21 | def destroy
22 | @on_changed_box = nil
23 | super
24 | end
25 |
26 | def text : String?
27 | str_ptr = LibUI.multiline_entry_text(@ref_ptr)
28 | UIng.string_from_pointer(str_ptr)
29 | end
30 |
31 | def text=(text : String) : Nil
32 | LibUI.multiline_entry_set_text(@ref_ptr, text)
33 | end
34 |
35 | def append(text : String) : Nil
36 | LibUI.multiline_entry_append(@ref_ptr, text)
37 | end
38 |
39 | def read_only? : Bool
40 | LibUI.multiline_entry_read_only(@ref_ptr)
41 | end
42 |
43 | def read_only=(readonly : Bool) : Nil
44 | LibUI.multiline_entry_set_read_only(@ref_ptr, readonly)
45 | end
46 |
47 | def on_changed(&block : -> Nil) : Nil
48 | @on_changed_box = ::Box.box(block)
49 | if boxed_data = @on_changed_box
50 | LibUI.multiline_entry_on_changed(
51 | @ref_ptr,
52 | ->(_sender, data) : Nil {
53 | begin
54 | data_as_callback = ::Box(typeof(block)).unbox(data)
55 | data_as_callback.call
56 | rescue e
57 | UIng.handle_callback_error(e, "MultilineEntry on_changed")
58 | end
59 | },
60 | boxed_data
61 | )
62 | end
63 | end
64 |
65 | # If a large amount of text is entered in the multiline entry,
66 | # it is heavy to get the text in the callback, so a separate method is provided.
67 |
68 | def on_changed_with_text(&block : String -> Nil) : Nil
69 | wrapper = -> : Nil {
70 | current_text = text || ""
71 | block.call(current_text)
72 | }
73 | @on_changed_box = ::Box.box(wrapper)
74 | if boxed_data = @on_changed_box
75 | LibUI.multiline_entry_on_changed(
76 | @ref_ptr,
77 | ->(_sender, data) : Nil {
78 | begin
79 | data_as_callback = ::Box(typeof(wrapper)).unbox(data)
80 | data_as_callback.call
81 | rescue e
82 | UIng.handle_callback_error(e, "MultilineEntry on_changed_with_text")
83 | end
84 | },
85 | boxed_data
86 | )
87 | end
88 | end
89 |
90 | def to_unsafe
91 | @ref_ptr
92 | end
93 | end
94 | end
95 |
--------------------------------------------------------------------------------
/src/uing/table/table/value.cr:
--------------------------------------------------------------------------------
1 | require "./value/type"
2 |
3 | module UIng
4 | class Table < Control
5 | class Value
6 | @released : Bool = false
7 | property? borrowed : Bool = false
8 |
9 | # Unified constructor - handles both borrowed and owned TableValue
10 | def initialize(@ref_ptr : Pointer(LibUI::TableValue), borrowed : Bool = true)
11 | @borrowed = borrowed
12 | end
13 |
14 | # Public constructors for creating new TableValue objects
15 | # These MUST be freed after use
16 | def initialize(str : String)
17 | @ref_ptr = LibUI.new_table_value_string(str)
18 | @borrowed = false
19 | end
20 |
21 | def initialize(image : Image)
22 | @ref_ptr = LibUI.new_table_value_image(image.to_unsafe)
23 | @borrowed = false
24 | end
25 |
26 | def initialize(i : Int32)
27 | @ref_ptr = LibUI.new_table_value_int(i)
28 | @borrowed = false
29 | end
30 |
31 | def initialize(r : Float64, g : Float64, b : Float64, a : Float64)
32 | @ref_ptr = LibUI.new_table_value_color(r, g, b, a)
33 | @borrowed = false
34 | end
35 |
36 | def self.new_color(r : Float64, g : Float64, b : Float64, a : Float64) : TableValue
37 | TableValue.new(r, g, b, a)
38 | end
39 |
40 | def free : Nil
41 | return if @released
42 | return if @borrowed # Don't free borrowed TableValue
43 | LibUI.free_table_value(@ref_ptr)
44 | @released = true
45 | end
46 |
47 | def type : Value::Type
48 | LibUI.table_value_get_type(@ref_ptr)
49 | end
50 |
51 | def string : String?
52 | str_ptr = LibUI.table_value_string(@ref_ptr)
53 | return nil if str_ptr.null?
54 | # DO NOT free the string pointer - it's borrowed from libui-ng
55 | String.new(str_ptr)
56 | end
57 |
58 | def image : Pointer(LibUI::Image)
59 | # Return the raw pointer - DO NOT create new Image object
60 | # The returned pointer is borrowed from libui-ng and should not be freed
61 | LibUI.table_value_image(@ref_ptr)
62 | end
63 |
64 | def int : Int32
65 | LibUI.table_value_int(@ref_ptr)
66 | end
67 |
68 | def color : {Float64, Float64, Float64, Float64}
69 | LibUI.table_value_color(@ref_ptr, out r, out g, out b, out a)
70 | {r, g, b, a}
71 | end
72 |
73 | def value
74 | case get_type
75 | when .string? then string
76 | when .image? then image
77 | when .int? then int
78 | when .color? then color
79 | else
80 | raise "Unknown TableValue type: #{get_type}"
81 | end
82 | end
83 |
84 | def to_unsafe
85 | @ref_ptr
86 | end
87 | end
88 | end
89 | end
90 |
--------------------------------------------------------------------------------
/examples/gallery/basic_draw_text.cr:
--------------------------------------------------------------------------------
1 | require "../../src/uing"
2 |
3 | UIng.init
4 |
5 | handler = UIng::Area::Handler.new
6 | area = UIng::Area.new(handler)
7 |
8 | title = "Michael Ende (1929-1995) The Neverending Story"
9 |
10 | str1 = \
11 | " At last Ygramul sensed that something was coming toward " \
12 | "her. With the speed of lightning, she turned about, confronting " \
13 | "Atreyu with an enormous steel-blue face. Her single eye had a " \
14 | "vertical pupil, which stared at Atreyu with inconceivable malignancy. "
15 |
16 | str2 = \
17 | " A cry of fear escaped Bastian. "
18 |
19 | str3 = \
20 | " A cry of terror passed through the ravine and echoed from " \
21 | "side to side. Ygramul turned her eye to left and right, to see if " \
22 | "someone else had arrived, for that sound could not have been " \
23 | "made by the boy who stood there as though paralyzed with " \
24 | "horror. "
25 |
26 | str4 = \
27 | " Could she have heard my cry? Bastion wondered in alarm. " \
28 | "But that's not possible. "
29 |
30 | str5 = \
31 | " And then Atreyu heard Ygramuls voice. It was very high " \
32 | "and slightly hoarse, not at all the right kind of voice for that " \
33 | "enormous face. Her lips did not move as she spoke. It was the " \
34 | "buzzing of a great swarm of hornets that shaped itself into " \
35 | "words. "
36 |
37 | ATTR_STR = UIng::Area::AttributedString.new("")
38 |
39 | RED = UIng::Area::Attribute.new_color(0.0, 0.5, 0.0, 0.7)
40 | GREEN = UIng::Area::Attribute.new_color(0.5, 0.0, 0.25, 0.7)
41 |
42 | DEFAULT_FONT = UIng::FontDescriptor.new(
43 | family: "Georgia",
44 | size: 13,
45 | weight: :normal,
46 | italic: :normal,
47 | stretch: :normal
48 | )
49 |
50 | def append_to_attr_str(attr_str, text, color)
51 | start = attr_str.len
52 | attr_str.append_unattributed(text)
53 | attr_str.set_attribute(color, start, start + text.bytesize)
54 | attr_str.append_unattributed("\n\n")
55 | end
56 |
57 | append_to_attr_str(ATTR_STR, str1, GREEN)
58 | append_to_attr_str(ATTR_STR, str2, RED)
59 | append_to_attr_str(ATTR_STR, str3, GREEN)
60 | append_to_attr_str(ATTR_STR, str4, RED)
61 | append_to_attr_str(ATTR_STR, str5, GREEN)
62 |
63 | handler.draw do |area, params|
64 | UIng::Area::Draw::TextLayout.open(
65 | string: ATTR_STR,
66 | default_font: DEFAULT_FONT,
67 | width: params.area_width,
68 | align: UIng::Area::Draw::TextAlign::Left
69 | ) do |text_layout|
70 | params.context.draw_text_layout(text_layout, 0, 0)
71 | end
72 | end
73 |
74 | handler.mouse_event { |_, _| }
75 | handler.mouse_crossed { |_, _| }
76 | handler.drag_broken { |_| }
77 | handler.key_event { |_, _| false }
78 |
79 | box = UIng::Box.new(:vertical)
80 | box.padded = true
81 | box.append area, true
82 |
83 | main_window = UIng::Window.new(title, 765, 430)
84 | main_window.margined = true
85 | main_window.child = box
86 |
87 | main_window.on_closing do
88 | ATTR_STR.free
89 | UIng.quit
90 | true
91 | end
92 | main_window.show
93 |
94 | UIng.main
95 | UIng.uninit
96 |
--------------------------------------------------------------------------------
/src/uing/tab.cr:
--------------------------------------------------------------------------------
1 | require "./control"
2 |
3 | module UIng
4 | class Tab < Control
5 | block_constructor
6 |
7 | # Store callback box to prevent GC collection
8 | @on_selected_box : Pointer(Void)?
9 | @children_refs : Array(Control) = [] of Control
10 |
11 | def initialize
12 | @ref_ptr = LibUI.new_tab
13 | end
14 |
15 | def destroy
16 | @children_refs.each do |child|
17 | child.release_ownership
18 | end
19 | @on_selected_box = nil
20 | super
21 | end
22 |
23 | def append(name : String, control, margined : Bool = false) : Nil
24 | control.check_can_move
25 | LibUI.tab_append(@ref_ptr, name, UIng.to_control(control))
26 | @children_refs << control
27 | control.take_ownership(self)
28 | index = num_pages - 1
29 | set_margined(index, margined) if margined
30 | end
31 |
32 | # For DSL style
33 | def append(name : String, margined : Bool = false, &block : -> Control) : Nil
34 | control = block.call
35 | append(name, control, margined)
36 | end
37 |
38 | def insert_at(name : String, index : Int32, control, margined : Bool = false) : Nil
39 | control.check_can_move
40 | LibUI.tab_insert_at(@ref_ptr, name, index, UIng.to_control(control))
41 | @children_refs.insert(index, control)
42 | control.take_ownership(self)
43 | set_margined(index, margined) if margined
44 | end
45 |
46 | def delete(index : Int32) : Nil
47 | child = @children_refs[index]
48 | LibUI.tab_delete(@ref_ptr, index)
49 | @children_refs.delete_at(index)
50 | child.release_ownership
51 | end
52 |
53 | def delete(child : Control)
54 | if index = @children_refs.index(child)
55 | delete(index)
56 | end
57 | end
58 |
59 | def num_pages : Int32
60 | LibUI.tab_num_pages(@ref_ptr)
61 | end
62 |
63 | def margined?(index : Int32) : Bool
64 | LibUI.tab_margined(@ref_ptr, index)
65 | end
66 |
67 | def set_margined(index : Int32, margined : Bool) : Nil
68 | LibUI.tab_set_margined(@ref_ptr, index, margined)
69 | end
70 |
71 | def selected : Int32
72 | LibUI.tab_selected(@ref_ptr)
73 | end
74 |
75 | def selected=(index : Int32) : Nil
76 | LibUI.tab_set_selected(@ref_ptr, index)
77 | end
78 |
79 | def on_selected(&block : Int32 -> Nil) : Nil
80 | wrapper = -> : Nil {
81 | idx = selected
82 | block.call(idx)
83 | }
84 | @on_selected_box = ::Box.box(wrapper)
85 | if boxed_data = @on_selected_box
86 | LibUI.tab_on_selected(
87 | @ref_ptr,
88 | ->(_sender, data) : Nil {
89 | begin
90 | data_as_callback = ::Box(typeof(wrapper)).unbox(data)
91 | data_as_callback.call
92 | rescue e
93 | UIng.handle_callback_error(e, "Tab on_selected")
94 | end
95 | },
96 | boxed_data
97 | )
98 | end
99 | end
100 |
101 | def to_unsafe
102 | @ref_ptr
103 | end
104 | end
105 | end
106 |
--------------------------------------------------------------------------------
/src/uing/area/area/attributed_string.cr:
--------------------------------------------------------------------------------
1 | require "./attribute/*"
2 |
3 | module UIng
4 | class Area < Control
5 | class AttributedString
6 | include BlockConstructor; block_constructor
7 |
8 | @released : Bool = false
9 | @for_each_attribute_box : Pointer(Void)?
10 |
11 | def initialize(@ref_ptr : Pointer(LibUI::AttributedString))
12 | end
13 |
14 | def initialize(string : String)
15 | @ref_ptr = LibUI.new_attributed_string(string)
16 | end
17 |
18 | def free : Nil
19 | return if @released
20 | LibUI.free_attributed_string(@ref_ptr)
21 | @released = true
22 | end
23 |
24 | def string : String?
25 | str_ptr = LibUI.attributed_string_string(@ref_ptr)
26 | # The returned string is owned by the attributed string?
27 | str_ptr.null? ? nil : String.new(str_ptr)
28 | end
29 |
30 | def len : LibC::SizeT
31 | LibUI.attributed_string_len(@ref_ptr)
32 | end
33 |
34 | def append_unattributed(text : String) : Nil
35 | LibUI.attributed_string_append_unattributed(@ref_ptr, text)
36 | end
37 |
38 | def insert_at_unattributed(text : String, at : LibC::SizeT) : Nil
39 | LibUI.attributed_string_insert_at_unattributed(@ref_ptr, text, at)
40 | end
41 |
42 | def delete(start : LibC::SizeT, end_ : LibC::SizeT) : Nil
43 | LibUI.attributed_string_delete(@ref_ptr, start, end_)
44 | end
45 |
46 | def set_attribute(attribute, start : LibC::SizeT, end_ : LibC::SizeT) : Nil
47 | LibUI.attributed_string_set_attribute(@ref_ptr, attribute, start, end_)
48 | # AttributedString takes ownership of the attribute
49 | if attribute.responds_to?(:released=)
50 | attribute.released = true
51 | end
52 | end
53 |
54 | # Return value: 0 = Continue, 1 = Stop (follows LibUI's uiForEach convention)
55 | def for_each_attribute(&block : (Attribute, LibC::SizeT, LibC::SizeT) -> LibC::Int) : Nil
56 | @for_each_attribute_box = ::Box.box(block)
57 |
58 | LibUI.attributed_string_for_each_attribute(@ref_ptr,
59 | ->(sender, attr, start, end_, data) do
60 | callback = ::Box(typeof(block)).unbox(data)
61 | # Convert to Attribute wrapper
62 | attribute = Area::Attribute.new(attr)
63 | # Return block's result directly to LibUI (0 or 1)
64 | callback.call(attribute, start, end_)
65 | end,
66 | @for_each_attribute_box.not_nil!
67 | )
68 |
69 | # Clear reference after enumeration
70 | @for_each_attribute_box = nil
71 | end
72 |
73 | def num_graphemes : LibC::SizeT
74 | LibUI.attributed_string_num_graphemes(@ref_ptr)
75 | end
76 |
77 | def byte_index_to_grapheme(pos : LibC::SizeT) : LibC::SizeT
78 | LibUI.attributed_string_byte_index_to_grapheme(@ref_ptr, pos)
79 | end
80 |
81 | def grapheme_to_byte_index(pos : LibC::SizeT) : LibC::SizeT
82 | LibUI.attributed_string_grapheme_to_byte_index(@ref_ptr, pos)
83 | end
84 |
85 | def to_unsafe
86 | @ref_ptr
87 | end
88 |
89 | def finalize
90 | free
91 | end
92 | end
93 | end
94 | end
95 |
--------------------------------------------------------------------------------
/src/uing/area/area/draw/stroke_params.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | class Area < Control
3 | module Draw
4 | class StrokeParams
5 | include BlockConstructor; block_constructor
6 |
7 | def initialize(cap : UIng::Area::Draw::LineCap = UIng::Area::Draw::LineCap::Flat,
8 | join : UIng::Area::Draw::LineJoin = UIng::Area::Draw::LineJoin::Miter,
9 | thickness : Number = 0.0,
10 | miter_limit : Number = 0.0,
11 | dash_phase : Number = 0.0,
12 | dashes : Enumerable(Float64)? = nil)
13 | @cstruct = LibUI::DrawStrokeParams.new
14 | @dashes_array = Array(Float64).new
15 |
16 | self.cap = cap if cap
17 | self.join = join if join
18 | self.thickness = thickness.to_f64 if thickness
19 | self.miter_limit = miter_limit.to_f64 if miter_limit
20 | self.dash_phase = dash_phase.to_f64 if dash_phase
21 | self.dashes = dashes if dashes
22 | end
23 |
24 | # Basic properties with direct delegation
25 | def cap : UIng::Area::Draw::LineCap
26 | @cstruct.cap
27 | end
28 |
29 | def cap=(value : UIng::Area::Draw::LineCap)
30 | @cstruct.cap = value
31 | end
32 |
33 | def join : UIng::Area::Draw::LineJoin
34 | @cstruct.join
35 | end
36 |
37 | def join=(value : UIng::Area::Draw::LineJoin)
38 | @cstruct.join = value
39 | end
40 |
41 | def thickness : Float64
42 | @cstruct.thickness
43 | end
44 |
45 | def thickness=(value : Float64)
46 | @cstruct.thickness = value
47 | end
48 |
49 | def miter_limit : Float64
50 | @cstruct.miter_limit
51 | end
52 |
53 | def miter_limit=(value : Float64)
54 | @cstruct.miter_limit = value
55 | end
56 |
57 | def dash_phase : Float64
58 | @cstruct.dash_phase
59 | end
60 |
61 | def dash_phase=(value : Float64)
62 | @cstruct.dash_phase = value
63 | end
64 |
65 | # Dashes property using Crystal Array
66 | def dashes : Array(Float64)
67 | @dashes_array
68 | end
69 |
70 | def dashes=(values : Array(Float64))
71 | @dashes_array = values
72 | sync_dashes
73 | end
74 |
75 | def dashes=(values : Enumerable(Float64))
76 | @dashes_array = values.to_a
77 | sync_dashes
78 | end
79 |
80 | def num_dashes : Int32
81 | @dashes_array.size
82 | end
83 |
84 | private def sync_dashes
85 | if @dashes_array.empty?
86 | @cstruct.dashes = Pointer(LibC::Double).null
87 | @cstruct.num_dashes = 0_u64
88 | else
89 | @cstruct.dashes = @dashes_array.to_unsafe.as(Pointer(LibC::Double))
90 | @cstruct.num_dashes = @dashes_array.size.to_u64
91 | end
92 | end
93 |
94 | def to_unsafe
95 | sync_dashes
96 | pointerof(@cstruct)
97 | end
98 | end
99 | end
100 | end
101 | end
102 |
--------------------------------------------------------------------------------
/src/uing/control.cr:
--------------------------------------------------------------------------------
1 | require "./block_constructor"
2 |
3 | module UIng
4 | abstract class Control
5 | include BlockConstructor
6 |
7 | # Flag to track if the control is released (prevents double-free)
8 | @released : Bool = false
9 |
10 | # Parent reference (for GC protection and tree uniqueness)
11 | # Use `__parent__` and `__set_parent__` if you need to access native functions for some reason
12 | protected getter parent : Control?
13 |
14 | # Public getter for parent (for testing and debugging)
15 | def parent : Control?
16 | @parent
17 | end
18 |
19 | # Helper method to check if this control can be moved to a new parent
20 | # Raises an exception if the control already has a parent (following libui-ng behavior)
21 | protected def check_can_move : Nil
22 | if @parent
23 | raise "You cannot give a uiControl a parent while it already has one"
24 | end
25 | end
26 |
27 | # Helper method to take ownership of this control by a new parent
28 | # Should only be called after check_can_move
29 | protected def take_ownership(new_parent : Control) : Nil
30 | @parent = new_parent
31 | end
32 |
33 | # Helper method to release ownership of this control (remove parent reference)
34 | protected def release_ownership : Nil
35 | @parent = nil
36 | end
37 |
38 | def destroy : Nil
39 | return if @released
40 | LibUI.control_destroy(UIng.to_control(@ref_ptr))
41 | @released = true
42 | end
43 |
44 | def handle
45 | LibUI.control_handle(UIng.to_control(@ref_ptr))
46 | end
47 |
48 | # native libui function
49 | def __parent__
50 | LibUI.control_parent(UIng.to_control(@ref_ptr))
51 | end
52 |
53 | # native libui function
54 | # should not be used directly
55 | def __set_parent__(parent) : Nil
56 | LibUI.control_set_parent(UIng.to_control(@ref_ptr), UIng.to_control(parent))
57 | end
58 |
59 | def toplevel? : Bool
60 | LibUI.control_toplevel(UIng.to_control(@ref_ptr))
61 | end
62 |
63 | def visible? : Bool
64 | LibUI.control_visible(UIng.to_control(@ref_ptr))
65 | end
66 |
67 | def show : Nil
68 | LibUI.control_show(UIng.to_control(@ref_ptr))
69 | end
70 |
71 | def hide : Nil
72 | LibUI.control_hide(UIng.to_control(@ref_ptr))
73 | end
74 |
75 | def enabled? : Bool
76 | LibUI.control_enabled(UIng.to_control(@ref_ptr))
77 | end
78 |
79 | def enable : Nil
80 | LibUI.control_enable(UIng.to_control(@ref_ptr))
81 | end
82 |
83 | def disable : Nil
84 | LibUI.control_disable(UIng.to_control(@ref_ptr))
85 | end
86 |
87 | def enabled_to_user? : Bool
88 | LibUI.control_enabled_to_user(UIng.to_control(@ref_ptr))
89 | end
90 |
91 | def verify_set_parent(parent) : Nil
92 | LibUI.control_verify_set_parent(UIng.to_control(@ref_ptr), UIng.to_control(parent))
93 | end
94 |
95 | def delete(child : Control)
96 | raise "delete(child : Control) is not implemented for #{self.class}"
97 | end
98 |
99 | abstract def to_unsafe
100 |
101 | def finalize
102 | @released = true
103 | end
104 | end
105 | end
106 |
--------------------------------------------------------------------------------
/.github/actions/screenshot/macos-screenshot.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 |
4 | APP_NAME="${1:?app binary name is required}"
5 | OUTPUT_FILE="${2:?output filename is required}"
6 |
7 | echo "Starting macOS window screenshot process for $APP_NAME"
8 |
9 | # Cleanup function to kill the app process
10 | cleanup() {
11 | echo "Cleaning up processes..."
12 | if [ -f app.pid ]; then
13 | APP_PID=$(cat app.pid)
14 | echo "Killing application process (PID: $APP_PID)..."
15 | kill "$APP_PID" 2>/dev/null || true
16 | rm -f app.pid
17 | fi
18 | }
19 | trap cleanup EXIT INT TERM
20 |
21 | # Launch the application (assume executable name = process name)
22 | echo "Launching application: ./$APP_NAME"
23 | "./$APP_NAME" &
24 | APP_PID=$!
25 | echo $APP_PID > app.pid
26 | echo "Application launched with PID: $APP_PID"
27 |
28 | # Wait for application startup
29 | sleep 3
30 |
31 | # Wait for the window to appear
32 | echo "Waiting for window to appear..."
33 | for i in {1..20}; do
34 | EXISTS=$(osascript -e "tell application \"System Events\" to return exists process \"$APP_NAME\"" || true)
35 | if [ "$EXISTS" = "true" ]; then
36 | HAS_WIN=$(osascript -e "tell application \"System Events\" to tell process \"$APP_NAME\" to return (count of windows) > 0" || true)
37 | if [ "$HAS_WIN" = "true" ]; then
38 | break
39 | fi
40 | fi
41 | sleep 0.5
42 | done
43 |
44 | if [ "$EXISTS" != "true" ] || [ "$HAS_WIN" != "true" ]; then
45 | echo "ERROR: process or window not found"
46 | osascript -e 'tell application "System Events" to get name of every process' | tr "," "\n" | head -50
47 | # Fallback: full screen capture
48 | screencapture -x "$OUTPUT_FILE"
49 | exit 0
50 | fi
51 |
52 | # Write AppleScript to a temporary file to avoid heredoc issues
53 | cat > get_rect.applescript <<'OSA'
54 | tell application "System Events"
55 | tell process "APP_NAME_PLACEHOLDER"
56 | set frontmost to true
57 | delay 1.0
58 | if (count of windows) = 0 then return "ERROR: no windows"
59 | set win to window 1
60 | set {xPos, yPos} to position of win
61 | set {wSize, hSize} to size of win
62 | set AppleScript's text item delimiters to ","
63 | return {xPos, yPos, wSize, hSize} as text
64 | end tell
65 | end tell
66 | OSA
67 |
68 | # Replace placeholder with actual app name
69 | sed -i '' "s/APP_NAME_PLACEHOLDER/$APP_NAME/g" get_rect.applescript
70 |
71 | # Get window rect as x,y,w,h
72 | RECT=$(osascript get_rect.applescript)
73 | case "$RECT" in
74 | ERROR:*) echo "$RECT"; screencapture -x "$OUTPUT_FILE"; exit 0 ;;
75 | esac
76 |
77 | # Remove whitespace and validate format
78 | RECT=$(echo "$RECT" | tr -d '[:space:]')
79 | if ! [[ "$RECT" =~ ^[0-9]+,[0-9]+,[0-9]+,[0-9]+$ ]]; then
80 | echo "ERROR: invalid rect: '$RECT'"
81 | screencapture -x "$OUTPUT_FILE"
82 | exit 0
83 | fi
84 | echo "Window rect: $RECT"
85 |
86 | # Check screencapture permission (TCC/GUI)
87 | if ! screencapture -x /tmp/_probe.png 2>/dev/null; then
88 | echo "Screen capture likely not permitted on hosted macOS runner (no GUI/TCC)."
89 | screencapture -x "$OUTPUT_FILE"
90 | exit 0
91 | fi
92 |
93 | # Capture the window region
94 | echo "Capturing with screencapture -R $RECT -> $OUTPUT_FILE"
95 | screencapture -x -t png -R "$RECT" "$OUTPUT_FILE"
96 | ls -la "$OUTPUT_FILE"
97 |
98 | echo "macOS window screenshot process completed successfully"
99 |
--------------------------------------------------------------------------------
/examples/video_player/src/mpv_bindings.cr:
--------------------------------------------------------------------------------
1 | # Crystal bindings for libmpv
2 |
3 | # Set locale for proper numeric formatting
4 | lib LibC
5 | fun setlocale(category : Int32, locale : UInt8*) : UInt8*
6 | end
7 |
8 | # Locale categories
9 | LC_NUMERIC = 1
10 |
11 | # GTK/GDK bindings for Linux only
12 | {% if flag?(:linux) %}
13 | @[Link("gtk-3")]
14 | lib LibGTK
15 | type GtkWidget = Void*
16 | type GdkWindow = Void*
17 | type GTypeInstance = Void*
18 |
19 | fun gtk_widget_realize(widget : GtkWidget) : Void
20 | fun gtk_widget_get_window(widget : GtkWidget) : GdkWindow
21 | fun g_type_name_from_instance(instance : GTypeInstance) : UInt8*
22 | end
23 |
24 | @[Link("gdk-3")]
25 | lib LibGDK
26 | fun gdk_x11_window_get_xid(window : LibGTK::GdkWindow) : UInt64
27 | end
28 | {% end %}
29 |
30 | @[Link("mpv")]
31 | lib LibMPV
32 | # Basic MPV functions (used)
33 | fun mpv_create : Void*
34 | fun mpv_initialize(ctx : Void*) : Int32
35 | fun mpv_terminate_destroy(ctx : Void*) : Void
36 |
37 | # Property functions (used)
38 | fun mpv_set_property(ctx : Void*, name : UInt8*, format : MPVFormat, data : Void*) : Int32
39 | fun mpv_set_property_string(ctx : Void*, name : UInt8*, data : UInt8*) : Int32
40 | fun mpv_get_property_string(ctx : Void*, name : UInt8*) : UInt8*
41 | fun mpv_get_property_async(ctx : Void*, reply_userdata : UInt64, name : UInt8*, format : MPVFormat) : Int32
42 | fun mpv_observe_property(ctx : Void*, reply_userdata : UInt64, name : UInt8*, format : MPVFormat) : Int32
43 |
44 | # Command functions (used)
45 | fun mpv_command_async(ctx : Void*, reply_userdata : UInt64, args : UInt8**) : Int32
46 |
47 | # Event functions (used)
48 | fun mpv_wait_event(ctx : Void*, timeout : Float64) : MPVEvent*
49 |
50 | # Utility functions (used)
51 | fun mpv_free(data : Void*) : Void
52 |
53 | # Error functions (used)
54 | fun mpv_error_string(error : Int32) : UInt8*
55 | fun mpv_event_name(event : Int32) : UInt8*
56 |
57 | # Enums (used)
58 | enum MPVFormat
59 | None = 0
60 | String = 1
61 | OSDString = 2
62 | Int64 = 4
63 | end
64 |
65 | enum MPVEventID
66 | None = 0
67 | LogMessage = 2
68 | GetPropertyReply = 3
69 | CommandReply = 5
70 | StartFile = 6
71 | EndFile = 7
72 | FileLoaded = 8
73 | Idle = 11
74 | VideoReconfig = 17
75 | AudioReconfig = 18
76 | PlaybackRestart = 19
77 | PropertyChange = 20
78 | end
79 |
80 | # Structures (used)
81 | struct MPVEvent
82 | event_id : MPVEventID
83 | error : Int32
84 | reply_userdata : UInt64
85 | data : Void*
86 | end
87 |
88 | struct MPVEventProperty
89 | name : UInt8*
90 | format : MPVFormat
91 | data : Void*
92 | end
93 |
94 | struct MPVEventLogMessage
95 | prefix : UInt8*
96 | level : UInt8*
97 | text : UInt8*
98 | log_level : MPVLogLevel
99 | end
100 |
101 | struct MPVEventEndFile
102 | reason : Int32
103 | error : Int32
104 | end
105 |
106 | enum MPVLogLevel
107 | None = 0
108 | Fatal = 10
109 | Error = 20
110 | Warn = 30
111 | Info = 40
112 | V = 50
113 | Debug = 60
114 | Trace = 70
115 | end
116 | end
117 |
--------------------------------------------------------------------------------
/src/uing/tm.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | class TM
3 | {% unless flag?(:windows) %}
4 | # C-side memory managed zone string pointer
5 | @zone_cstr : Pointer(UInt8)? = nil
6 | # Crystal-side reference for convenience methods
7 | @zone : String? = nil
8 | {% end %}
9 |
10 | def initialize(@cstruct : LibUI::TM = LibUI::TM.new)
11 | {% unless flag?(:windows) %}
12 | # Explicitly initialize zone to NULL for safety
13 | @cstruct.zone = Pointer(UInt8).null
14 | {% end %}
15 | end
16 |
17 | # Overloaded constructor: Convert Time to TM
18 | def initialize(time : ::Time)
19 | @cstruct = LibUI::TM.new
20 | {% unless flag?(:windows) %}
21 | @cstruct.zone = Pointer(UInt8).null
22 | {% end %}
23 |
24 | self.year = time.year - 1900 # tm_year is years since 1900
25 | self.mon = time.month - 1 # tm_mon is 0-based (0-11)
26 | self.mday = time.day
27 | self.hour = time.hour
28 | self.min = time.minute
29 | self.sec = time.second
30 | self.wday = time.day_of_week.to_i % 7 # 0 = Sunday
31 | self.yday = time.day_of_year - 1 # 0-based
32 | self.isdst = 0 # Not handling DST
33 | {% unless flag?(:windows) %}
34 | self.gmtoff = time.offset.to_i64
35 | self.zone = time.location.name
36 | {% end %}
37 | end
38 |
39 | {% unless flag?(:windows) %}
40 | def zone : String?
41 | # Return Crystal-side reference if available
42 | return @zone if @zone
43 | # Otherwise copy from C pointer if not NULL
44 | ptr = @cstruct.zone
45 | return nil if ptr.null?
46 | String.new(ptr)
47 | end
48 |
49 | def zone=(value : String)
50 | # Free previous C memory if allocated
51 | if (old = @zone_cstr)
52 | LibC.free(old.as(Void*))
53 | @zone_cstr = nil
54 | end
55 |
56 | # Allocate C memory with strdup for safe ownership
57 | dup = LibC.strdup(value)
58 | raise "strdup failed" if dup.null?
59 |
60 | @zone_cstr = dup
61 | @cstruct.zone = dup
62 | @zone = value
63 | end
64 |
65 | def finalize
66 | # Clean up owned C memory
67 | if (ptr = @zone_cstr)
68 | LibC.free(ptr.as(Void*))
69 | @zone_cstr = nil
70 | end
71 | end
72 | {% end %}
73 |
74 | forward_missing_to(@cstruct)
75 |
76 | def to_unsafe
77 | pointerof(@cstruct)
78 | end
79 |
80 | # Convert TM to Time with error handling
81 | def to_time : ::Time
82 | begin
83 | ::Time.local(
84 | year + 1900, # tm_year is years since 1900
85 | mon + 1, # tm_mon is 0-based (0-11)
86 | mday,
87 | hour,
88 | min,
89 | sec,
90 | nanosecond: 0
91 | )
92 | rescue e
93 | # Fallback to current time if conversion fails
94 | ::Time.local
95 | end
96 | end
97 |
98 | # Delegate to_s to Time for convenient formatting
99 | def to_s(io : IO, format : String) : Nil
100 | to_time.to_s(io, format)
101 | end
102 |
103 | def to_s(format : String) : String
104 | to_time.to_s(format)
105 | end
106 |
107 | def to_s(io : IO) : Nil
108 | to_time.to_s(io)
109 | end
110 | end
111 | end
112 |
--------------------------------------------------------------------------------
/.github/actions/screenshot/action.yml:
--------------------------------------------------------------------------------
1 | name: "Take Screenshot"
2 | description: "Take a screenshot of one or more applications on different platforms"
3 | inputs:
4 | os:
5 | description: "Operating system (ubuntu or windows or macos)"
6 | required: true
7 | app-names:
8 | description: "Space-separated list of application executable names"
9 | required: true
10 | output-files:
11 | description: "Space-separated list of output screenshot filenames (must match app-names order)"
12 | required: true
13 | outputs:
14 | screenshot-paths:
15 | description: "Space-separated list of generated screenshot paths"
16 | value: ${{ steps.screenshot.outputs.paths }}
17 | runs:
18 | using: "composite"
19 | steps:
20 | - name: Take screenshots on Ubuntu
21 | id: screenshot-ubuntu
22 | if: inputs.os == 'ubuntu'
23 | shell: bash
24 | run: |
25 | chmod +x ${{ github.action_path }}/ubuntu-screenshot.sh
26 | IFS=' ' read -r -a apps <<< "${{ inputs.app-names }}"
27 | IFS=' ' read -r -a outs <<< "${{ inputs.output-files }}"
28 | paths=""
29 | for i in "${!apps[@]}"; do
30 | app="${apps[$i]}"
31 | out="${outs[$i]}"
32 | echo "Taking screenshot for $app -> $out"
33 | ${{ github.action_path }}/ubuntu-screenshot.sh "$app" "$out"
34 | paths="$paths$out "
35 | done
36 | echo "paths=${paths}" >> $GITHUB_OUTPUT
37 |
38 | - name: Take screenshots on Windows
39 | id: screenshot-windows
40 | if: inputs.os == 'windows'
41 | shell: pwsh
42 | run: |
43 | $apps = "${{ inputs.app-names }}".Split(" ")
44 | $outs = "${{ inputs.output-files }}".Split(" ")
45 | $paths = ""
46 | for ($i = 0; $i -lt $apps.Length; $i++) {
47 | $app = $apps[$i]
48 | $out = $outs[$i]
49 | Write-Host "Taking screenshot for $app -> $out"
50 | & "${{ github.action_path }}/windows-screenshot.ps1" -AppName $app -OutputFile $out
51 | $paths += "$out "
52 | }
53 | echo "paths=$paths" >> $env:GITHUB_OUTPUT
54 |
55 | - name: Maximize macOS resolution (non-Retina)
56 | if: inputs.os == 'macos'
57 | shell: bash
58 | run: |
59 | brew install displayplacer
60 | DISPLAY_ID=$(displayplacer list | awk -F': ' '/Persistent screen id/{print $2; exit}')
61 | BEST_RES=$(displayplacer list | awk '/Resolution:/ {print $2}' \
62 | | awk -Fx '{print $1*$2, $1"x"$2}' \
63 | | sort -nr | head -1 | awk '{print $2}')
64 | echo "Selected best resolution: $BEST_RES"
65 | displayplacer "id:${DISPLAY_ID} res:${BEST_RES} hz:60 color_depth:8 scaling:off origin:(0,0) degree:0" || true
66 | sleep 2
67 |
68 | - name: Take screenshots on macOS
69 | id: screenshot-macos
70 | if: inputs.os == 'macos'
71 | shell: bash
72 | run: |
73 | chmod +x ${{ github.action_path }}/macos-screenshot.sh
74 | IFS=' ' read -r -a apps <<< "${{ inputs.app-names }}"
75 | IFS=' ' read -r -a outs <<< "${{ inputs.output-files }}"
76 | paths=""
77 | for i in "${!apps[@]}"; do
78 | app="${apps[$i]}"
79 | out="${outs[$i]}"
80 | echo "Taking screenshot for $app -> $out"
81 | ${{ github.action_path }}/macos-screenshot.sh "$app" "$out"
82 | paths="$paths$out "
83 | done
84 | echo "paths=${paths}" >> $GITHUB_OUTPUT
85 |
86 | - name: Set output
87 | id: screenshot
88 | shell: bash
89 | run: |
90 | if [ "${{ inputs.os }}" == "ubuntu" ]; then
91 | echo "paths=${{ steps.screenshot-ubuntu.outputs.paths }}" >> $GITHUB_OUTPUT
92 | elif [ "${{ inputs.os }}" == "windows" ]; then
93 | echo "paths=${{ steps.screenshot-windows.outputs.paths }}" >> $GITHUB_OUTPUT
94 | elif [ "${{ inputs.os }}" == "macos" ]; then
95 | echo "paths=${{ steps.screenshot-macos.outputs.paths }}" >> $GITHUB_OUTPUT
96 | fi
97 |
--------------------------------------------------------------------------------
/examples/md5_checker/src/app.cr:
--------------------------------------------------------------------------------
1 | require "uing"
2 | require "./checker"
3 | require "./table_handler"
4 |
5 | # UI Application
6 | class MD5CheckerApp
7 | @main_window : UIng::Window
8 | @file_path_entry : UIng::Entry
9 | @select_button : UIng::Button
10 | @run_button : UIng::Button
11 | @table_handler : MD5TableHandler
12 | @table_model : UIng::Table::Model
13 | @main_vbox : UIng::Box
14 | @table : UIng::Table
15 |
16 | def initialize
17 | UIng.init
18 |
19 | @main_window = UIng::Window.new("MD5 Checker", 600, 400)
20 | @main_window.margined = true
21 | @main_window.on_closing do
22 | cleanup
23 | UIng.quit
24 | true
25 | end
26 |
27 | # Initialize file path entry
28 | @file_path_entry = UIng::Entry.new
29 | @file_path_entry.text = "md5.txt"
30 |
31 | # Initialize buttons
32 | @select_button = UIng::Button.new("Browse...")
33 | @select_button.on_clicked { handle_file_selection }
34 |
35 | @run_button = UIng::Button.new("Run")
36 | @run_button.on_clicked { handle_run_button }
37 |
38 | # Create table model and handler
39 | @table_handler = MD5TableHandler.new
40 | @table_model = @table_handler.create_model
41 |
42 | # Create layout
43 | @main_vbox = UIng::Box.new(:vertical, padded: true)
44 | @main_window.child = @main_vbox
45 |
46 | # Top box
47 | top_hbox = UIng::Box.new(:horizontal)
48 | top_hbox.padded = true
49 | @main_vbox.append(top_hbox, false)
50 |
51 | # File path entry
52 | top_hbox.append(@file_path_entry, true)
53 |
54 | # File selection button
55 | top_hbox.append(@select_button, false)
56 |
57 | # Run button
58 | top_hbox.append(@run_button, false)
59 |
60 | # Create table
61 | @table = create_table
62 | @main_vbox.append(@table, true)
63 | end
64 |
65 | # Run the application
66 | def run
67 | @main_window.show
68 | UIng.main
69 | UIng.uninit
70 | end
71 |
72 | # Create and configure table
73 | private def create_table
74 | UIng::Table.new(@table_model) do
75 | append_text_column("Filename", 0, -1)
76 | append_text_column("Status", 1, -1)
77 | append_text_column("Message", 2, -1)
78 | header_visible = true
79 | selection_mode = UIng::Table::Selection::Mode::ZeroOrMany
80 | end
81 | end
82 |
83 | # Handle file selection button click
84 | private def handle_file_selection
85 | path = @main_window.open_file
86 | if path
87 | @file_path_entry.text = path
88 | end
89 | end
90 |
91 | # Handle run button click
92 | private def handle_run_button
93 | path = @file_path_entry.text
94 | if path && !path.empty?
95 | process_md5_file(path)
96 | else
97 | @main_window.msg_box_error("Error", "No file path specified")
98 | end
99 | end
100 |
101 | # Process MD5 file and update table
102 | private def process_md5_file(path)
103 | # Record old row count
104 | old_row_count = MD5Checker.instance.result_count
105 |
106 | # Process md5.txt file
107 | MD5Checker.instance.process_md5_file(path)
108 |
109 | # Update table
110 | update_table(old_row_count, MD5Checker.instance.result_count)
111 |
112 | # Show completion dialog
113 | @main_window.msg_box("Process Complete", "MD5 check completed")
114 | end
115 |
116 | # Update table with new data
117 | private def update_table(old_row_count, new_row_count)
118 | # Delete all old rows
119 | old_row_count.times do
120 | @table_model.row_deleted(0) # Always delete the first row
121 | end
122 |
123 | # Insert all new rows
124 | new_row_count.times do |i|
125 | @table_model.row_inserted(i)
126 | end
127 | end
128 |
129 | # Cleanup resources
130 | private def cleanup
131 | # Table cleanup. See: https://github.com/kojix2/uing/issues/6
132 | @main_vbox.delete(1)
133 | @table.destroy
134 | @table_model.free
135 | end
136 | end
137 |
--------------------------------------------------------------------------------
/examples/md5_checker/src/checker.cr:
--------------------------------------------------------------------------------
1 | require "digest/md5"
2 |
3 | # File verification result
4 | struct FileResult
5 | property filename : String
6 | property status : String
7 | property message : String
8 |
9 | def initialize(@filename : String, @status : String, @message : String)
10 | end
11 |
12 | # Convert status to emoji
13 | def status_emoji
14 | case @status
15 | when "OK" then "✅"
16 | when "FAIL" then "❌"
17 | when "ERROR" then "⚠️"
18 | else "❓"
19 | end
20 | end
21 |
22 | # Get display status with emoji
23 | def display_status
24 | "#{status_emoji} #{@status}"
25 | end
26 | end
27 |
28 | # MD5 checker application
29 | class MD5Checker
30 | # Singleton pattern
31 | @@instance = new
32 |
33 | def self.instance
34 | @@instance
35 | end
36 |
37 | private def initialize
38 | @results = [] of FileResult
39 | end
40 |
41 | # Results storage
42 | getter results : Array(FileResult)
43 |
44 | # Clear results
45 | def clear_results
46 | @results.clear
47 | end
48 |
49 | # Get result count
50 | def result_count
51 | @results.size
52 | end
53 |
54 | # Get result at index for table display
55 | def result_at(index, column)
56 | return "" if index >= @results.size
57 |
58 | result = @results[index]
59 | case column
60 | when 0 then result.filename
61 | when 1 then result.display_status
62 | when 2 then result.message
63 | else ""
64 | end
65 | end
66 |
67 | # Calculate MD5 hash for a file
68 | private def calculate_md5(file_path : String) : String
69 | Digest::MD5.hexdigest(File.read(file_path))
70 | end
71 |
72 | # Process MD5 checksum file
73 | def process_md5_file(md5_file_path : String) : Nil
74 | @results.clear
75 |
76 | # Check if file exists
77 | unless File.exists?(md5_file_path)
78 | @results << FileResult.new("File not found", "ERROR", md5_file_path)
79 | return
80 | end
81 |
82 | begin
83 | # Process each line in the file
84 | line_number = 0
85 | File.each_line(md5_file_path) do |line|
86 | line_number += 1
87 | begin
88 | process_line(line, line_number, md5_file_path)
89 | rescue ex : Exception
90 | @results << FileResult.new("Line #{line_number}", "ERROR", "Parse error: #{ex.message}")
91 | end
92 | end
93 | rescue ex : Exception
94 | @results << FileResult.new("File error", "ERROR", "Failed to read file: #{ex.message}")
95 | end
96 | end
97 |
98 | # Process a single line from the MD5 file
99 | private def process_line(line, line_number, md5_file_path)
100 | # Skip empty lines
101 | return if line.strip.empty?
102 |
103 | # Parse line (format: "MD5hash filename")
104 | parts = line.strip.split(' ', 2)
105 | if parts.size < 2
106 | @results << FileResult.new("Line #{line_number}", "ERROR", "Invalid format")
107 | return
108 | end
109 |
110 | expected_hash = parts[0].downcase
111 | # Validate MD5 hash format (32 hex characters)
112 | unless expected_hash =~ /^[0-9a-f]{32}$/
113 | @results << FileResult.new("Line #{line_number}", "ERROR", "Invalid MD5 hash format")
114 | return
115 | end
116 |
117 | filename = parts[1].strip
118 | file_path = File.join(File.dirname(md5_file_path), filename)
119 |
120 | # Check if file exists
121 | unless File.exists?(file_path)
122 | @results << FileResult.new(filename, "ERROR", "File not found")
123 | return
124 | end
125 |
126 | # Calculate and compare MD5 hash
127 | begin
128 | actual_hash = calculate_md5(file_path)
129 | if actual_hash.downcase == expected_hash
130 | @results << FileResult.new(filename, "OK", "Verification successful")
131 | else
132 | @results << FileResult.new(filename, "FAIL", "Checksum mismatch")
133 | end
134 | rescue ex : Exception
135 | @results << FileResult.new(filename, "ERROR", "Failed to calculate MD5: #{ex.message}")
136 | end
137 | end
138 | end
139 |
--------------------------------------------------------------------------------
/src/uing/area/area/attribute.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | class Area < Control
3 | class Attribute
4 | include BlockConstructor; block_constructor
5 |
6 | @released : Bool = false
7 |
8 | def initialize(@ref_ptr : Pointer(LibUI::Attribute))
9 | end
10 |
11 | def self.new_family(family : String) : Attribute
12 | ref_ptr = LibUI.new_family_attribute(family)
13 | Attribute.new(ref_ptr)
14 | end
15 |
16 | def self.new_size(size : Float64) : Attribute
17 | ref_ptr = LibUI.new_size_attribute(size)
18 | Attribute.new(ref_ptr)
19 | end
20 |
21 | def self.new_weight(weight : TextWeight) : Attribute
22 | ref_ptr = LibUI.new_weight_attribute(weight)
23 | Attribute.new(ref_ptr)
24 | end
25 |
26 | def self.new_italic(italic : TextItalic) : Attribute
27 | ref_ptr = LibUI.new_italic_attribute(italic)
28 | Attribute.new(ref_ptr)
29 | end
30 |
31 | def self.new_stretch(stretch : TextStretch) : Attribute
32 | ref_ptr = LibUI.new_stretch_attribute(stretch)
33 | Attribute.new(ref_ptr)
34 | end
35 |
36 | def self.new_color(r : Float64, g : Float64, b : Float64, a : Float64) : Attribute
37 | ref_ptr = LibUI.new_color_attribute(r, g, b, a)
38 | Attribute.new(ref_ptr)
39 | end
40 |
41 | def self.new_background(r : Float64, g : Float64, b : Float64, a : Float64) : Attribute
42 | ref_ptr = LibUI.new_background_attribute(r, g, b, a)
43 | Attribute.new(ref_ptr)
44 | end
45 |
46 | def self.new_underline(underline : Underline) : Attribute
47 | ref_ptr = LibUI.new_underline_attribute(underline)
48 | Attribute.new(ref_ptr)
49 | end
50 |
51 | def self.new_underline_color(underline_color : UnderlineColor, r : Float64, g : Float64, b : Float64, a : Float64) : Attribute
52 | ref_ptr = LibUI.new_underline_color_attribute(underline_color, r, g, b, a)
53 | Attribute.new(ref_ptr)
54 | end
55 |
56 | def self.new_features(open_type_features : OpenTypeFeatures) : Attribute
57 | ref_ptr = LibUI.new_features_attribute(open_type_features.to_unsafe)
58 | Attribute.new(ref_ptr)
59 | end
60 |
61 | def free : Nil
62 | return if @released
63 | LibUI.free_attribute(@ref_ptr)
64 | @released = true
65 | end
66 |
67 | def type : AttributeType
68 | LibUI.attribute_get_type(@ref_ptr)
69 | end
70 |
71 | def family : String?
72 | str_ptr = LibUI.attribute_family(@ref_ptr)
73 | # The returned string is owned by the attribute
74 | # and should not be freed (probably)
75 | str_ptr.null? ? nil : String.new(str_ptr)
76 | end
77 |
78 | def size : Float64
79 | LibUI.attribute_size(@ref_ptr)
80 | end
81 |
82 | def weight : TextWeight
83 | LibUI.attribute_weight(@ref_ptr)
84 | end
85 |
86 | def italic : TextItalic
87 | LibUI.attribute_italic(@ref_ptr)
88 | end
89 |
90 | def stretch : TextStretch
91 | LibUI.attribute_stretch(@ref_ptr)
92 | end
93 |
94 | def color : {Float64, Float64, Float64, Float64}
95 | LibUI.attribute_color(@ref_ptr, out r, out g, out b, out a)
96 | {r, g, b, a}
97 | end
98 |
99 | def underline : Underline
100 | LibUI.attribute_underline(@ref_ptr)
101 | end
102 |
103 | def underline_color : {UnderlineColor, Float64, Float64, Float64, Float64}
104 | underline_color = LibUI::UnderlineColor.new
105 | LibUI.attribute_underline_color(@ref_ptr, pointerof(underline_color), out r, out g, out b, out a)
106 | {underline_color, r, g, b, a}
107 | end
108 |
109 | def features : OpenTypeFeatures
110 | ref_ptr = LibUI.attribute_features(@ref_ptr)
111 | OpenTypeFeatures.new(ref_ptr)
112 | end
113 |
114 | def to_unsafe
115 | @ref_ptr
116 | end
117 |
118 | def finalize
119 | free
120 | end
121 | end
122 | end
123 | end
124 |
--------------------------------------------------------------------------------
/src/uing/area/area/draw/path.cr:
--------------------------------------------------------------------------------
1 | module UIng
2 | class Area < Control
3 | module Draw
4 | class Path
5 | include BlockConstructor; block_constructor
6 |
7 | @ended : Bool = false
8 | @released : Bool = false
9 | @ref_ptr : Pointer(LibUI::DrawPath)
10 |
11 | def initialize(@ref_ptr : Pointer(LibUI::DrawPath))
12 | end
13 |
14 | def initialize(mode : FillMode)
15 | @ref_ptr = LibUI.draw_new_path(mode)
16 | end
17 |
18 | # Creates a new Path and yields it to the block.
19 | # - The block must explicitly call end_path before using this path in Context APIs.
20 | # - The path is always freed after the block, even if an exception occurs.
21 | # - Returns the block's return value (NOT the Path instance).
22 | def self.open(mode : FillMode, &)
23 | instance = new(mode)
24 | begin
25 | result = yield instance
26 | ensure
27 | instance.free
28 | end
29 | result
30 | end
31 |
32 | private def ensure_not_released
33 | raise RuntimeError.new("Path is already freed") if @released
34 | end
35 |
36 | private def ensure_not_ended
37 | ensure_not_released
38 | raise RuntimeError.new("Path is already ended") if @ended
39 | end
40 |
41 | def new_figure(x : Float64, y : Float64) : self
42 | ensure_not_ended
43 | LibUI.draw_path_new_figure(@ref_ptr, x, y)
44 | self
45 | end
46 |
47 | def new_figure_with_arc(x_center : Float64, y_center : Float64, radius : Float64, start_angle : Float64, sweep : Float64, negative : Bool) : self
48 | ensure_not_ended
49 | LibUI.draw_path_new_figure_with_arc(@ref_ptr, x_center, y_center, radius, start_angle, sweep, negative)
50 | self
51 | end
52 |
53 | def line_to(x : Float64, y : Float64) : self
54 | ensure_not_ended
55 | LibUI.draw_path_line_to(@ref_ptr, x, y)
56 | self
57 | end
58 |
59 | def arc_to(x_center : Float64, y_center : Float64, radius : Float64, start_angle : Float64, sweep : Float64, negative : Bool) : self
60 | ensure_not_ended
61 | LibUI.draw_path_arc_to(@ref_ptr, x_center, y_center, radius, start_angle, sweep, negative)
62 | self
63 | end
64 |
65 | def bezier_to(c1x : Float64, c1y : Float64, c2x : Float64, c2y : Float64, end_x : Float64, end_y : Float64) : self
66 | ensure_not_ended
67 | LibUI.draw_path_bezier_to(@ref_ptr, c1x, c1y, c2x, c2y, end_x, end_y)
68 | self
69 | end
70 |
71 | def close_figure : self
72 | ensure_not_ended
73 | LibUI.draw_path_close_figure(@ref_ptr)
74 | self
75 | end
76 |
77 | def add_rectangle(x : Float64, y : Float64, width : Float64, height : Float64) : self
78 | ensure_not_ended
79 | LibUI.draw_path_add_rectangle(@ref_ptr, x, y, width, height)
80 | self
81 | end
82 |
83 | def ended? : Bool
84 | @ended
85 | end
86 |
87 | def released? : Bool
88 | @released
89 | end
90 |
91 | def end_path : Nil
92 | ensure_not_released
93 | return if @ended # Idempotent
94 | LibUI.draw_path_end(@ref_ptr)
95 | @ended = true
96 | end
97 |
98 | def free : Nil
99 | return if @released # Idempotent
100 | # Ensure path is ended before freeing (safe even if already ended)
101 | unless @ended
102 | LibUI.draw_path_end(@ref_ptr)
103 | @ended = true
104 | end
105 | LibUI.draw_free_path(@ref_ptr)
106 | @released = true
107 | # Help catch misuse after free
108 | @ref_ptr = Pointer(LibUI::DrawPath).null
109 | end
110 |
111 | def to_unsafe
112 | ensure_not_released
113 | @ref_ptr
114 | end
115 | end
116 | end
117 | end
118 | end
119 |
--------------------------------------------------------------------------------
/examples/md5_checker/build-mac.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | # Load configuration from .env
5 | set -a
6 | source .env
7 | set +a
8 |
9 | EXECUTABLE_PATH="bin/$APP_NAME"
10 | APP_BUNDLE="$APP_NAME_CAPITALIZED.app"
11 | MACOS_DIR="$APP_BUNDLE/Contents/MacOS"
12 | RESOURCES_DIR="$APP_BUNDLE/Contents/Resources"
13 | FRAMEWORKS_DIR="$APP_BUNDLE/Contents/Frameworks"
14 | PLIST_PATH="$APP_BUNDLE/Contents/Info.plist"
15 | ICON_NAME="app_icon"
16 | ICON_PATH="resources/$ICON_NAME.icns"
17 | DMG_NAME="${APP_NAME}.dmg"
18 | VOL_NAME="$APP_NAME_CAPITALIZED"
19 | STAGING_DIR="dmg_stage"
20 | DIST_DIR="dist"
21 |
22 | echo "Building $APP_NAME v$VERSION..."
23 | shards install
24 | shards build --release
25 |
26 | rm -rf "$APP_BUNDLE" "$DMG_NAME" "$STAGING_DIR" "$DIST_DIR"
27 | mkdir -p "$MACOS_DIR" "$RESOURCES_DIR" "$FRAMEWORKS_DIR" "$DIST_DIR"
28 |
29 | # Create .app bundle
30 | cp "$EXECUTABLE_PATH" "$MACOS_DIR/$APP_NAME"
31 | chmod +x "$MACOS_DIR/$APP_NAME"
32 |
33 | # Generate .icns from .png
34 | if [ -f "resources/$ICON_NAME.png" ]; then
35 | echo "Generating $ICON_NAME.icns from $ICON_NAME.png..."
36 | ICONSET_DIR="resources/$ICON_NAME.iconset"
37 | mkdir -p "$ICONSET_DIR"
38 |
39 | # Generate different sizes using sips
40 | sips -z 16 16 "resources/$ICON_NAME.png" --out "$ICONSET_DIR/icon_16x16.png"
41 | sips -z 32 32 "resources/$ICON_NAME.png" --out "$ICONSET_DIR/icon_16x16@2x.png"
42 | sips -z 32 32 "resources/$ICON_NAME.png" --out "$ICONSET_DIR/icon_32x32.png"
43 | sips -z 64 64 "resources/$ICON_NAME.png" --out "$ICONSET_DIR/icon_32x32@2x.png"
44 | sips -z 128 128 "resources/$ICON_NAME.png" --out "$ICONSET_DIR/icon_128x128.png"
45 | sips -z 256 256 "resources/$ICON_NAME.png" --out "$ICONSET_DIR/icon_128x128@2x.png"
46 | sips -z 256 256 "resources/$ICON_NAME.png" --out "$ICONSET_DIR/icon_256x256.png"
47 | sips -z 512 512 "resources/$ICON_NAME.png" --out "$ICONSET_DIR/icon_256x256@2x.png"
48 | sips -z 512 512 "resources/$ICON_NAME.png" --out "$ICONSET_DIR/icon_512x512.png"
49 | sips -z 1024 1024 "resources/$ICON_NAME.png" --out "$ICONSET_DIR/icon_512x512@2x.png"
50 |
51 | # Generate .icns file
52 | iconutil -c icns "$ICONSET_DIR" -o "$ICON_PATH"
53 |
54 | # Clean up iconset directory
55 | rm -rf "$ICONSET_DIR"
56 |
57 | echo "Generated $ICON_PATH"
58 | fi
59 |
60 | if [ -f "$ICON_PATH" ]; then
61 | cp "$ICON_PATH" "$RESOURCES_DIR/$ICON_NAME.icns"
62 | fi
63 |
64 | # Create Info.plist
65 | cat > "$PLIST_PATH" <
67 |
69 |
70 |
71 | CFBundleExecutable
72 | $APP_NAME
73 | CFBundleIdentifier
74 | com.example.$APP_NAME
75 | CFBundleName
76 | $APP_NAME
77 | CFBundleVersion
78 | $VERSION
79 | CFBundlePackageType
80 | APPL
81 | CFBundleIconFile
82 | $ICON_NAME
83 |
84 |
85 | EOF
86 |
87 | # Bundle Homebrew libraries
88 | otool -L "$MACOS_DIR/$APP_NAME" \
89 | | awk '{print $1}' \
90 | | grep "^/opt/homebrew" \
91 | | while read -r lib; do
92 | base=$(basename "$lib")
93 | cp "$lib" "$FRAMEWORKS_DIR/$base"
94 | echo "Before install_name_tool:"
95 | otool -L "$MACOS_DIR/$APP_NAME"
96 |
97 | install_name_tool -change "$lib" "@executable_path/../Frameworks/$base" "$MACOS_DIR/$APP_NAME"
98 |
99 | echo "After install_name_tool:"
100 | otool -L "$MACOS_DIR/$APP_NAME"
101 | done
102 |
103 | # Create DMG
104 | mkdir -p "$STAGING_DIR"
105 | cp -R "$APP_BUNDLE" "$STAGING_DIR/"
106 | ln -s /Applications "$STAGING_DIR/Applications"
107 |
108 | hdiutil create "$DMG_NAME" \
109 | -volname "$VOL_NAME" \
110 | -srcfolder "$STAGING_DIR" \
111 | -fs HFS+ \
112 | -format UDZO \
113 | -imagekey zlib-level=9 \
114 | -quiet
115 |
116 | rm -rf "$STAGING_DIR"
117 |
118 | mv "$DMG_NAME" "$DIST_DIR/"
119 | mv "$APP_BUNDLE" "$DIST_DIR/"
120 |
121 | echo "Created: dist/$DMG_NAME"
122 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: test
2 |
3 | on:
4 | push:
5 | pull_request:
6 | schedule:
7 | - cron: "51 3 * * 6" # Runs at 03:51, only on Saturday
8 | workflow_dispatch:
9 | jobs:
10 | build:
11 | name: ${{ matrix.os }}
12 | runs-on: ${{ matrix.os }}
13 | strategy:
14 | fail-fast: false
15 | matrix:
16 | os: ["ubuntu-latest", "macos-latest", "windows-latest", "macos-13"]
17 | steps:
18 | - uses: actions/checkout@v6
19 | with:
20 | submodules: true
21 | - uses: crystal-lang/install-crystal@v1
22 | with:
23 | crystal: latest
24 | # Initialize MSVC Developer Command Prompt to ensure LINK/CL/SDK env vars are set
25 | - if: ${{ matrix.os == 'windows-latest' }}
26 | name: Setup MSVC Developer Command Prompt
27 | uses: ilammy/msvc-dev-cmd@v1
28 | with:
29 | arch: x64
30 | - if: ${{ matrix.os == 'ubuntu-latest' }}
31 | run: |
32 | sudo apt-get update -y
33 | sudo apt-get install -y libgtk-3-dev
34 | - name: Install dependencies
35 | run: shards install
36 | - name: Download libui library
37 | run: |
38 | crystal run download.cr
39 | ls libui
40 | - name: Run tests
41 | run: crystal spec
42 | - name: Build all examples (Ubuntu)
43 | if: ${{ matrix.os == 'ubuntu-latest' }}
44 | run: |
45 | ls examples/*.cr | xargs -P 0 -I {} crystal build {}
46 | ls
47 | - name: Build all examples (macOS)
48 | if: ${{ matrix.os == 'macos-latest' || matrix.os == 'macos-13' }}
49 | run: |
50 | ls examples/*.cr | xargs -n 1 crystal build
51 | ls
52 | - name: Build all examples (Windows)
53 | if: ${{ matrix.os == 'windows-latest' }}
54 | run: |
55 | Get-ChildItem -Path examples -Filter *.cr | ForEach-Object { crystal build $_.FullName }
56 | ls
57 |
58 | build_Aarch64:
59 | name: Aarch64
60 | runs-on: ${{ matrix.os }}
61 | strategy:
62 | fail-fast: false
63 | matrix:
64 | os: ["ubuntu-22.04-arm", "ubuntu-24.04-arm"]
65 | steps:
66 | - uses: actions/checkout@v6
67 | with:
68 | submodules: true
69 | - name: Install crystal
70 | run: |
71 | curl -fsSL https://packagecloud.io/84codes/crystal/gpgkey | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/84codes_crystal.gpg > /dev/null
72 | . /etc/os-release
73 | echo "deb https://packagecloud.io/84codes/crystal/$ID $VERSION_CODENAME main" | sudo tee /etc/apt/sources.list.d/84codes_crystal.list
74 | sudo apt-get update -y
75 | sudo apt-get install -y crystal
76 | - name: Install system dependencies
77 | run: |
78 | sudo apt-get install -y libgtk-3-dev
79 | - name: Install Crystal dependencies
80 | run: shards install
81 | - name: Download libui library
82 | run: |
83 | crystal run download.cr
84 | ls libui
85 | - name: Run tests
86 | run: crystal spec
87 | - name: Build all examples (Ubuntu)
88 | run: |
89 | ls examples/*.cr | xargs -P 0 -I {} crystal build {}
90 | ls
91 |
92 | build_MinGW64:
93 | name: MinGW64
94 | runs-on: windows-latest
95 | steps:
96 | - uses: actions/checkout@v6
97 | with:
98 | submodules: true
99 | - uses: msys2/setup-msys2@v2
100 | with:
101 | msystem: MINGW64
102 | update: true
103 | install: >-
104 | mingw-w64-x86_64-crystal
105 | mingw-w64-x86_64-shards
106 | git
107 | - name: Install dependencies
108 | shell: msys2 {0}
109 | run: shards install
110 | - name: Download libui library
111 | shell: msys2 {0}
112 | run: |
113 | crystal run download.cr
114 | ls libui
115 | - name: Run tests
116 | shell: msys2 {0}
117 | run: crystal spec
118 | - name: Build all examples
119 | shell: msys2 {0}
120 | run: |
121 | for f in examples/*.cr; do crystal build "$f"; done
122 | ls -l
123 |
--------------------------------------------------------------------------------
/.github/actions/screenshot/ubuntu-screenshot.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | APP_NAME="$1"
5 | OUTPUT_FILE="$2"
6 |
7 | echo "Starting Ubuntu window screenshot process for $APP_NAME"
8 |
9 | # Start Xvfb with proper screen resolution
10 | Xvfb :99 -screen 0 1280x800x24 -ac +extension GLX +render -noreset &
11 | echo $! > xvfb.pid
12 | sleep 3
13 |
14 | # Ensure all X clients use the same display
15 | export DISPLAY=:99
16 | echo "Using DISPLAY=$DISPLAY"
17 |
18 | # Start window manager (avoid --config-file /dev/null to suppress syntax error dialog)
19 | openbox &
20 | echo $! > wm.pid
21 | sleep 2
22 |
23 | # Launch the application in background
24 | "./$APP_NAME" &
25 | echo $! > app.pid
26 |
27 | # Short initial wait; loop below will wait for the window anyway
28 | sleep 1
29 |
30 | # Try to find the application window by PID
31 | WINDOW_ID=""
32 | ATTEMPTS=0
33 | MAX_ATTEMPTS=10
34 | APP_PID=$(cat app.pid)
35 |
36 | echo "Searching for window belonging to PID: $APP_PID"
37 |
38 | # Helper: try to find a window by matching PID via wmctrl -lp
39 | find_window_by_pid() {
40 | local pids="$1"
41 | wmctrl -lp 2>/dev/null | awk -v pids="$pids" '
42 | BEGIN {
43 | n = split(pids, a, /[ \t]+/);
44 | for (i = 1; i <= n; i++) if (a[i] != "") wanted[a[i]] = 1;
45 | }
46 | $3 in wanted { print $1; exit }
47 | ' | head -n1
48 | }
49 |
50 | # Helper: collect immediate child PIDs (in case the window is owned by a child process)
51 | collect_child_pids() {
52 | local root="$1"
53 | local list="$root"
54 | local kids
55 | kids="$(pgrep -P "$root" 2>/dev/null || true)"
56 | if [ -n "$kids" ]; then
57 | list="$list $kids"
58 | fi
59 | echo "$list"
60 | }
61 |
62 | while [ -z "$WINDOW_ID" ] && [ $ATTEMPTS -lt $MAX_ATTEMPTS ]; do
63 | CANDIDATE_PIDS="$(collect_child_pids "$APP_PID")"
64 |
65 | # 1) Fast path: use wmctrl -lp to match PID column
66 | WINDOW_ID="$(find_window_by_pid "$CANDIDATE_PIDS" || true)"
67 |
68 | # 2) Fallback: scan windows and read _NET_WM_PID via xprop
69 | if [ -z "$WINDOW_ID" ]; then
70 | for wid in $(wmctrl -l 2>/dev/null | awk '{print $1}'); do
71 | WIN_PID="$(xprop -id "$wid" _NET_WM_PID 2>/dev/null | awk '{print $3}' || true)"
72 | for p in $CANDIDATE_PIDS; do
73 | if [ -n "$WIN_PID" ] && [ "$WIN_PID" = "$p" ]; then
74 | WINDOW_ID="$wid"
75 | break
76 | fi
77 | done
78 | [ -n "$WINDOW_ID" ] && break
79 | done
80 | fi
81 |
82 | if [ -z "$WINDOW_ID" ]; then
83 | echo "Attempt $((ATTEMPTS + 1))/$MAX_ATTEMPTS: Window not found yet, waiting..."
84 | sleep 1
85 | ATTEMPTS=$((ATTEMPTS + 1))
86 | fi
87 | done
88 |
89 | if [ -n "$WINDOW_ID" ]; then
90 | echo "Found window with ID: $WINDOW_ID"
91 | WINDOW_TITLE="$(xprop -id "$WINDOW_ID" WM_NAME 2>/dev/null | cut -d'"' -f2 || echo "Unknown")"
92 | echo "Window title: $WINDOW_TITLE"
93 |
94 | # Focus the window
95 | wmctrl -i -a "$WINDOW_ID" || true
96 | sleep 1
97 |
98 | # Take screenshot incl. window decorations (title bar)
99 | import -frame -window "$WINDOW_ID" "$OUTPUT_FILE"
100 |
101 | echo "Window screenshot captured successfully"
102 | else
103 | echo "Warning: Could not find application window, falling back to root window screenshot"
104 |
105 | # Fallback: Take screenshot of the entire screen
106 | import -window root "$OUTPUT_FILE"
107 |
108 | echo "Root window screenshot captured as fallback"
109 | fi
110 |
111 | # Verify screenshot was created
112 | if [ -f "$OUTPUT_FILE" ]; then
113 | echo "Screenshot created successfully: $OUTPUT_FILE"
114 | file "$OUTPUT_FILE"
115 | identify "$OUTPUT_FILE" || true
116 | else
117 | echo "Failed to create screenshot"
118 | exit 1
119 | fi
120 |
121 | # Cleanup function
122 | cleanup() {
123 | echo "Cleaning up processes..."
124 | if [ -f app.pid ]; then
125 | kill $(cat app.pid) 2>/dev/null || true
126 | rm -f app.pid
127 | fi
128 | if [ -f wm.pid ]; then
129 | kill $(cat wm.pid) 2>/dev/null || true
130 | rm -f wm.pid
131 | fi
132 | if [ -f xvfb.pid ]; then
133 | kill $(cat xvfb.pid) 2>/dev/null || true
134 | rm -f xvfb.pid
135 | fi
136 | }
137 |
138 | # Set trap to cleanup on exit
139 | trap cleanup EXIT
140 |
141 | echo "Ubuntu window screenshot process completed successfully"
142 |
--------------------------------------------------------------------------------
/examples/gallery/area_analog_clock.cr:
--------------------------------------------------------------------------------
1 | require "../../src/uing"
2 |
3 | class ClockDrawer
4 | getter ctx : UIng::Area::Draw::Context
5 | getter center_x : Float64
6 | getter center_y : Float64
7 | getter radius : Float64
8 |
9 | @black_brush : UIng::Area::Draw::Brush
10 | @red_brush : UIng::Area::Draw::Brush
11 | @stroke_thin : UIng::Area::Draw::StrokeParams
12 | @stroke_mid : UIng::Area::Draw::StrokeParams
13 | @stroke_thick : UIng::Area::Draw::StrokeParams
14 |
15 | def initialize(@ctx : UIng::Area::Draw::Context, @center_x : Float64, @center_y : Float64, radius : Float64)
16 | @radius = radius.clamp(10.0, Float64::MAX)
17 | # brushes
18 | @black_brush = UIng::Area::Draw::Brush.new(:solid, 0.0, 0.0, 0.0, 1.0)
19 | @red_brush = UIng::Area::Draw::Brush.new(:solid, 0.8, 0.0, 0.0, 1.0)
20 | # stroke params (reused)
21 | @stroke_thin = stroke_params(1.0)
22 | @stroke_mid = stroke_params(3.0)
23 | @stroke_thick = stroke_params(6.0)
24 | end
25 |
26 | # Create common stroke parameters
27 | private def stroke_params(thickness : Float64)
28 | UIng::Area::Draw::StrokeParams.new(
29 | cap: :round, join: :round,
30 | thickness: thickness, miter_limit: 10.0
31 | )
32 | end
33 |
34 | # Convert polar coordinates to cartesian
35 | private def polar_to_cartesian(radius : Float64, angle : Float64)
36 | {center_x + radius * Math.cos(angle), center_y + radius * Math.sin(angle)}
37 | end
38 |
39 | # Draw clock face outline
40 | def draw_clock_face
41 | ctx.stroke_path(@black_brush, @stroke_mid) do |path|
42 | path.new_figure_with_arc(center_x, center_y, radius, 0, Math::PI * 2, false)
43 | end
44 | end
45 |
46 | # Draw 12-hour markers
47 | def draw_hour_markers
48 | 12.times do |i|
49 | angle = i * Math::PI / 6 - Math::PI / 2
50 | inner_x, inner_y = polar_to_cartesian(radius - 15.0, angle)
51 | outer_x, outer_y = polar_to_cartesian(radius - 5.0, angle)
52 | ctx.stroke_path(@black_brush, @stroke_mid) do |path|
53 | path.new_figure(inner_x, inner_y)
54 | path.line_to(outer_x, outer_y)
55 | end
56 | end
57 | end
58 |
59 | # Draw a clock hand
60 | def draw_hand(angle : Float64, length : Float64, stroke : UIng::Area::Draw::StrokeParams, hand_brush : UIng::Area::Draw::Brush)
61 | end_x, end_y = polar_to_cartesian(length, angle)
62 | ctx.stroke_path(hand_brush, stroke) do |path|
63 | path.new_figure(center_x, center_y)
64 | path.line_to(end_x, end_y)
65 | end
66 | end
67 |
68 | # Draw center dot (fill + tiny outline)
69 | def draw_center_dot
70 | ctx.fill_path(@black_brush) do |path|
71 | path.new_figure_with_arc(center_x, center_y, 4.5, 0, Math::PI * 2, false)
72 | end
73 | ctx.stroke_path(@black_brush, @stroke_thin) do |path|
74 | path.new_figure_with_arc(center_x, center_y, 4.5, 0, Math::PI * 2, false)
75 | end
76 | end
77 |
78 | # Draw all hands for a given time
79 | def draw_hands(now : Time)
80 | # High-resolution sweep (continuous rotation)
81 | sec = now.second + now.nanosecond / 1_000_000_000.0
82 | min = now.minute + sec / 60.0
83 | hour = (now.hour % 12) + min / 60.0
84 |
85 | # Offset by -π/2 to make 12 o'clock point up
86 | hour_angle = hour * Math::PI / 6 - Math::PI / 2
87 | minute_angle = min * Math::PI / 30 - Math::PI / 2
88 | second_angle = sec * Math::PI / 30 - Math::PI / 2
89 |
90 | draw_hand(hour_angle, radius * 0.52, @stroke_thick, @black_brush)
91 | draw_hand(minute_angle, radius * 0.76, @stroke_mid, @black_brush)
92 | draw_hand(second_angle, radius * 0.85, @stroke_thin, @red_brush)
93 | end
94 |
95 | # Draw complete clock
96 | def draw_clock(now : Time = Time.local)
97 | draw_clock_face
98 | draw_hour_markers
99 | draw_hands(now)
100 | draw_center_dot
101 | end
102 | end
103 |
104 | UIng.init
105 |
106 | handler = UIng::Area::Handler.new do
107 | draw { |area, params|
108 | center_x, center_y = params.area_width / 2.0, params.area_height / 2.0
109 | # Ensure 20px margin while clamping radius to minimum 10
110 | radius = Math.min(params.area_width, params.area_height) / 2.0 - 20.0
111 | radius = radius.clamp(10.0, Float64::MAX)
112 |
113 | clock_drawer = ClockDrawer.new(params.context, center_x, center_y, radius)
114 | clock_drawer.draw_clock(Time.local)
115 | }
116 | end
117 |
118 | window = UIng::Window.new("Simple Analog Clock", 300, 300)
119 | window.on_closing { UIng.quit; true }
120 |
121 | area = UIng::Area.new(handler)
122 | window.child = UIng::Box.new(:vertical, padded: true).tap(&.append(area, stretchy: true))
123 |
124 | window.show
125 |
126 | # Higher FPS for smooth sweep second hand (reduce to 33ms if CPU load is a concern)
127 | UIng.timer(16) { area.queue_redraw_all; 1 }
128 |
129 | UIng.main
130 | UIng.uninit
131 |
--------------------------------------------------------------------------------
/examples/md5_checker/build-win.ps1:
--------------------------------------------------------------------------------
1 | # PowerShell script for building Windows installer
2 |
3 | $ErrorActionPreference = "Stop"
4 |
5 | # Load configuration from .env
6 | Get-Content .env | ForEach-Object {
7 | if ($_ -match '^\s*([^#][^=]*)=(.*)$') {
8 | $name = $matches[1].Trim()
9 | $value = $matches[2].Trim('"')
10 | Set-Variable -Name $name -Value $value -Scope Global
11 | }
12 | }
13 |
14 | $EXECUTABLE_PATH = "bin\$APP_NAME.exe"
15 | $DIST_DIR = "dist"
16 | $INSTALLER_NAME = "$APP_NAME-setup.exe"
17 | $ISS_FILE = "$APP_NAME.iss"
18 | $OUTPUT_DIR = "Output"
19 |
20 | # Use Inno Setup compiler from configuration
21 | $ISCC = $INNO_SETUP_PATH
22 | if (-not (Get-Command $ISCC -ErrorAction SilentlyContinue)) {
23 | Write-Error "Error: Inno Setup not found at '$ISCC'. Please set INNO_SETUP_PATH in .env file or environment variable."
24 | Write-Host "Example: INNO_SETUP_PATH=C:\Program Files (x86)\Inno Setup 6\ISCC.exe"
25 | exit 1
26 | }
27 |
28 | Write-Host "Building $APP_NAME v$VERSION..."
29 | & shards install
30 | & shards build --release --no-debug --static --link-flags=/SUBSYSTEM:WINDOWS
31 |
32 | if (Test-Path $DIST_DIR) { Remove-Item $DIST_DIR -Recurse -Force }
33 | if (Test-Path $OUTPUT_DIR) { Remove-Item $OUTPUT_DIR -Recurse -Force }
34 | New-Item -ItemType Directory -Path $DIST_DIR | Out-Null
35 | New-Item -ItemType Directory -Path $OUTPUT_DIR | Out-Null
36 |
37 | # Generate .ico from .png
38 | if (Test-Path "resources\app_icon.png") {
39 | Write-Host "Generating app_icon.ico from app_icon.png..."
40 |
41 | Add-Type -AssemblyName System.Drawing
42 | Add-Type -AssemblyName System.Windows.Forms
43 |
44 | try {
45 | # Load the PNG image
46 | $pngPath = (Resolve-Path "resources\app_icon.png").Path
47 | $icoPath = (Join-Path $PWD "resources\app_icon.ico")
48 |
49 | # Load the original image
50 | $originalImage = [System.Drawing.Image]::FromFile($pngPath)
51 |
52 | # Create a bitmap with 32x32 size (standard for ICO)
53 | $bitmap = New-Object System.Drawing.Bitmap(32, 32)
54 | $graphics = [System.Drawing.Graphics]::FromImage($bitmap)
55 | $graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
56 | $graphics.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighQuality
57 | $graphics.DrawImage($originalImage, 0, 0, 32, 32)
58 | $graphics.Dispose()
59 |
60 | # Create icon from bitmap
61 | $iconHandle = $bitmap.GetHicon()
62 | $icon = [System.Drawing.Icon]::FromHandle($iconHandle)
63 |
64 | # Save the icon to file
65 | $fileStream = [System.IO.FileStream]::new($icoPath, [System.IO.FileMode]::Create)
66 | $icon.Save($fileStream)
67 | $fileStream.Close()
68 |
69 | # Clean up resources
70 | $icon.Dispose()
71 | $bitmap.Dispose()
72 | $originalImage.Dispose()
73 |
74 | # Verify the file was created
75 | if (Test-Path $icoPath) {
76 | Write-Host "Successfully generated resources\app_icon.ico"
77 | }
78 | else {
79 | throw "ICO file was not created"
80 | }
81 | }
82 | catch {
83 | Write-Error "Failed to generate ICO from PNG: $_"
84 | Write-Host "Build cannot continue without icon file."
85 | exit 1
86 | }
87 | }
88 | else {
89 | Write-Error "PNG icon file not found at resources\app_icon.png"
90 | exit 1
91 | }
92 |
93 | # Create Inno Setup script using here-string
94 | $issContent = @"
95 | [Setup]
96 | AppName=$APP_NAME_CAPITALIZED
97 | AppVersion=$VERSION
98 | DefaultDirName={userpf}\$APP_NAME_CAPITALIZED
99 | DefaultGroupName=$APP_NAME_CAPITALIZED
100 | OutputDir=$OUTPUT_DIR
101 | OutputBaseFilename=$APP_NAME-setup
102 | Compression=lzma
103 | SolidCompression=yes
104 | PrivilegesRequired=lowest
105 | UninstallDisplayIcon={app}\app_icon.ico
106 |
107 | [Files]
108 | Source: "$EXECUTABLE_PATH"; DestDir: "{app}"; Flags: ignoreversion
109 | Source: "README.md"; DestDir: "{app}"; Flags: isreadme ignoreversion
110 | Source: "resources\app_icon.ico"; DestDir: "{app}"; Flags: ignoreversion
111 |
112 | [Icons]
113 | Name: "{group}\$APP_NAME_CAPITALIZED"; Filename: "{app}\$APP_NAME.exe"; IconFilename: "{app}\app_icon.ico"
114 | Name: "{group}\Uninstall $APP_NAME_CAPITALIZED"; Filename: "{uninstallexe}"
115 |
116 | [Run]
117 | Filename: "{app}\$APP_NAME.exe"; Description: "Launch $APP_NAME_CAPITALIZED"; Flags: nowait postinstall skipifsilent
118 | "@
119 |
120 | $issContent | Out-File -FilePath $ISS_FILE -Encoding UTF8
121 |
122 | & $ISCC $ISS_FILE
123 |
124 | Copy-Item $EXECUTABLE_PATH $DIST_DIR\ | Out-Null
125 | if (Test-Path "$OUTPUT_DIR\$INSTALLER_NAME") {
126 | Move-Item "$OUTPUT_DIR\$INSTALLER_NAME" $DIST_DIR\
127 | }
128 |
129 | Remove-Item $OUTPUT_DIR -Recurse -Force
130 | Remove-Item $ISS_FILE
131 |
132 | Write-Host "Created: dist\$INSTALLER_NAME"
133 |
--------------------------------------------------------------------------------
/src/uing/area/area/draw/brush.cr:
--------------------------------------------------------------------------------
1 | require "./brush/*"
2 |
3 | module UIng
4 | class Area < Control
5 | module Draw
6 | class Brush
7 | include BlockConstructor; block_constructor
8 |
9 | # FIX: Keep gradient stops buffer alive for C-side pointer lifetime management
10 | @stops_buffer : Array(LibUI::DrawBrushGradientStop) = [] of LibUI::DrawBrushGradientStop
11 |
12 | def initialize(type : Brush::Type,
13 | r : Number = 0.0,
14 | g : Number = 0.0,
15 | b : Number = 0.0,
16 | a : Number = 1.0,
17 | x0 : Number = 0.0,
18 | y0 : Number = 0.0,
19 | x1 : Number = 0.0,
20 | y1 : Number = 0.0,
21 | outer_radius : Number = 0.0,
22 | stops : Array(GradientStop)? = nil)
23 | @cstruct = LibUI::DrawBrush.new
24 | @cstruct.type = type
25 | @cstruct.r = r.to_f64
26 | @cstruct.g = g.to_f64
27 | @cstruct.b = b.to_f64
28 | @cstruct.a = a.to_f64
29 | @cstruct.x0 = x0.to_f64
30 | @cstruct.y0 = y0.to_f64
31 | @cstruct.x1 = x1.to_f64
32 | @cstruct.y1 = y1.to_f64
33 | @cstruct.outer_radius = outer_radius.to_f64
34 |
35 | if stops
36 | set_gradient_stops(stops)
37 | else
38 | @stops_buffer.clear
39 | @cstruct.stops = Pointer(LibUI::DrawBrushGradientStop).null
40 | @cstruct.num_stops = 0_u64
41 | end
42 | end
43 |
44 | private def set_gradient_stops(stops : Array(GradientStop))
45 | if stops.empty?
46 | @stops_buffer.clear
47 | @cstruct.stops = Pointer(LibUI::DrawBrushGradientStop).null
48 | @cstruct.num_stops = 0_u64
49 | else
50 | # FIX: Store C structs in instance variable to keep them alive
51 | # Crystal's Array.to_unsafe is guaranteed to be C-compatible
52 | @stops_buffer = Array(LibUI::DrawBrushGradientStop).new(stops.size)
53 | stops.each do |gs|
54 | @stops_buffer << gs.to_unsafe.value
55 | end
56 | @cstruct.stops = @stops_buffer.to_unsafe
57 | @cstruct.num_stops = @stops_buffer.size.to_u64
58 | end
59 | end
60 |
61 | def type : Type
62 | @cstruct.type
63 | end
64 |
65 | def type=(value : Type)
66 | @cstruct.type = value
67 | end
68 |
69 | def r : Float64
70 | @cstruct.r
71 | end
72 |
73 | def r=(value : Float64)
74 | @cstruct.r = value
75 | end
76 |
77 | def g : Float64
78 | @cstruct.g
79 | end
80 |
81 | def g=(value : Float64)
82 | @cstruct.g = value
83 | end
84 |
85 | def b : Float64
86 | @cstruct.b
87 | end
88 |
89 | def b=(value : Float64)
90 | @cstruct.b = value
91 | end
92 |
93 | def a : Float64
94 | @cstruct.a
95 | end
96 |
97 | def a=(value : Float64)
98 | @cstruct.a = value
99 | end
100 |
101 | def x0 : Float64
102 | @cstruct.x0
103 | end
104 |
105 | def x0=(value : Float64)
106 | @cstruct.x0 = value
107 | end
108 |
109 | def y0 : Float64
110 | @cstruct.y0
111 | end
112 |
113 | def y0=(value : Float64)
114 | @cstruct.y0 = value
115 | end
116 |
117 | def x1 : Float64
118 | @cstruct.x1
119 | end
120 |
121 | def x1=(value : Float64)
122 | @cstruct.x1 = value
123 | end
124 |
125 | def y1 : Float64
126 | @cstruct.y1
127 | end
128 |
129 | def y1=(value : Float64)
130 | @cstruct.y1 = value
131 | end
132 |
133 | def outer_radius : Float64
134 | @cstruct.outer_radius
135 | end
136 |
137 | def outer_radius=(value : Float64)
138 | @cstruct.outer_radius = value
139 | end
140 |
141 | def stops : Array(GradientStop)
142 | return Array(GradientStop).new if @cstruct.num_stops == 0 || @cstruct.stops.null?
143 |
144 | Array(GradientStop).new(@cstruct.num_stops.to_i) do |i|
145 | c_stop = (@cstruct.stops + i).value
146 | GradientStop.new(
147 | pos: c_stop.pos,
148 | r: c_stop.r,
149 | g: c_stop.g,
150 | b: c_stop.b,
151 | a: c_stop.a
152 | )
153 | end
154 | end
155 |
156 | def stops=(value : Array(GradientStop))
157 | set_gradient_stops(value)
158 | end
159 |
160 | def num_stops : LibC::SizeT
161 | @cstruct.num_stops
162 | end
163 |
164 | def to_unsafe
165 | pointerof(@cstruct)
166 | end
167 | end
168 | end
169 | end
170 | end
171 |
--------------------------------------------------------------------------------
/examples/notepad.cr:
--------------------------------------------------------------------------------
1 | require "../src/uing"
2 |
3 | class NotepadApp
4 | @current_file_path : String?
5 | @is_modified : Bool
6 | @main_window : UIng::Window
7 | @vbox : UIng::Box
8 | @entry : UIng::MultilineEntry
9 |
10 | def initialize
11 | @current_file_path = nil
12 | @is_modified = false
13 |
14 | UIng.init
15 |
16 | # Create menus and menu items BEFORE creating any windows
17 | file_menu = UIng::Menu.new("File")
18 | open_item = file_menu.append_item("Open")
19 | save_item = file_menu.append_item("Save")
20 | save_as_item = file_menu.append_item("Save As")
21 | file_menu.append_separator
22 | quit_item = file_menu.append_quit_item
23 |
24 | help_menu = UIng::Menu.new("Help")
25 | about_item = help_menu.append_about_item
26 |
27 | # Now create UI components
28 | @main_window = UIng::Window.new("Notepad", 500, 300, menubar: true)
29 | @vbox = UIng::Box.new(:vertical)
30 | @entry = UIng::MultilineEntry.new(wrapping: true)
31 |
32 | # Set up the window layout
33 | @main_window.child = @vbox
34 | @vbox.append @entry, true
35 |
36 | # Set up event handlers
37 | setup_event_handlers(open_item, save_item, save_as_item, quit_item, about_item)
38 |
39 | # Initialize title
40 | update_title
41 | end
42 |
43 | def setup_event_handlers(open_item, save_item, save_as_item, quit_item, about_item)
44 | # Track text changes
45 | @entry.on_changed do
46 | @is_modified = true
47 | update_title
48 | end
49 |
50 | # Open file functionality
51 | open_item.on_clicked do |window|
52 | if file_path = @main_window.open_file
53 | begin
54 | content = File.read(file_path)
55 | @entry.text = content
56 | @current_file_path = file_path
57 | @is_modified = false
58 | update_title
59 | rescue ex
60 | window.msg_box_error("Error", "Could not open file: #{ex.message}")
61 | end
62 | end
63 | end
64 |
65 | # Save file functionality
66 | save_item.on_clicked do |window|
67 | if @current_file_path
68 | begin
69 | if current_file_path = @current_file_path
70 | File.write(current_file_path, @entry.text || "")
71 | end
72 | @is_modified = false
73 | update_title
74 | rescue ex
75 | window.msg_box_error("Error", "Could not save file: #{ex.message}")
76 | end
77 | else
78 | # If no current file, act like Save As
79 | if file_path = @main_window.save_file
80 | begin
81 | File.write(file_path, @entry.text || "")
82 | @current_file_path = file_path
83 | @is_modified = false
84 | update_title
85 | rescue ex
86 | @main_window.msg_box_error("Error", "Could not save file: #{ex.message}")
87 | end
88 | end
89 | end
90 | end
91 |
92 | # Save As functionality
93 | save_as_item.on_clicked do |window|
94 | if file_path = @main_window.save_file
95 | begin
96 | File.write(file_path, @entry.text || "")
97 | @current_file_path = file_path
98 | @is_modified = false
99 | update_title
100 | rescue ex
101 | window.msg_box_error("Error", "Could not save file: #{ex.message}")
102 | end
103 | end
104 | end
105 |
106 | # Quit functionality - use UIng.on_should_quit instead of quit_item.on_clicked
107 | UIng.on_should_quit do
108 | if @is_modified
109 | # In a real application, you might want to show a confirmation dialog
110 | # For now, we'll just quit
111 | end
112 | cleanup
113 | true
114 | end
115 |
116 | # About functionality
117 | about_item.on_clicked do |window|
118 | window.msg_box("About Notepad", "Simple Notepad Application\nBuilt with UIng (Crystal binding for libui-ng)")
119 | end
120 |
121 | # Window closing handler
122 | @main_window.on_closing do
123 | if @is_modified
124 | # In a real application, you might want to show a save confirmation dialog
125 | else
126 | end
127 | UIng.quit
128 | true
129 | end
130 | end
131 |
132 | def update_title
133 | title = if current_file_path = @current_file_path
134 | filename = File.basename(current_file_path)
135 | modified_marker = @is_modified ? "*" : ""
136 | "#{modified_marker}#{filename} - Notepad"
137 | else
138 | modified_marker = @is_modified ? "*" : ""
139 | "#{modified_marker}Untitled - Notepad"
140 | end
141 | @main_window.title = title
142 | end
143 |
144 | def run
145 | @main_window.show
146 | UIng.main
147 | UIng.uninit
148 | end
149 |
150 | private def cleanup
151 | # See https://github.com/kojix2/uing/issues/19
152 | @vbox.delete(0)
153 | @entry.destroy
154 | @main_window.destroy
155 | puts "Bye Bye"
156 | end
157 | end
158 |
159 | # Create and run the application
160 | app = NotepadApp.new
161 | app.run
162 |
--------------------------------------------------------------------------------
/examples/zigzag/zigzag.cr:
--------------------------------------------------------------------------------
1 | require "uing"
2 | require "chipmunk"
3 |
4 | PX_PER_M = 32.0
5 | WIN_W = 600
6 | WIN_H = 400
7 | DT = 1.0/120.0
8 |
9 | def to_px(v : CP::Vect) : {Float64, Float64}
10 | {v.x * PX_PER_M, WIN_H - v.y * PX_PER_M} # Flip Y because screen Y+ is downward
11 | end
12 |
13 | # Collision types
14 | COLL_GROUND = 1
15 | COLL_PLAYER = 2
16 |
17 | # Main game class
18 | class Game
19 | getter space : CP::Space
20 | getter! player_body : CP::Body
21 | @player_body : CP::Body
22 | @segments = [] of {CP::Vect, CP::Vect}
23 |
24 | def initialize
25 | @space = CP::Space.new
26 | @space.gravity = CP.v(0, -9.8)
27 | @space.iterations = 10
28 | build_terrain
29 | @player_body = build_player
30 | end
31 |
32 | # Generate gentle zigzag planks as segments
33 | def build_terrain
34 | sb = @space.static_body
35 | @segments.clear
36 |
37 | width_m = WIN_W / PX_PER_M
38 | height_m = WIN_H / PX_PER_M
39 | margin_x = 1.0
40 | top_y = height_m - 1.2
41 | levels = 6
42 | len = (width_m - margin_x * 2) * 0.55
43 | vertical_gap = (height_m - 3.0) / (levels + 1)
44 | slope_drop = 3
45 |
46 | # Side walls and floor
47 | add_segment(sb, CP.v(margin_x * 0.2, 0.8), CP.v(margin_x * 0.2, height_m - 0.5))
48 | add_segment(sb, CP.v(width_m - margin_x * 0.2, 0.8), CP.v(width_m - margin_x * 0.2, height_m - 0.5))
49 | add_segment(sb, CP.v(margin_x * 0.2, 0.8), CP.v(width_m - margin_x * 0.2, 0.8))
50 |
51 | levels.times do |i|
52 | y = top_y - i * vertical_gap
53 | if i.even?
54 | x0 = margin_x
55 | x1 = (margin_x + len).clamp(margin_x, width_m - margin_x)
56 | y0 = y
57 | y1 = (y - slope_drop).clamp(0.8, height_m - 0.5)
58 | else
59 | x1 = width_m - margin_x
60 | x0 = (x1 - len).clamp(margin_x, width_m - margin_x)
61 | y1 = y
62 | y0 = (y - slope_drop).clamp(0.8, height_m - 0.5)
63 | end
64 | add_segment(sb, CP.v(x0, y0), CP.v(x1, y1))
65 | end
66 | end
67 |
68 | # Player is a single circle (simple model)
69 | def build_player : CP::Body
70 | mass = 70.0
71 | radius = 0.30
72 | moment = CP::Shape::Circle.moment(mass, 0.0, radius, CP.v(0, 0))
73 | body = CP::Body.new(mass, moment)
74 | # Start above the first plank on the left
75 | start_x = 1.2
76 | start_y = (WIN_H / PX_PER_M) - 1.0
77 | body.position = CP.v(start_x, start_y)
78 | @space.add body
79 | shape = CP::Shape::Circle.new(body, radius, CP.v(0, 0))
80 | shape.friction = 0.95
81 | shape.elasticity = 1.0
82 | shape.collision_type = COLL_PLAYER
83 | @space.add shape
84 | body
85 | end
86 |
87 | # Restart (R key)
88 | def reset!
89 | # Recreate space for a clean state
90 | @space = CP::Space.new
91 | @space.gravity = CP.v(0, -9.8)
92 | @space.iterations = 10
93 | build_terrain
94 | @player_body = build_player
95 | end
96 |
97 | # Step physics (twice per frame for ~60FPS)
98 | def update
99 | 2.times { @space.step(DT) }
100 | end
101 |
102 | # Segments for drawing
103 | def terrain_segments : Array({CP::Vect, CP::Vect})
104 | @segments
105 | end
106 |
107 | private def add_segment(sb : CP::Body, a : CP::Vect, b : CP::Vect)
108 | seg = CP::Shape::Segment.new(sb, a, b, 0.05)
109 | seg.friction = 1.1
110 | seg.elasticity = 0.6
111 | seg.collision_type = COLL_GROUND
112 | @space.add seg
113 | @segments << {a, b}
114 | end
115 | end
116 |
117 | UIng.init
118 | game = Game.new
119 |
120 | window = UIng::Window.new("ZigZag (Chipmunk)", WIN_W, WIN_H, menubar: false)
121 | box = UIng::Box.new(:vertical)
122 | label = UIng::Label.new("Press [R] to reset")
123 |
124 | # Area handler: drawing and key input
125 | handler = UIng::Area::Handler.new do
126 | draw do |area, params|
127 | ctx = params.context
128 | bg = UIng::Area::Draw::Brush.new(:solid, 0.96, 0.98, 1.0, 1.0)
129 | ctx.fill_path(bg) { |p| p.add_rectangle(0, 0, WIN_W, WIN_H) }
130 | line = UIng::Area::Draw::Brush.new(:solid, 0.0, 0.0, 0.0, 1.0)
131 | game.terrain_segments.each do |(a, b)|
132 | x0, y0 = to_px(a)
133 | x1, y1 = to_px(b)
134 | ctx.stroke_path(line, thickness: 3.0) do |path|
135 | path.new_figure(x0, y0)
136 | path.line_to(x1, y1)
137 | end
138 | end
139 | pos = game.player_body.position
140 | x, y = to_px(pos)
141 | player = UIng::Area::Draw::Brush.new(:solid, 0.2, 0.2, 0.2, 1.0)
142 | ctx.fill_path(player) do |path|
143 | path.new_figure_with_arc(x, y, 0.30 * PX_PER_M, 0, Math::PI * 2, false)
144 | end
145 | label.text = "Press [R] to reset"
146 | end
147 | key_event do |area, event|
148 | # Ignore key repeat (only handle key down)
149 | next true if event.up != 0
150 | case event.key
151 | when 'r', 'R'
152 | game.reset!
153 | area.queue_redraw_all
154 | true
155 | else
156 | false
157 | end
158 | end
159 | end
160 | area = UIng::Area.new(handler, WIN_W, WIN_H - 40)
161 |
162 | box.append(area, stretchy: true)
163 | box.append(label, stretchy: false)
164 | window.child = box
165 |
166 | UIng.timer(8) do
167 | game.update
168 | area.queue_redraw_all
169 | 1
170 | end
171 |
172 | window.on_closing { UIng.quit; true }
173 | window.show
174 | UIng.main
175 |
--------------------------------------------------------------------------------
/.github/workflows/screenshot.yml:
--------------------------------------------------------------------------------
1 | name: Screenshot Tests
2 |
3 | on:
4 | push:
5 | paths:
6 | - "examples/gallery/**"
7 | - "download.cr"
8 | workflow_dispatch:
9 |
10 | jobs:
11 | screenshot:
12 | name: ${{ matrix.os }} Screenshot
13 | runs-on: ${{ matrix.runner }}
14 | strategy:
15 | matrix:
16 | include:
17 | - os: ubuntu
18 | runner: ubuntu-latest
19 | setup-display: true
20 | - os: windows
21 | runner: windows-latest
22 | setup-display: false
23 | - os: macos
24 | runner: macos-latest
25 | setup-display: false
26 |
27 | env:
28 | DISPLAY: ${{ matrix.setup-display && ':99' || '' }}
29 |
30 | steps:
31 | - name: Checkout repository
32 | uses: actions/checkout@v6
33 | with:
34 | submodules: true
35 |
36 | - name: Install Crystal
37 | uses: crystal-lang/install-crystal@v1
38 | with:
39 | crystal: latest
40 |
41 | - name: Install Ubuntu dependencies
42 | if: matrix.os == 'ubuntu'
43 | run: |
44 | sudo apt-get update -y
45 | sudo apt-get install -y libgtk-3-dev xvfb imagemagick xdotool openbox wmctrl
46 |
47 | - name: Setup Windows Developer Command Prompt
48 | if: matrix.os == 'windows'
49 | uses: ilammy/msvc-dev-cmd@v1
50 | with:
51 | arch: x64
52 |
53 | - name: Install Crystal dependencies
54 | run: shards install
55 |
56 | - name: Download libui library
57 | run: |
58 | crystal run download.cr
59 | ${{ matrix.os == 'windows' && 'Get-ChildItem libui' || 'ls -la libui/' }}
60 |
61 | - name: Build all gallery (Linux/macOS)
62 | if: matrix.os != 'windows'
63 | shell: bash
64 | run: |
65 | for f in examples/gallery/*.cr; do
66 | name=$(basename "$f" .cr)
67 | crystal build "$f" -o "$name"
68 | done
69 |
70 | - name: Build all gallery (Windows)
71 | if: matrix.os == 'windows'
72 | shell: pwsh
73 | run: |
74 | Get-ChildItem examples/gallery/*.cr | ForEach-Object {
75 | $name = $_.BaseName
76 | crystal build $_.FullName -o $name
77 | }
78 |
79 | - name: Generate app-names and output-files
80 | if: matrix.os != 'windows'
81 | id: gen_names
82 | shell: bash
83 | run: |
84 | APPS=$(for f in examples/gallery/*.cr; do basename "$f" .cr; done | xargs)
85 | OUTPUTS=$(for f in examples/gallery/*.cr; do basename "$f" .cr; done | xargs -I{} echo -n "{}-${{ matrix.os }}.png ")
86 | echo "APP_NAMES=$APPS" >> $GITHUB_ENV
87 | echo "OUTPUT_FILES=$OUTPUTS" >> $GITHUB_ENV
88 |
89 | - name: Generate app-names and output-files (Windows)
90 | if: matrix.os == 'windows'
91 | id: gen_names_win
92 | shell: pwsh
93 | run: |
94 | $apps = (Get-ChildItem examples/gallery/*.cr | ForEach-Object { $_.BaseName }) -join ' '
95 | $outputs = (Get-ChildItem examples/gallery/*.cr | ForEach-Object { "$($_.BaseName)-${{ matrix.os }}.png" }) -join ' '
96 | echo "APP_NAMES=$apps" | Out-File -FilePath $env:GITHUB_ENV -Append
97 | echo "OUTPUT_FILES=$outputs" | Out-File -FilePath $env:GITHUB_ENV -Append
98 |
99 | - name: Take screenshots for all gallery
100 | uses: ./.github/actions/screenshot
101 | with:
102 | os: ${{ matrix.os }}
103 | app-names: ${{ env.APP_NAMES }}
104 | output-files: ${{ env.OUTPUT_FILES }}
105 |
106 | - name: Upload screenshots
107 | uses: actions/upload-artifact@v6
108 | with:
109 | name: screenshots-${{ matrix.os }}
110 | path: "*.png"
111 | retention-days: 30
112 |
113 | push_screenshots:
114 | name: Push all screenshots to orphan branch
115 | needs: screenshot
116 | runs-on: ubuntu-latest
117 | if: github.ref == 'refs/heads/main'
118 | steps:
119 | - name: Checkout repository
120 | uses: actions/checkout@v6
121 | with:
122 | submodules: true
123 |
124 | - name: Prepare orphan branch
125 | run: |
126 | git config --global user.name "github-actions[bot]"
127 | git config --global user.email "github-actions[bot]@users.noreply.github.com"
128 |
129 | # Prepare orphan branch first
130 | git fetch origin screenshots || true
131 | git checkout --orphan screenshots || git checkout screenshots
132 |
133 | - name: Download all screenshots artifacts
134 | uses: actions/download-artifact@v7
135 | with:
136 | pattern: screenshots-*
137 | merge-multiple: true
138 |
139 | - name: Commit screenshots
140 | run: |
141 | # Debug: Check downloaded files
142 | echo "=== Downloaded files ==="
143 | ls -la
144 |
145 | # Add gallery.md if it exists
146 | if [ -f gallery.md ]; then
147 | git add gallery.md
148 | fi
149 |
150 | # Commit only if PNG files exist
151 | if ls *.png 1> /dev/null 2>&1; then
152 | git add *.png
153 | git status
154 | if ! git diff --cached --quiet; then
155 | git commit -m "Update screenshots [skip ci]"
156 | git push -f origin screenshots
157 | else
158 | echo "No changes to commit."
159 | fi
160 | else
161 | echo "No PNG files found to commit."
162 | fi
163 |
--------------------------------------------------------------------------------