├── 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 | --------------------------------------------------------------------------------