├── .vdocignore ├── apps ├── v2048 │ ├── .gitignore │ ├── demo.png │ ├── v.mod │ ├── README.md │ ├── LICENSE │ └── 2048_app_ui.v ├── explorer │ └── events.v └── README.md ├── src ├── .DS_Store ├── assets │ ├── .DS_Store │ └── img │ │ ├── arrow.png │ │ ├── check.png │ │ ├── logo.png │ │ ├── radio.png │ │ ├── circle.png │ │ ├── cursor.png │ │ ├── arrow_black.png │ │ ├── arrow_white.png │ │ ├── check_black.png │ │ ├── check_white.png │ │ ├── darwin_circle.png │ │ ├── radio_selected.png │ │ ├── selected_radio.png │ │ ├── icons8-cursor-67.png │ │ ├── radio_white_selected.png │ │ ├── selected_radio_linux.png │ │ ├── icons8-hand-cursor-50.png │ │ └── icons8-text-cursor-50.png ├── tool_settings_paths_default.c.v ├── interface_mouse_enter_leave.v ├── extra_files_droped.v ├── ui_windows.c.v ├── tool_settings_paths_android.c.v ├── window_default.c.v ├── interface_build.v ├── interface_action.v ├── interface_themestyle.v ├── tool_settings.v ├── tool_draw.v ├── layout_layer.v ├── ui_darwin.c.v ├── extra_text.v ├── tool_coordinates.v ├── interface_clipping.v ├── ui_android.c.v ├── tool_rect.v ├── window_style.v ├── interface_adjustable.v ├── layout_row.v ├── tool_align.v ├── draw_device_context.v ├── layout_column.v ├── tool_gg.v ├── ui_default.c.v ├── interface_components.v ├── style_label.v ├── style_drawtextwidget.v ├── window_manager.v ├── style_progressbar.v ├── tool_calculate.v ├── tool_message_dialog.v ├── tool_easing.v ├── canvas.v ├── interface_shortcut.v ├── style_slider.v ├── interface_draw_device.v ├── interface_focusable.v └── style_textbox.v ├── examples ├── logo.png ├── plus.png ├── screenshot.png ├── switch_open.png ├── message.v ├── switch_close.png ├── README.md ├── group_with_auto_incr_size.png ├── android │ └── java │ │ ├── res │ │ ├── mipmap │ │ │ └── icon.png │ │ └── values │ │ │ └── strings.xml │ │ └── AndroidManifest.xml ├── textbox_input │ ├── .vab │ ├── textbox_demo_default.c.v │ ├── textbox_demo.v │ └── textbox_demo_android.c.v ├── apps │ ├── editor.v │ └── users.v ├── wm │ ├── wm_apps.v │ └── wm_free_apps.v ├── demo_files_droped_listbox.v ├── demo_label.v ├── component │ ├── rasterview.v │ ├── filebrowser.v │ ├── colorbox.v │ ├── dirbrowser.v │ ├── gg2048.v │ ├── rgb_color.v │ ├── double_listbox.v │ ├── grid.v │ ├── grid_boxlayout.v │ ├── treeview.v │ ├── tabs.v │ ├── fontchooser.v │ ├── splitpanel.v │ └── grid2.v ├── grid.v ├── dropdown.v ├── layout │ ├── box_layout.v │ ├── box_layout_with_textbox.v │ └── box_layout_inside_row.v ├── rectangles_resizable.v ├── 7guis │ ├── counter.v │ ├── counter_with_closure.v │ ├── cells.v │ ├── temperature.v │ └── timer.v ├── label_justify.v ├── switch.v ├── group.v ├── demo_calculate.v ├── dropdown2.v ├── demo_logview.v ├── change_title.v ├── rectangles.v ├── child_window.v ├── slider.v ├── demo_style_4colors.v ├── demo_radio.v ├── demo_scrollview.v ├── group2.v ├── canvas_plus_gradient_texture.v ├── build_examples.vsh ├── demo_style_accent_color.v ├── webview.v ├── group2_resizable.v ├── nested_scrollview_box_layout.v ├── demo_text_width_additive.v ├── demo_event.v ├── demo_textbox.v ├── transitions.v └── nested_scrollview.v ├── assets ├── img │ └── logo.png └── fonts │ ├── RobotoMono-Regular.ttf │ └── noto_emoji_font │ └── NotoEmoji.ttf ├── bin ├── README.md ├── help │ ├── vui_build.help │ ├── vui_png.help │ └── vui_demo.help ├── assets │ ├── README.md │ ├── build_demo_json.vsh │ └── demos.json ├── template │ ├── README.md │ └── vui_build.vv ├── 2048.v ├── demo │ ├── widgets │ │ ├── button_ui.vv │ │ └── label_ui.vv │ └── layouts │ │ ├── box_layout_ui.vv │ │ └── box_layout_clipping_ui.vv └── vui_settings.v ├── webview ├── windows │ ├── stbi.lib │ └── WebView2LoaderStatic.lib ├── webview_windows.c.v ├── webview_darwin.c.v ├── webview_linux.c.v └── webview.v ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── ci.yml │ ├── lint.yml │ ├── macos.yml │ ├── windows.yml │ └── linux.yml ├── .gitattributes ├── .editorconfig ├── v.mod ├── .gitignore ├── component ├── subwindow_fontchooser.v ├── subwindow_messagebox.v ├── settings.v ├── subwindow_colorbox.v ├── gg_app.v ├── fontbutton.v ├── messagebox.v ├── subwindow_filebrowser.v └── fontchooser.v ├── libvg ├── bitmap_text_style.v ├── svg_text_style.v └── raster_ttf.v ├── LICENSE └── tools └── demo_tools.v /.vdocignore: -------------------------------------------------------------------------------- 1 | /examples 2 | /bin 3 | -------------------------------------------------------------------------------- /apps/v2048/.gitignore: -------------------------------------------------------------------------------- 1 | 2048 2 | main 3 | -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/ui/HEAD/src/.DS_Store -------------------------------------------------------------------------------- /examples/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/ui/HEAD/examples/logo.png -------------------------------------------------------------------------------- /examples/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/ui/HEAD/examples/plus.png -------------------------------------------------------------------------------- /apps/v2048/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/ui/HEAD/apps/v2048/demo.png -------------------------------------------------------------------------------- /assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/ui/HEAD/assets/img/logo.png -------------------------------------------------------------------------------- /bin/README.md: -------------------------------------------------------------------------------- 1 | ## Running vui_demo.v 2 | 3 | ``` 4 | v -live run vui_demo.v 5 | ``` -------------------------------------------------------------------------------- /src/assets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/ui/HEAD/src/assets/.DS_Store -------------------------------------------------------------------------------- /examples/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/ui/HEAD/examples/screenshot.png -------------------------------------------------------------------------------- /examples/switch_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/ui/HEAD/examples/switch_open.png -------------------------------------------------------------------------------- /src/assets/img/arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/ui/HEAD/src/assets/img/arrow.png -------------------------------------------------------------------------------- /src/assets/img/check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/ui/HEAD/src/assets/img/check.png -------------------------------------------------------------------------------- /src/assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/ui/HEAD/src/assets/img/logo.png -------------------------------------------------------------------------------- /src/assets/img/radio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/ui/HEAD/src/assets/img/radio.png -------------------------------------------------------------------------------- /webview/windows/stbi.lib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/ui/HEAD/webview/windows/stbi.lib -------------------------------------------------------------------------------- /examples/message.v: -------------------------------------------------------------------------------- 1 | import ui 2 | 3 | fn main() { 4 | ui.message_box('Hello World') 5 | } 6 | -------------------------------------------------------------------------------- /examples/switch_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/ui/HEAD/examples/switch_close.png -------------------------------------------------------------------------------- /src/assets/img/circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/ui/HEAD/src/assets/img/circle.png -------------------------------------------------------------------------------- /src/assets/img/cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/ui/HEAD/src/assets/img/cursor.png -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | ## Examples 2 | 3 | users.v: 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/img/arrow_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/ui/HEAD/src/assets/img/arrow_black.png -------------------------------------------------------------------------------- /src/assets/img/arrow_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/ui/HEAD/src/assets/img/arrow_white.png -------------------------------------------------------------------------------- /src/assets/img/check_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/ui/HEAD/src/assets/img/check_black.png -------------------------------------------------------------------------------- /src/assets/img/check_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/ui/HEAD/src/assets/img/check_white.png -------------------------------------------------------------------------------- /src/assets/img/darwin_circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/ui/HEAD/src/assets/img/darwin_circle.png -------------------------------------------------------------------------------- /src/assets/img/radio_selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/ui/HEAD/src/assets/img/radio_selected.png -------------------------------------------------------------------------------- /src/assets/img/selected_radio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/ui/HEAD/src/assets/img/selected_radio.png -------------------------------------------------------------------------------- /assets/fonts/RobotoMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/ui/HEAD/assets/fonts/RobotoMono-Regular.ttf -------------------------------------------------------------------------------- /src/assets/img/icons8-cursor-67.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/ui/HEAD/src/assets/img/icons8-cursor-67.png -------------------------------------------------------------------------------- /examples/group_with_auto_incr_size.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/ui/HEAD/examples/group_with_auto_incr_size.png -------------------------------------------------------------------------------- /src/assets/img/radio_white_selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/ui/HEAD/src/assets/img/radio_white_selected.png -------------------------------------------------------------------------------- /src/assets/img/selected_radio_linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/ui/HEAD/src/assets/img/selected_radio_linux.png -------------------------------------------------------------------------------- /assets/fonts/noto_emoji_font/NotoEmoji.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/ui/HEAD/assets/fonts/noto_emoji_font/NotoEmoji.ttf -------------------------------------------------------------------------------- /examples/android/java/res/mipmap/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/ui/HEAD/examples/android/java/res/mipmap/icon.png -------------------------------------------------------------------------------- /src/assets/img/icons8-hand-cursor-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/ui/HEAD/src/assets/img/icons8-hand-cursor-50.png -------------------------------------------------------------------------------- /src/assets/img/icons8-text-cursor-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/ui/HEAD/src/assets/img/icons8-text-cursor-50.png -------------------------------------------------------------------------------- /webview/windows/WebView2LoaderStatic.lib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/ui/HEAD/webview/windows/WebView2LoaderStatic.lib -------------------------------------------------------------------------------- /bin/help/vui_build.help: -------------------------------------------------------------------------------- 1 | Shortcuts: 2 | 3 | Ctrl + H: toggle this help message box 4 | Ctrl + O: toggle Menu File 5 | Ctrl + P: toggle Palette 6 | -------------------------------------------------------------------------------- /examples/textbox_input/.vab: -------------------------------------------------------------------------------- 1 | Vab { 2 | activity_name: 'VUIActivity' 3 | package_id: 'io.v.android.ui' 4 | package_overrides: '../android/java' 5 | } 6 | -------------------------------------------------------------------------------- /src/tool_settings_paths_default.c.v: -------------------------------------------------------------------------------- 1 | module ui 2 | 3 | import os 4 | 5 | const settings_dir = os.join_path_single(os.config_dir() or { os.home_dir() }, '.vui') 6 | -------------------------------------------------------------------------------- /examples/apps/editor.v: -------------------------------------------------------------------------------- 1 | import ui.apps.editor 2 | 3 | const win_width = 780 4 | const win_height = 395 5 | 6 | fn main() { 7 | mut app := editor.app() 8 | app.run() 9 | } 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: 'enhancement' 6 | assignees: '' 7 | 8 | --- 9 | -------------------------------------------------------------------------------- /src/interface_mouse_enter_leave.v: -------------------------------------------------------------------------------- 1 | module ui 2 | 3 | pub interface EnterLeaveWidget { 4 | mut: 5 | id string 6 | mouse_enter(e &MouseMoveEvent) 7 | mouse_leave(e &MouseMoveEvent) 8 | } 9 | -------------------------------------------------------------------------------- /apps/v2048/v.mod: -------------------------------------------------------------------------------- 1 | Module { 2 | name: 'v2048', 3 | description: 'A simple 2048 game written in V.', 4 | version: '0.0.2', 5 | repo_url: 'https://github.com/spytheman/v2048', 6 | dependencies: [] 7 | } 8 | -------------------------------------------------------------------------------- /bin/assets/README.md: -------------------------------------------------------------------------------- 1 | ## Creation of demos.json file 2 | 3 | After adding a new demo file in the `../demo` folder, `v run build_demo_json.vsh` to generate the `demos.json` file which is embedded in `vui_demo.v` -------------------------------------------------------------------------------- /examples/android/java/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | V 4 | v 5 | 6 | 7 | -------------------------------------------------------------------------------- /bin/template/README.md: -------------------------------------------------------------------------------- 1 | `bin/vui_demo.v` as a program executed in `-live` mode, so it is modified. `vui_demo.vv` is the original version of `bin/vui_demo.v`. 2 | 3 | 4 | `vui_build.vv` is a template to generate a vui app. -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | *.bat eol=crlf 3 | 4 | **/*.v linguist-language=V 5 | **/*.vv linguist-language=V 6 | **/*.vsh linguist-language=V 7 | **/v.mod linguist-language=V 8 | 9 | webview/windows/* linguist-vendored 10 | -------------------------------------------------------------------------------- /examples/textbox_input/textbox_demo_default.c.v: -------------------------------------------------------------------------------- 1 | import ui 2 | 3 | fn (mut a App) init(window &ui.Window) { 4 | // Stub 5 | } 6 | 7 | fn (mut a App) show_soft_input() { 8 | // Stub 9 | } 10 | 11 | fn (mut a App) hide_soft_input() { 12 | // Stub 13 | } 14 | -------------------------------------------------------------------------------- /bin/help/vui_png.help: -------------------------------------------------------------------------------- 1 | Shortcuts: 2 | 3 | Ctrl + H: toggle this help message box 4 | Ctrl + O: toggle Menu File 5 | Ctrl + P: toggle Palette 6 | 7 | Specify size for a new png file: 8 | append `-(width)x(height)` or `-(size)` (equivalent to `-(size)x(size)`) or at the end of the file name just before the extension .png -------------------------------------------------------------------------------- /bin/2048.v: -------------------------------------------------------------------------------- 1 | import ui.apps.v2048 2 | 3 | // This code is not really V UI related 4 | // It is just a consequence of 2048 packaged as a module 5 | // However, the package is complete enough to be called inside VUI 6 | // see examples/component/gg2048.v 7 | 8 | fn main() { 9 | mut app := v2048.new_gg_app() 10 | app.run() 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | 9 | [*.v] 10 | indent_style = tab 11 | indent_size = 4 12 | 13 | [*.{yml,yaml}] 14 | indent_style = space 15 | indent_size = 2 16 | 17 | [*.md] 18 | trim_trailing_whitespace = false 19 | -------------------------------------------------------------------------------- /examples/apps/users.v: -------------------------------------------------------------------------------- 1 | import ui 2 | import ui.apps.users 3 | 4 | fn main() { 5 | mut app := users.app() 6 | app.add_window( 7 | width: 780 8 | height: 395 9 | title: 'V UI Demo' 10 | mode: .resizable 11 | bg_color: ui.color_solaris 12 | // theme: 'red' 13 | native_message: false 14 | ) 15 | app.run() 16 | } 17 | -------------------------------------------------------------------------------- /examples/wm/wm_apps.v: -------------------------------------------------------------------------------- 1 | import ui 2 | import ui.apps.users 3 | import ui.apps.editor 4 | import ui.apps.v2048 5 | 6 | fn main() { 7 | mut wm := ui.wm( 8 | apps: { 9 | 'appusers: (20,20) ++ (600,400)': users.new() 10 | 'editor: (400,10) ++ (600,400)': editor.new() 11 | 'v2048: (100,100) ++ (600,400)': v2048.new() 12 | } 13 | ) 14 | wm.run() 15 | } 16 | -------------------------------------------------------------------------------- /src/extra_files_droped.v: -------------------------------------------------------------------------------- 1 | module ui 2 | 3 | import sokol.sapp 4 | 5 | // wrapper just to not include `import sokol.sapp` 6 | 7 | // TODO: documentation 8 | pub fn get_num_dropped_files() int { 9 | return sapp.get_num_dropped_files() 10 | } 11 | 12 | // TODO: documentation 13 | pub fn get_dropped_file_path(i int) string { 14 | return sapp.get_dropped_file_path(i) 15 | } 16 | -------------------------------------------------------------------------------- /bin/help/vui_demo.help: -------------------------------------------------------------------------------- 1 | Shortcuts: 2 | 3 | ctrl + h: toggle this help message box 4 | ctrl + o: show tree of examples 5 | ctrl + r: run the current code 6 | ctrl + e: only show the editor of the source code 7 | ctrl + v: only show the view of the result 8 | ctrl + n: show editor and view of the result 9 | ctrl + b: show tree of children to show the bounding box of selected child 10 | -------------------------------------------------------------------------------- /examples/demo_files_droped_listbox.v: -------------------------------------------------------------------------------- 1 | import ui 2 | 3 | fn main() { 4 | window := ui.window( 5 | mode: .resizable 6 | height: 240 7 | layout: ui.row( 8 | widths: ui.stretch 9 | children: [ 10 | ui.listbox( 11 | id: 'lb' 12 | draw_lines: true 13 | files_dropped: true 14 | ), 15 | ] 16 | ) 17 | ) 18 | ui.run(window) 19 | } 20 | -------------------------------------------------------------------------------- /v.mod: -------------------------------------------------------------------------------- 1 | Module { 2 | name: 'ui' 3 | author: 'medvednikov' 4 | version: '0.0.4' 5 | repo_url: 'https://github.com/vlang/ui' 6 | vcs: 'git' 7 | tags: ['gui','user interface'] 8 | description: 'V UI is a cross-platform UI toolkit for Windows, macOS, Linux, and soon Android, iOS and the web (JS/WASM).' 9 | license: 'MIT' 10 | } 11 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '31 1,12 * * *' 8 | 9 | jobs: 10 | lint: 11 | uses: ./.github/workflows/lint.yml 12 | 13 | linux: 14 | uses: ./.github/workflows/linux.yml 15 | 16 | macos: 17 | uses: ./.github/workflows/macos.yml 18 | 19 | windows: 20 | uses: ./.github/workflows/windows.yml 21 | -------------------------------------------------------------------------------- /examples/demo_label.v: -------------------------------------------------------------------------------- 1 | import ui 2 | import gg 3 | 4 | fn main() { 5 | ui.run(ui.window( 6 | width: 300 7 | height: 100 8 | title: 'Name' 9 | layout: ui.box_layout( 10 | children: { 11 | 'rect: stretch': ui.rectangle(color: gg.white) 12 | 'lab: stretch': ui.label( 13 | text: 'Centered text' 14 | justify: ui.center 15 | ) 16 | } 17 | ) 18 | )) 19 | } 20 | -------------------------------------------------------------------------------- /examples/wm/wm_free_apps.v: -------------------------------------------------------------------------------- 1 | import ui 2 | import ui.apps.users 3 | import ui.apps.editor 4 | import ui.apps.v2048 5 | 6 | fn main() { 7 | mut wm := ui.wm( 8 | mode: .max_size 9 | kind: .free 10 | apps: { 11 | 'appusers: (0,0) ++ (0.3,0.5)': users.new() 12 | 'editor: (0.3,0) -> (1,1)': editor.new() 13 | 'v2048: (0,0.5) ++ (0.3,0.5)': v2048.new() 14 | } 15 | ) 16 | wm.run() 17 | } 18 | -------------------------------------------------------------------------------- /src/ui_windows.c.v: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2022 Alexander Medvednikov. All rights reserved. 2 | // Use of this source code is governed by a MIT license 3 | // that can be found in the LICENSE file. 4 | module ui 5 | 6 | fn C.MessageBox(h voidptr, text &u16, caption &u16, kind u32) int 7 | 8 | pub fn message_box(s string) { 9 | title := '' 10 | C.MessageBox(0, s.to_wide(), title.to_wide(), C.MB_OK) 11 | } 12 | -------------------------------------------------------------------------------- /src/tool_settings_paths_android.c.v: -------------------------------------------------------------------------------- 1 | module ui 2 | 3 | import os 4 | import sokol.sapp 5 | 6 | #include 7 | 8 | const settings_dir = os.join_path_single(get_app_data_directory(), '.vui') 9 | 10 | fn get_app_data_directory() string { 11 | activity := &os.NativeActivity(sapp.android_get_native_activity()) 12 | path := unsafe { cstring_to_vstring(activity.internalDataPath) } 13 | return path 14 | } 15 | -------------------------------------------------------------------------------- /src/window_default.c.v: -------------------------------------------------------------------------------- 1 | module ui 2 | 3 | fn C.sapp_mouse_locked() bool 4 | 5 | fn C.sapp_set_window_title(&char) 6 | 7 | /* 8 | // #define cls objc_getClass 9 | // #define sel sel_getUid 10 | #define objc_msg ((id (*)(id, SEL, ...))objc_msgSend) 11 | #define objc_cls_msg ((id (*)(Class, SEL, ...))objc_msgSend) 12 | 13 | fn C.objc_msg() 14 | 15 | fn C.objc_cls_msg() 16 | 17 | fn C.sel_getUid() 18 | 19 | fn C.objc_getClass() 20 | */ 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Bug report 4 | title: '' 5 | labels: 'bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | **V version:** 13 | **UI version:** 14 | **OS:** 15 | 16 | **What did you do?** 17 | 18 | 19 | **What did you expect to see?** 20 | 21 | 22 | **What did you see instead?** 23 | -------------------------------------------------------------------------------- /bin/demo/widgets/button_ui.vv: -------------------------------------------------------------------------------- 1 | import ui 2 | 3 | btn_click := fn (_ &ui.Button) { 4 | ui.message_box('coucou toto!') 5 | } 6 | 7 | layout = ui.box_layout( 8 | children: { 9 | 'btn: (0.2, 0.4) -> (0.5,0.5)': ui.button( 10 | text: 'show' 11 | on_click: fn (btn &ui.Button) { 12 | ui.message_box('Hi everybody !') 13 | } 14 | ) 15 | 'btn2: (0.7, 0.2) ++ (40,20)': ui.button( 16 | text: 'show2' 17 | on_click: btn_click 18 | ) 19 | } 20 | ) 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore build binaries 2 | *.so 3 | *.dll 4 | *.dylib 5 | *.exe 6 | *.ilk 7 | *.pdb 8 | 9 | # All sub-level build binaries 10 | */**/* 11 | !*/**/*/ 12 | !*/**/*.* 13 | 14 | # Common editor/system specific metadata 15 | .DS_Store 16 | .idea 17 | .vscode 18 | *.code-workspace 19 | .lite_workspace.lua 20 | ui.iml 21 | *~ 22 | *# 23 | 24 | # Other files 25 | fns.txt 26 | screenshot*.png 27 | screenshot*.svg 28 | 29 | # To develop examples inside ui folder 30 | devel 31 | -------------------------------------------------------------------------------- /bin/assets/build_demo_json.vsh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S v 2 | 3 | import json 4 | import os 5 | 6 | mut demos := map[string]string{} 7 | 8 | dir := os.join_path(os.dir(@FILE), '..', 'demo') 9 | json_file := os.join_path(os.dir(@FILE), 'demos.json') 10 | os.chdir(dir)! 11 | for demo in os.walk_ext('.', '_ui.vv') { 12 | tmp := demo.split(os.path_separator) 13 | tmp2 := tmp#[1..] 14 | tmp3 := tmp2.join(os.path_separator) 15 | demos[tmp3#[..-6]] = os.read_file(demo)! 16 | } 17 | 18 | os.write_file(json_file, json.encode(demos))! 19 | -------------------------------------------------------------------------------- /bin/demo/layouts/box_layout_ui.vv: -------------------------------------------------------------------------------- 1 | tb := ui.textbox( 2 | mode: .multiline 3 | bg_color: gx.yellow 4 | text_value: 'blah blah blah\n'.repeat(10) 5 | ) 6 | layout = ui.box_layout( 7 | id: 'bl' 8 | children: { 9 | 'id1: (0,0) ++ (30,30)': ui.rectangle( 10 | color: gx.rgb(255, 100, 100) 11 | ) 12 | 'id2: (30,30) -> (-30.5,-30.5)': ui.rectangle( 13 | color: gx.rgb(100, 255, 100) 14 | ) 15 | 'id3: (0.5,0.5) -> (1,1)': tb 16 | 'id4: (-30.5, -30.5) ++ (30,30)': ui.rectangle( 17 | color: gx.white 18 | ) 19 | } 20 | ) -------------------------------------------------------------------------------- /apps/explorer/events.v: -------------------------------------------------------------------------------- 1 | module explorer 2 | 3 | import ui 4 | 5 | @[heap] 6 | pub struct AppEvents { 7 | pub mut: 8 | id string 9 | window &ui.Window = unsafe { nil } 10 | layout &ui.Layout = ui.empty_stack 11 | on_init ui.WindowFn 12 | } 13 | 14 | @[params] 15 | pub struct AppEventsParams { 16 | pub mut: 17 | id string 18 | } 19 | 20 | pub fn new(p AppEventsParams) &AppEvents { 21 | mut app := &AppEvents{ 22 | id: p.id 23 | users: p.users 24 | } 25 | app.make_layout() 26 | return app 27 | } 28 | 29 | pub fn app(p AppEventsParams) &ui.Application { 30 | app := new(p) 31 | return &ui.Application(app) 32 | } 33 | 34 | pub fn (mut app AppEvents) make_layout() { 35 | } 36 | -------------------------------------------------------------------------------- /examples/component/rasterview.v: -------------------------------------------------------------------------------- 1 | import ui 2 | import ui.component as uic 3 | import os 4 | 5 | const win_width = 500 6 | const win_height = 500 7 | 8 | fn main() { 9 | window := ui.window( 10 | width: win_width 11 | height: win_height 12 | title: 'Grid' 13 | mode: .resizable 14 | on_init: win_init 15 | layout: ui.row( 16 | children: [ 17 | uic.rasterview_canvaslayout( 18 | id: 'rv' 19 | ), 20 | ] 21 | ) 22 | ) 23 | ui.run(window) 24 | } 25 | 26 | fn win_init(mut w ui.Window) { 27 | mut rv := uic.rasterview_component_from_id(w, 'rv') 28 | rv.load_image(os.resource_abs_path(os.join_path('../../assets/img', 'logo.png'))) 29 | // rv.load_image('../assets/img/icons8-cursor-67.png') 30 | } 31 | -------------------------------------------------------------------------------- /bin/vui_settings.v: -------------------------------------------------------------------------------- 1 | import ui 2 | // import gx 3 | // import gg 4 | import ui.component as uic 5 | import os.font 6 | 7 | fn main() { 8 | mut window := ui.window( 9 | width: 800 10 | height: 600 11 | title: 'V UI Settings' 12 | mode: .resizable 13 | // on_key_down: fn(e ui.KeyEvent, wnd &ui.Window) { 14 | // println('key down') 15 | //} 16 | layout: ui.column( 17 | // alignment: .center 18 | spacing: 5 19 | margin_: 5 20 | widths: ui.stretch 21 | heights: 25.0 22 | children: [ 23 | uic.setting_font(id: 'color', text: 'toto'), 24 | uic.setting_font(id: 'color2', text: 'toto2'), 25 | ] 26 | ) 27 | ) 28 | uic.fontchooser_subwindow_add(mut window) 29 | println(font.default()) 30 | ui.run(window) 31 | } 32 | -------------------------------------------------------------------------------- /examples/grid.v: -------------------------------------------------------------------------------- 1 | import ui 2 | 3 | const win_width = 500 4 | const win_height = 500 5 | 6 | struct App { 7 | mut: 8 | grid &ui.Grid 9 | window &ui.Window 10 | } 11 | 12 | fn main() { 13 | h := ['One', 'Two', 'Three'] 14 | b := [['body one', 'body two', 'body three'], ['V', 'UI is', 'Beautiful']] 15 | mut app := &App{ 16 | window: unsafe { nil } 17 | grid: ui.grid(header: h, body: b, width: win_width - 10, height: win_height) 18 | } 19 | app.window = ui.window( 20 | width: win_width 21 | height: win_height 22 | title: 'Grid' 23 | mode: .resizable 24 | children: [ 25 | ui.row( 26 | margin: ui.Margin{5, 5, 5, 5} 27 | children: [ 28 | app.grid, 29 | ] 30 | ), 31 | ] 32 | ) 33 | ui.run(app.window) 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | fmt: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Setup V 11 | uses: vlang/setup-v@v1.3 12 | with: 13 | check-latest: true 14 | - uses: actions/checkout@v4 15 | with: 16 | path: ui 17 | - name: Check formatting 18 | run: v fmt -verify ui/ 19 | 20 | vet: 21 | runs-on: ubuntu-latest 22 | if: false # TODO: satisfy vet tool 23 | steps: 24 | - name: Setup V 25 | uses: vlang/setup-v@v1.3 26 | with: 27 | check-latest: true 28 | - uses: actions/checkout@v4 29 | with: 30 | path: ui 31 | - name: Run vet 32 | run: v vet ui/ 33 | -------------------------------------------------------------------------------- /webview/webview_windows.c.v: -------------------------------------------------------------------------------- 1 | module webview 2 | 3 | // WebView2.h includes objidl.h, but _too late_. 4 | // We fix it by including objidl.h earlier than including WebView2.h 5 | #include 6 | 7 | // WinRT headers. EventToken.h lies here. 8 | #flag windows -I $env('SystemDrive')/Program Files (x86)/Windows Kits/10/Include/10.0.19041.0/winrt 9 | #include 10 | 11 | #flag Version.lib Advapi32.lib Shell32.lib 12 | 13 | #flag @VMODROOT/webview/windows/WebView2LoaderStatic.lib 14 | #flag @VMODROOT/webview/windows/stbi.lib 15 | #include "@VMODROOT/webview/windows/webview_windows.c" 16 | 17 | fn C.new_windows_web_view(url &byte, title &byte) voidptr 18 | 19 | fn C.windows_webview_close() 20 | 21 | fn C.exec(scriptSource &byte) 22 | 23 | fn C.navigate(url &u16) 24 | -------------------------------------------------------------------------------- /examples/component/filebrowser.v: -------------------------------------------------------------------------------- 1 | import ui 2 | import ui.component as uic 3 | // import gg 4 | 5 | const win_width = 800 6 | const win_height = 600 7 | 8 | fn main() { 9 | window := ui.window( 10 | width: win_width 11 | height: win_height 12 | title: 'V UI: File Browser' 13 | native_message: false 14 | mode: .resizable 15 | layout: uic.filebrowser_stack( 16 | id: 'fb' 17 | on_click_ok: on_click_ok 18 | on_click_cancel: on_click_cancel 19 | ) 20 | ) 21 | ui.run(window) 22 | } 23 | 24 | fn on_click_ok(b &ui.Button) { 25 | println(uic.filebrowser_component(b).selected_full_title()) 26 | } 27 | 28 | fn on_click_cancel(b &ui.Button) { 29 | if b.ui.dd is ui.DrawDeviceContext { 30 | b.ui.dd.quit() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /apps/README.md: -------------------------------------------------------------------------------- 1 | ## Applications as module 2 | 3 | Application interface provide a way to develop an application as a self-content module. 4 | 1) It is then possible to launch several same applications together that can interact with each other (see wm). 5 | ```v 6 | import ui 7 | import ui.apps.users 8 | import ui.apps.editor 9 | 10 | fn main() { 11 | mut wm := ui.wm() 12 | mut app := users.new() 13 | wm.add('appusers: (20,20) ++ (600,400)', mut app) 14 | mut app2 := editor.new() 15 | wm.add('editor: (400,10) ++ (600,400)', mut app2) 16 | wm.run() 17 | } 18 | ``` 19 | 2) An application developed in a module can be also used as a simple application (see `example/apps` folder) 20 | 21 | ```v 22 | import ui.apps.editor 23 | 24 | fn main() { 25 | mut app := editor.app() 26 | app.run() 27 | } 28 | ``` 29 | -------------------------------------------------------------------------------- /bin/demo/layouts/box_layout_clipping_ui.vv: -------------------------------------------------------------------------------- 1 | layout = ui.box_layout( 2 | children: { 3 | "bl1: (0,0) -> (0.4, 0.5)": ui.box_layout( 4 | children: { 5 | "bl1/rect: (0, 0) ++ (300, 300)": ui.rectangle(color: gx.yellow) 6 | "bl1/lab: (0, 0) ++ (300, 300)": ui.label( 7 | text: "loooonnnnnggggg ttteeeeeeexxxxxxxtttttttttt\nwoulbe clipped inside a boxlayout when reducing the window" 8 | ) 9 | } 10 | ) 11 | "bl2: (0.5,0.5) -> (0.9, 1)": ui.box_layout( 12 | children: { 13 | "bl2/rect: (0, 0) ++ (300, 300)": ui.rectangle(color: gx.orange) 14 | "bl2/lab: (0, 0) ++ (300, 300)": ui.label( 15 | text: "clipped loooonnnnnggggg ttteeeeeeexxxxxxxtttttttttt\nwoulbe clipped inside a boxlayout when reducing the window" 16 | clipping: true 17 | ) 18 | } 19 | ) 20 | } 21 | ) -------------------------------------------------------------------------------- /bin/demo/widgets/label_ui.vv: -------------------------------------------------------------------------------- 1 | layout = ui.box_layout( 2 | children: { 3 | 'rect: (0.2, 0.4) -> (0.5,0.5)': ui.rectangle( 4 | color: ui.alpha_colored(gx.yellow,30) 5 | ), 6 | 'rect2: (0.5, 0.5) -> (1,1)': ui.rectangle( 7 | color: ui.alpha_colored(gx.blue, 30) 8 | ) 9 | 'rect3: (0.1, 0.1) -> (0.3,0.2)': ui.rectangle( 10 | color: ui.alpha_colored(gx.orange, 30) 11 | ), 12 | 'lab: (0.2, 0.4) -> (0.5,0.5)': ui.label( 13 | text: 'Centered text' 14 | justify: ui.center // [0.5, 0.5] 15 | ), 16 | 'lab2: (0.5, 0.5) -> (1,1)': ui.label( 17 | text: 'Centered text\n2nd line\n3rd line' 18 | justify: ui.top_center // [0.0, 0.5] 19 | ), 20 | 'lab3: (0.1, 0.1) -> (0.3,0.2)': ui.label( 21 | text: 'long texttttttttttttttttttttttttttttttttt' 22 | clipping: true 23 | ), 24 | } 25 | ) 26 | -------------------------------------------------------------------------------- /examples/component/colorbox.v: -------------------------------------------------------------------------------- 1 | import ui 2 | import ui.component as uic 3 | import gg 4 | 5 | const win_width = 30 + 256 + 4 * 10 + uic.cb_cv_hsv_w 6 | const win_height = 376 7 | 8 | fn main() { 9 | cb_layout := uic.colorbox_stack(id: 'cbox', light: false, hsl: false) 10 | rect := ui.rectangle(text: 'Here a simple ui rectangle') 11 | mut dtw := ui.DrawTextWidget(rect) 12 | dtw.update_style(color: gg.blue, size: 30) 13 | window := ui.window( 14 | width: win_width 15 | height: win_height 16 | title: 'V UI: Toolbar' 17 | native_message: false 18 | layout: ui.column( 19 | heights: [ui.compact, ui.compact] 20 | children: [cb_layout, rect] 21 | ) 22 | ) 23 | mut cb := uic.colorbox_component(cb_layout) 24 | cb.connect(&rect.style.color) 25 | ui.run(window) 26 | } 27 | -------------------------------------------------------------------------------- /examples/component/dirbrowser.v: -------------------------------------------------------------------------------- 1 | // Same as demo_component_filebrowser with folder_only: true 2 | import ui 3 | import ui.component as uic 4 | 5 | const win_width = 800 6 | const win_height = 600 7 | 8 | fn main() { 9 | window := ui.window( 10 | width: win_width 11 | height: win_height 12 | title: 'V UI: File Browser' 13 | native_message: false 14 | mode: .resizable 15 | layout: uic.filebrowser_stack( 16 | id: 'fb' 17 | on_click_ok: on_click_ok 18 | on_click_cancel: on_click_cancel 19 | folder_only: true 20 | ) 21 | ) 22 | ui.run(window) 23 | } 24 | 25 | fn on_click_ok(b &ui.Button) { 26 | println(uic.filebrowser_component(b).selected_full_title()) 27 | } 28 | 29 | fn on_click_cancel(b &ui.Button) { 30 | if b.ui.dd is ui.DrawDeviceContext { 31 | b.ui.dd.quit() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /component/subwindow_fontchooser.v: -------------------------------------------------------------------------------- 1 | module component 2 | 3 | import ui 4 | 5 | const fontchooser_subwindow_id = '_sw_font' 6 | 7 | // Append fontchooser to window 8 | pub fn fontchooser_subwindow_add(mut w ui.Window) { //}, fontchooser_lb_change ui.ListBoxSelectionChangedFn) { 9 | // only once 10 | if !ui.Layout(w).has_child_id(fontchooser_subwindow_id) { 11 | w.subwindows << ui.subwindow( 12 | id: fontchooser_subwindow_id 13 | layout: fontchooser_stack() 14 | ) 15 | } 16 | } 17 | 18 | // TODO: documentation 19 | pub fn fontchooser_subwindow_visible(w &ui.Window) { 20 | mut s := w.get_or_panic[ui.SubWindow](fontchooser_subwindow_id) 21 | s.set_visible(s.hidden) 22 | s.update_layout() 23 | } 24 | 25 | // TODO: documentation 26 | pub fn fontchooser_subwindow(w &ui.Window) &ui.SubWindow { 27 | return w.get_or_panic[ui.SubWindow](fontchooser_subwindow_id) 28 | } 29 | -------------------------------------------------------------------------------- /apps/v2048/README.md: -------------------------------------------------------------------------------- 1 | This code is a slight adaptation of the excellent 2048 game and it is proposed here as a module callable as a component and an inside v ui 2 | 3 | # V 2048 4 | 5 | This is a simple 2048 game, written in [the V programming language](https://vlang.io/). 6 | 7 | WebAssembly demo: https://v2048.vercel.app 8 | 9 | ![screenshot](demo.png) 10 | 11 | ## Description: 12 | Merge tiles by moving them. 13 | After each move, a new random tile is added (2 or 4). 14 | The goal of the game is to create a tile with a value of 2048. 15 | 16 | ## Keys: 17 | Escape - exit the game 18 | Backspace - undo last move 19 | n - restart the game 20 | t - toggle the UI theme 21 | Enter - toggle the tile text format 22 | 23 | UP,LEFT,DOWN,RIGHT / W,A,S,D / touchscreen swipes - move the tiles 24 | 25 | ## Running instructions: 26 | Compile & run the game with `./v run examples/2048` 27 | 28 | -------------------------------------------------------------------------------- /src/interface_build.v: -------------------------------------------------------------------------------- 1 | module ui 2 | 3 | pub interface WidgetBuild { 4 | mut: 5 | id string 6 | ui &UI 7 | build(mut win Window) 8 | } 9 | 10 | // pub fn (l Layout) build_(win &Window) { 11 | // for mut w in l.get_children() { 12 | // if mut w is WidgetBuild { 13 | // mut wb := w as WidgetBuild 14 | // wb.build(win) 15 | // } 16 | // if w is Layout { 17 | // wl := w as Layout 18 | // wl.build_(win) 19 | // } 20 | // } 21 | // } 22 | 23 | // TODO: documentation 24 | pub fn (mut win Window) build_layout(l Layout) { 25 | for mut w in l.get_children() { 26 | if mut w is WidgetBuild { 27 | mut wb := w as WidgetBuild 28 | wb.build(mut win) 29 | } 30 | if mut w is Layout { 31 | mut wl := w as Layout 32 | win.build_layout(wl) 33 | } 34 | } 35 | } 36 | 37 | // TODO: documentation 38 | pub fn (mut win Window) build() { 39 | win.build_layout(win) 40 | } 41 | -------------------------------------------------------------------------------- /examples/dropdown.v: -------------------------------------------------------------------------------- 1 | import ui 2 | 3 | const win_width = 250 4 | const win_height = 250 5 | 6 | fn dd_change(dd &ui.Dropdown) { 7 | println(dd.selected().text) 8 | } 9 | 10 | fn main() { 11 | window := ui.window( 12 | width: win_width 13 | height: win_height 14 | title: 'Dropdown' 15 | children: [ 16 | ui.column( 17 | margin: ui.Margin{5, 5, 5, 5} 18 | children: [ 19 | ui.dropdown( 20 | width: 140 21 | def_text: 'Select an option' 22 | on_selection_changed: dd_change 23 | items: [ 24 | ui.DropdownItem{ 25 | text: 'Delete all users' 26 | }, 27 | ui.DropdownItem{ 28 | text: 'Export users' 29 | }, 30 | ui.DropdownItem{ 31 | text: 'Exit' 32 | }, 33 | ] 34 | ), 35 | ] 36 | ), 37 | ] 38 | ) 39 | ui.run(window) 40 | } 41 | -------------------------------------------------------------------------------- /examples/layout/box_layout.v: -------------------------------------------------------------------------------- 1 | import ui 2 | import gg 3 | 4 | const win_width = 400 5 | const win_height = 300 6 | 7 | fn main() { 8 | ui.run(ui.window( 9 | width: win_width 10 | height: win_height 11 | title: 'V UI: Rectangles inside BoxLayout' 12 | mode: .resizable 13 | layout: ui.box_layout( 14 | id: 'bl' 15 | children: { 16 | 'id1: (0,0) ++ (30,30)': ui.rectangle( 17 | color: gg.rgb(255, 100, 100) 18 | ) 19 | 'id2: (30,30) -> (-30.5,-30.5)': ui.rectangle( 20 | color: gg.rgb(100, 255, 100) 21 | ) 22 | 'id3: (50%,50%) -> (100%,100%)': ui.rectangle( 23 | color: gg.rgb(100, 100, 255) 24 | ) 25 | 'id4: (-30.5, -30.5) ++ (30,30)': ui.rectangle( 26 | color: gg.white 27 | ) 28 | 'id5: (@id4.x + 5, @id4.y+5) ++ (20,20)': ui.rectangle( 29 | color: gg.black 30 | ) 31 | } 32 | ) 33 | )) 34 | } 35 | -------------------------------------------------------------------------------- /src/interface_action.v: -------------------------------------------------------------------------------- 1 | module ui 2 | 3 | // Adding actions field for a Widget or Component (having id field) makes it react as user-dedined actions 4 | // see tool_key for parsing action as string 5 | 6 | // This provides user defined action actions (see grid and grid_data as a use case) 7 | pub type ActionFn = fn (context voidptr) 8 | 9 | pub type Actions = map[string]Action 10 | 11 | pub struct Action { 12 | pub mut: 13 | action_fn ActionFn = unsafe { nil } 14 | context voidptr 15 | } 16 | 17 | pub interface Actionable { 18 | id string 19 | mut: 20 | actions Actions 21 | } 22 | 23 | // TODO: documentation 24 | pub fn (mut s Actionable) add_action(action string, context voidptr, action_fn ActionFn) { 25 | s.actions[action] = Action{ 26 | context: context 27 | action_fn: action_fn 28 | } 29 | } 30 | 31 | // TODO: documentation 32 | pub fn (s &Actionable) run_action(action string) { 33 | if a := s.actions[action] { 34 | a.action_fn(a.context) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/interface_themestyle.v: -------------------------------------------------------------------------------- 1 | module ui 2 | 3 | pub interface WidgetThemeStyle { 4 | id string 5 | mut: 6 | theme_style string 7 | load_style() 8 | } 9 | 10 | // TODO: documentation 11 | pub fn (mut w WidgetThemeStyle) update_theme_style(theme_style string) { 12 | w.theme_style = theme_style 13 | } 14 | 15 | // TODO: documentation 16 | pub fn (mut l Layout) update_theme_style(theme_style string) { 17 | if mut l is WidgetThemeStyle { 18 | mut w := mut l as WidgetThemeStyle 19 | w.update_theme_style(theme_style) 20 | // println("$w.id update_theme_style load style") 21 | w.load_style() 22 | } 23 | for mut child in l.get_children() { 24 | if mut child is Layout { 25 | mut w := child as Layout 26 | w.update_theme_style(theme_style) 27 | } 28 | if mut child is WidgetThemeStyle { 29 | mut w := mut child as WidgetThemeStyle 30 | w.update_theme_style(theme_style) 31 | // println('$w.id update_theme_style load style') 32 | w.load_style() 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/textbox_input/textbox_demo.v: -------------------------------------------------------------------------------- 1 | import ui 2 | 3 | @[heap] 4 | struct App { 5 | mut: 6 | tb string 7 | soft_input_visible bool 8 | soft_input_buffer string 9 | soft_input_parsed_char string 10 | window &ui.Window = unsafe { nil } 11 | } 12 | 13 | fn main() { 14 | mut app := &App{ 15 | tb: 'Textbox example' 16 | } 17 | 18 | c := ui.column( 19 | widths: ui.stretch 20 | heights: [ui.compact, ui.stretch] 21 | margin_: 5 22 | spacing: 10 23 | children: [ 24 | ui.row( 25 | spacing: 5 26 | children: [ 27 | ui.label( 28 | text: 'Text input' //&app.tb 29 | ), 30 | ] 31 | ), 32 | ui.textbox( 33 | id: 'tb1' 34 | mode: .multiline | .word_wrap 35 | text: &app.tb 36 | // fitted_height: true 37 | ), 38 | ] 39 | ) 40 | w := ui.window( 41 | width: 500 42 | height: 300 43 | mode: .resizable 44 | children: [c] 45 | ) 46 | app.window = w 47 | ui.run(w) 48 | } 49 | -------------------------------------------------------------------------------- /src/tool_settings.v: -------------------------------------------------------------------------------- 1 | module ui 2 | 3 | import toml 4 | import os 5 | 6 | const settings_styles_dir = os.join_path_single(settings_dir, 'styles') 7 | 8 | // Tool for TOML 9 | pub fn load_settings() { 10 | } 11 | 12 | @[params] 13 | struct PrintTomlParams { 14 | title string 15 | } 16 | 17 | fn printed_toml(v toml.Any, p PrintTomlParams) string { 18 | mut out := '' 19 | am := v.as_map() 20 | for k, e in am { 21 | title := if p.title == '' { k } else { '${p.title}.${k}' } 22 | indent := [' '].repeat(title.split('.').len - 1).join('') 23 | toml_ := e.as_map().to_toml() 24 | if toml_[0..4] == '0 = ' { 25 | out += '${indent}${k} = ${e.to_toml()}\n' 26 | } else if toml_.contains('{') { 27 | // map 28 | out += printed_toml(e.as_map(), 29 | title: title 30 | ) 31 | } else { 32 | out += '${indent}[${title}]\n' 33 | mut res := '' 34 | for l in toml_.split('\n') { 35 | res += '${indent}${l}\n' 36 | } 37 | out += res 38 | } 39 | } 40 | return out 41 | } 42 | -------------------------------------------------------------------------------- /src/tool_draw.v: -------------------------------------------------------------------------------- 1 | module ui 2 | 3 | import gg 4 | 5 | pub const color_solaris = gg.hex(0xfcf4e4ff) 6 | pub const color_solaris_transparent = gg.hex(0xfcf4e4f0) 7 | 8 | // fn (tb &TextBox) draw_inner_border() { 9 | fn draw_device_inner_border(border_accentuated bool, d DrawDevice, x int, y int, width int, height int, is_error bool) { 10 | if !border_accentuated { 11 | color := if is_error { gg.rgb(255, 0, 0) } else { text_border_color } 12 | d.draw_rect_empty(x, y, width, height, color) 13 | // gg.draw_rect_empty(tb.x, tb.y, tb.width, tb.height, color) //ui.text_border_color) 14 | // TODO this should be +-1, not 0.5, a bug in gg/opengl 15 | d.draw_rect_empty(0.5 + f32(x), 0.5 + f32(y), width - 1, height - 1, text_inner_border_color) // inner lighter border 16 | } else { 17 | d.draw_rect_empty(x, y, width, height, text_border_accentuated_color) 18 | d.draw_rect_empty(1.5 + f32(x), 1.5 + f32(y), width - 3, height - 3, text_border_accentuated_color) // inner lighter border 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/rectangles_resizable.v: -------------------------------------------------------------------------------- 1 | import ui 2 | import gg 3 | 4 | const win_width = 64 * 4 + 25 5 | const win_height = 74 6 | 7 | fn main() { 8 | rect := ui.rectangle( 9 | height: 64 10 | width: 64 11 | color: gg.rgb(255, 100, 100) 12 | radius: 10 13 | text: 'Red' 14 | ) 15 | window := ui.window( 16 | width: win_width 17 | height: win_height 18 | title: 'V UI: Rectangles' 19 | mode: .max_size 20 | // on_key_down: fn(e ui.KeyEvent, wnd &ui.Window) { 21 | // println('key down') 22 | //} 23 | children: [ 24 | ui.row( 25 | alignment: .center 26 | spacing: 5 27 | margin: ui.Margin{5, 5, 5, 5} 28 | widths: ui.stretch 29 | children: [ 30 | rect, 31 | ui.rectangle(color: gg.rgb(100, 255, 100), radius: 10, text: 'Green'), 32 | ui.rectangle(color: gg.rgb(100, 100, 255), radius: 10, text: 'Blue'), 33 | ui.rectangle(color: gg.rgb(255, 100, 255), radius: 10, text: 'Pink'), 34 | ] 35 | ), 36 | ] 37 | ) 38 | ui.run(window) 39 | } 40 | -------------------------------------------------------------------------------- /examples/7guis/counter.v: -------------------------------------------------------------------------------- 1 | import ui 2 | import gg 3 | 4 | const win_width = 200 5 | const win_height = 40 6 | 7 | @[heap] 8 | struct App { 9 | mut: 10 | counter string = '0' 11 | } 12 | 13 | fn main() { 14 | mut app := &App{} 15 | window := ui.window( 16 | width: win_width 17 | height: win_height 18 | title: 'Counter' 19 | mode: .resizable 20 | layout: ui.row( 21 | spacing: 5 22 | margin_: 10 23 | widths: ui.stretch 24 | heights: ui.stretch 25 | children: [ 26 | ui.textbox( 27 | max_len: 20 28 | // height: 30 29 | read_only: true 30 | is_numeric: true 31 | text: &app.counter 32 | ), 33 | ui.button( 34 | text: 'Count' 35 | bg_color: gg.light_gray 36 | radius: 5 37 | border_color: gg.gray 38 | on_click: app.btn_click 39 | ), 40 | ] 41 | ) 42 | ) 43 | ui.run(window) 44 | } 45 | 46 | fn (mut app App) btn_click(btn &ui.Button) { 47 | app.counter = (app.counter.int() + 1).str() 48 | } 49 | -------------------------------------------------------------------------------- /examples/7guis/counter_with_closure.v: -------------------------------------------------------------------------------- 1 | import ui 2 | import gg 3 | 4 | const win_width = 200 5 | const win_height = 40 6 | 7 | @[heap] 8 | struct App { 9 | mut: 10 | counter string = '0' 11 | } 12 | 13 | fn main() { 14 | mut app := &App{} 15 | window := ui.window( 16 | width: win_width 17 | height: win_height 18 | title: 'Counter' 19 | mode: .resizable 20 | layout: ui.row( 21 | spacing: 5 22 | margin_: 10 23 | widths: ui.stretch 24 | heights: ui.stretch 25 | children: [ 26 | ui.textbox( 27 | max_len: 20 28 | // height: 30 29 | read_only: true 30 | is_numeric: true 31 | text: &app.counter 32 | ), 33 | ui.button( 34 | text: 'Count' 35 | bg_color: gg.light_gray 36 | radius: 5 37 | border_color: gg.gray 38 | on_click: fn [mut app] (btn &ui.Button) { 39 | cpt := app.counter.int() + 1 40 | app.counter = cpt.str() 41 | } 42 | ), 43 | ] 44 | ) 45 | ) 46 | ui.run(window) 47 | } 48 | -------------------------------------------------------------------------------- /examples/label_justify.v: -------------------------------------------------------------------------------- 1 | import ui 2 | import gg 3 | 4 | fn main() { 5 | layout := ui.box_layout( 6 | children: { 7 | 'rect: (0.2, 0.4) -> (0.5,0.5)': ui.rectangle( 8 | color: ui.alpha_colored(gg.yellow, 30) 9 | ) 10 | 'rect2: (0.5, 0.5) -> (1,1)': ui.rectangle( 11 | color: ui.alpha_colored(gg.blue, 30) 12 | ) 13 | 'rect3: (0.1, 0.1) -> (0.3,0.2)': ui.rectangle( 14 | color: ui.alpha_colored(gg.orange, 30) 15 | ) 16 | 'lab: (0.2, 0.4) -> (0.5,0.5)': ui.label( 17 | text: 'Centered text' 18 | justify: ui.center // [0.5, 0.5] 19 | ) 20 | 'lab2: (0.5, 0.5) -> (1,1)': ui.label( 21 | text: 'Centered text\n2nd line\n3rd line' 22 | justify: ui.top_center // [0.0, 0.5] 23 | ) 24 | 'lab3: (0.1, 0.1) -> (0.3,0.2)': ui.label( 25 | text: 'long texttttttttttttttttttttttttttttttttt' 26 | clipping: true 27 | ) 28 | } 29 | ) 30 | ui.run(ui.window( 31 | title: 'Label jusify' 32 | mode: .resizable 33 | layout: layout 34 | )) 35 | } 36 | -------------------------------------------------------------------------------- /examples/switch.v: -------------------------------------------------------------------------------- 1 | import ui 2 | 3 | const win_width = 250 4 | const win_height = 250 5 | 6 | @[heap] 7 | struct App { 8 | mut: 9 | label &ui.Label 10 | switcher &ui.Switch = unsafe { nil } 11 | window &ui.Window = unsafe { nil } 12 | } 13 | 14 | fn main() { 15 | mut app := &App{ 16 | label: ui.label(text: 'Enabled') 17 | } 18 | app.switcher = ui.switcher(open: true, on_click: app.on_switch_click) 19 | app.window = ui.window( 20 | width: win_width 21 | height: win_height 22 | title: 'Switch' 23 | mode: .resizable 24 | children: [ 25 | ui.row( 26 | alignment: .top 27 | spacing: 5 28 | margin: ui.Margin{5, 5, 5, 5} 29 | widths: ui.stretch 30 | children: [ 31 | app.label, 32 | app.switcher, 33 | ] 34 | ), 35 | ] 36 | ) 37 | ui.run(app.window) 38 | } 39 | 40 | fn (mut app App) on_switch_click(switcher &ui.Switch) { 41 | switcher_state := if switcher.open { 'Enabled' } else { 'Disabled' } 42 | app.label.set_text(switcher_state) 43 | } 44 | -------------------------------------------------------------------------------- /libvg/bitmap_text_style.v: -------------------------------------------------------------------------------- 1 | module libvg 2 | 3 | import gg 4 | import x.ttf 5 | 6 | pub struct BitmapTextStyle { 7 | pub mut: 8 | font_name string 9 | font_path string 10 | size int 11 | color gg.Color 12 | align ttf.Text_align 13 | vertical_align f32 14 | } 15 | 16 | // TODO: documentation 17 | pub fn bitmap_text_style() &BitmapTextStyle { 18 | return &BitmapTextStyle{} 19 | } 20 | 21 | // TODO: documentation 22 | pub fn (mut ts BitmapTextStyle) set_align(align int) { 23 | ts.align = match align { 24 | C.FONS_ALIGN_LEFT { .left } 25 | C.FONS_ALIGN_CENTER { .center } 26 | C.FONS_ALIGN_RIGHT { .right } 27 | else { .justify } 28 | } 29 | } 30 | 31 | // TODO: documentation 32 | pub fn (mut ts BitmapTextStyle) set_vertical_align(align int) { 33 | ts.vertical_align = match align { 34 | C.FONS_ALIGN_BOTTOM { f32(0) } 35 | C.FONS_ALIGN_TOP { f32(-0.3) } 36 | C.FONS_ALIGN_MIDDLE { f32(-0.6) } 37 | C.FONS_ALIGN_BASELINE { f32(0) } 38 | else { f32(0) } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /bin/template/vui_build.vv: -------------------------------------------------------------------------------- 1 | import ui 2 | // <>// <> 3 | 4 | // <>// <> 5 | 6 | @[heap] 7 | struct App { 8 | mut: 9 | window &ui.Window = unsafe { nil } 10 | layout &ui.Layout 11 | // <>// <> 12 | } 13 | 14 | fn (mut app App) make_root_layout() { 15 | // <>// <> 16 | app.layout = layout 17 | } 18 | 19 | fn (mut app App) make_precode() { 20 | // <>// <> 21 | } 22 | 23 | fn (mut app App) make_postcode() { 24 | // <>// <> 25 | } 26 | 27 | // <>// <> 28 | 29 | fn (mut app App) win_init(_ &ui.Window) { 30 | // <>// <> 31 | } 32 | 33 | fn main() { 34 | mut app := App{} 35 | app.make_root_layout() 36 | app.make_precode() 37 | app.window = ui.window( 38 | // <>// <> 39 | on_init: app.win_init 40 | layout: app.layout 41 | ) 42 | app.make_postcode() 43 | ui.run(app.window) 44 | } 45 | -------------------------------------------------------------------------------- /examples/component/gg2048.v: -------------------------------------------------------------------------------- 1 | import ui.component as uic 2 | import ui 3 | import gg 4 | import ui.apps.v2048 5 | 6 | fn main() { 7 | mut app := v2048.new_ui_app() 8 | mut app2 := v2048.new_ui_app() 9 | mut win := ui.window( 10 | title: '2048 inside VUI' 11 | width: 800 12 | height: 800 13 | mode: .resizable 14 | layout: ui.box_layout( 15 | children: { 16 | 'tb: (0.1, 0.1) -> (0.4,0.4)': ui.textbox( 17 | mode: .multiline 18 | id: 'edit' 19 | z_index: 20 20 | height: 200 21 | line_height_factor: 1.0 // double the line_height 22 | text_size: 24 23 | text_font_name: 'fixed' 24 | bg_color: gg.hex(0xfcf4e4ff) // gg.rgb(252, 244, 228) 25 | ) 26 | 'gg: (0.41, 0.41) -> (0.9,0.9)': uic.gg_canvaslayout( 27 | id: 'gg2048' 28 | app: app 29 | ) 30 | 'gg2: (0.1, 0.5) -> (0.45,0.9)': uic.gg_canvaslayout( 31 | id: 'gg2048bis' 32 | app: app2 33 | ) 34 | } 35 | ) 36 | ) 37 | ui.run(win) 38 | } 39 | -------------------------------------------------------------------------------- /src/layout_layer.v: -------------------------------------------------------------------------------- 1 | module ui 2 | 3 | const id_append_top_layer = '___ON_TOP_LAYER___' 4 | 5 | // CanvasLayout as Layer 6 | 7 | // Used to absolute coordinates on top (of everything) 8 | pub fn canvas_layer(c CanvasLayoutParams) &CanvasLayout { 9 | mut cl := canvas_layout(c) 10 | cl.is_root_layout = false 11 | cl.id = 'top_layer' 12 | cl.z_index = -1 13 | cl.clipping = false 14 | cl.active_evt_mngr = false 15 | cl.is_canvas_layer = true 16 | cl.update_style_params(bg_color: transparent) 17 | // println('canvas_layer $cl.id') 18 | return cl 19 | } 20 | 21 | pub fn (mut c CanvasLayout) add_top_layer(w Widget) { 22 | if c.is_canvas_layer { 23 | c.children << w 24 | c.drawing_children << w 25 | } 26 | } 27 | 28 | pub fn (mut window Window) add_top_layer(w Widget) { 29 | window.top_layer.add_top_layer(w) 30 | } 31 | 32 | // init for top layer 33 | fn (mut window Window) init_top_layer() { 34 | window.top_layer.init(window) 35 | window.top_layer.width = window.width 36 | window.top_layer.height = window.height 37 | window.top_layer.update_layout() 38 | } 39 | -------------------------------------------------------------------------------- /examples/group.v: -------------------------------------------------------------------------------- 1 | import ui 2 | 3 | const win_width = 300 4 | const win_height = 300 5 | 6 | struct App { 7 | mut: 8 | window &ui.Window = unsafe { nil } 9 | first_name string 10 | last_name string 11 | } 12 | 13 | fn main() { 14 | mut app := &App{} 15 | app.window = ui.window( 16 | width: win_width 17 | height: win_height 18 | title: 'Group Demo' 19 | children: [ 20 | ui.group( 21 | x: 20 22 | y: 20 23 | title: 'Group Demo' 24 | children: [ 25 | ui.textbox( 26 | max_len: 20 27 | width: 200 28 | placeholder: 'First name' 29 | text: &app.first_name 30 | ), 31 | ui.textbox( 32 | max_len: 50 33 | width: 200 34 | placeholder: 'Last name' 35 | text: &app.last_name 36 | ), 37 | ui.checkbox(checked: true, text: 'Online registration1'), 38 | ui.checkbox(checked: true, text: 'Online registration2'), 39 | ui.checkbox(checked: true, text: 'Online registration3'), 40 | ui.button(text: 'Add user'), 41 | ] 42 | ), 43 | ] 44 | ) 45 | ui.run(app.window) 46 | } 47 | -------------------------------------------------------------------------------- /examples/demo_calculate.v: -------------------------------------------------------------------------------- 1 | import ui 2 | import gg 3 | 4 | fn main() { 5 | win := ui.window( 6 | width: 500 7 | height: 300 8 | mode: .resizable 9 | on_init: fn (w &ui.Window) { 10 | w.calculate('11.0') 11 | } 12 | layout: ui.box_layout( 13 | children: { 14 | 'rect: (0, 25) -> (1,1)': ui.rectangle( 15 | color: gg.orange 16 | ) 17 | 'tb: (0,0) -> (100%, 25)': ui.textbox( 18 | id: 'tb' 19 | text_value: '((23.3 + 10) / 4) - 3' 20 | on_enter: fn (mut tb ui.TextBox) { 21 | mut res := ui.Widget(tb).get[ui.Label]('res') 22 | res.set_text(tb.ui.window.calculate(tb.get_text()).str()) 23 | } 24 | ) 25 | 'res: (0, 30) -> (1,1)': ui.label( 26 | id: 'res' 27 | justify: ui.center 28 | text_size: 24 29 | ) 30 | } 31 | ) 32 | ) 33 | ui.run(win) 34 | } 35 | 36 | // mut mc := tools.mini_calc() 37 | // println(mc.calculate("22.0")) 38 | // println(mc.calculate("3 + 22.0")) 39 | // println(mc.calculate("10.0 + 22/2.0")) 40 | // println(mc.calculate("22.0 / 2 + 10.0")) 41 | // println(mc.calculate("(22.0 + (13 - 5) * 4) / 2 + 10.0")) 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 The V Programming Language 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 | -------------------------------------------------------------------------------- /apps/v2048/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Delyan Angelov 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/dropdown2.v: -------------------------------------------------------------------------------- 1 | import ui 2 | import gg 3 | 4 | const win_width = 250 5 | const win_height = 250 6 | 7 | fn dd_change(dd &ui.Dropdown) { 8 | println(dd.selected().text) 9 | } 10 | 11 | fn main() { 12 | window := ui.window( 13 | width: win_width 14 | height: win_height 15 | title: 'Dropdown' 16 | children: [ 17 | ui.column( 18 | margin_: 5 19 | widths: ui.compact 20 | children: [ 21 | ui.dropdown( 22 | width: 140 23 | def_text: 'Select an option' 24 | text_color: gg.blue 25 | text_size: 20 26 | bg_color: gg.light_blue 27 | on_selection_changed: dd_change 28 | items: [ 29 | ui.DropdownItem{ 30 | text: 'Delete all users' 31 | }, 32 | ui.DropdownItem{ 33 | text: 'Export users' 34 | }, 35 | ui.DropdownItem{ 36 | text: 'Exit' 37 | }, 38 | ] 39 | ), 40 | ui.rectangle( 41 | height: 100 42 | width: 250 43 | color: gg.rgb(100, 255, 100) 44 | ), 45 | ] 46 | ), 47 | ] 48 | ) 49 | ui.run(window) 50 | } 51 | -------------------------------------------------------------------------------- /webview/webview_darwin.c.v: -------------------------------------------------------------------------------- 1 | module webview 2 | 3 | #flag darwin -framework WebKit 4 | #include "@VROOT/webview/webview_darwin.m" 5 | 6 | fn C.new_darwin_web_view(url string, title string, js_on_init string) voidptr 7 | 8 | // fn create_darwin_web_view(url string, title string) { 9 | // C.new_darwin_web_view(url, title) 10 | //} 11 | // fn C.darwin_webview_eval_js(obj voidptr, js string, result &string) string 12 | fn C.darwin_webview_eval_js(obj voidptr, js string) string 13 | 14 | fn C.darwin_webview_load(obj voidptr, url string) 15 | fn C.darwin_delete_all_cookies2(obj voidptr) 16 | 17 | fn C.darwin_webview_close() 18 | 19 | pub fn (w &WebView) eval_js(s string) { //, result &string) { 20 | C.darwin_webview_eval_js(w.obj, s) //, result) 21 | } 22 | 23 | pub fn (w &WebView) load(url string) { 24 | C.darwin_webview_load(w.obj, url) 25 | } 26 | 27 | fn C.darwin_delete_all_cookies() 28 | 29 | pub fn delete_all_cookies() { 30 | C.darwin_delete_all_cookies() 31 | } 32 | 33 | pub fn (w &WebView) delete_all_cookies() { 34 | C.darwin_delete_all_cookies2(w.obj) 35 | } 36 | 37 | fn C.darwin_get_webview_js_val() string 38 | fn C.darwin_get_webview_cookie_val() string 39 | -------------------------------------------------------------------------------- /libvg/svg_text_style.v: -------------------------------------------------------------------------------- 1 | module libvg 2 | 3 | import gg 4 | 5 | pub struct SvgTextStyle { 6 | pub mut: 7 | font_name string 8 | font_path string 9 | size int 10 | color gg.Color 11 | align string 12 | vertical_align string 13 | } 14 | 15 | // TODO: documentation 16 | pub fn svg_text_style() &SvgTextStyle { 17 | return &SvgTextStyle{} 18 | } 19 | 20 | // utility 21 | 22 | // TODO: documentation 23 | pub fn (mut ts SvgTextStyle) set_align(align int) { 24 | ts.align = match align { 25 | C.FONS_ALIGN_LEFT { 'start' } 26 | C.FONS_ALIGN_CENTER { 'middle' } 27 | C.FONS_ALIGN_RIGHT { 'end' } 28 | else { '' } 29 | } 30 | } 31 | 32 | // TODO: documentation 33 | pub fn (mut ts SvgTextStyle) set_vertical_align(align int) { 34 | ts.vertical_align = match align { 35 | C.FONS_ALIGN_BOTTOM { 'text-top' } 36 | C.FONS_ALIGN_TOP { 'hanging' } // weird 37 | C.FONS_ALIGN_MIDDLE { 'middle' } 38 | C.FONS_ALIGN_BASELINE { 'hanging' } 39 | else { '' } 40 | } 41 | } 42 | 43 | // Color (because of cycle modules copy here) 44 | pub fn hex_color(c gg.Color) string { 45 | return '#${c.r.hex()}${c.g.hex()}${c.b.hex()}${c.a.hex()}' 46 | } 47 | -------------------------------------------------------------------------------- /examples/demo_logview.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import ui 4 | import time 5 | 6 | @[heap] 7 | struct App { 8 | mut: 9 | window &ui.Window = unsafe { nil } 10 | task int 11 | log string 12 | } 13 | 14 | fn main() { 15 | mut app := &App{} 16 | app.window = ui.window( 17 | mode: .resizable 18 | height: 220 19 | layout: ui.column( 20 | widths: ui.stretch 21 | children: [ 22 | ui.textbox( 23 | id: 'tb' 24 | is_multiline: true 25 | text: &app.log 26 | height: 200 27 | is_sync: true 28 | // is_wordwrap: true 29 | // scrollview: true 30 | read_only: true 31 | // text_size: 20 32 | ), 33 | ui.button(text: 'start scan', on_click: app.btn_connect), 34 | ] 35 | ) 36 | ) 37 | ui.run(app.window) 38 | } 39 | 40 | fn (mut app App) wait_complete(mut tb ui.TextBox) { 41 | for task in 0 .. 1001 { 42 | app.log += 'processing ... task ${task} complete\n' 43 | time.sleep(500 * time.millisecond) 44 | tb.tv.do_logview() 45 | } 46 | } 47 | 48 | fn (mut app App) btn_connect(btn &ui.Button) { 49 | mut tb := app.window.get_or_panic[ui.TextBox]('tb') 50 | spawn app.wait_complete(mut tb) 51 | } 52 | -------------------------------------------------------------------------------- /component/subwindow_messagebox.v: -------------------------------------------------------------------------------- 1 | module component 2 | 3 | import ui 4 | 5 | @[params] 6 | pub struct MessageBoxSubWindowParams { 7 | pub: 8 | id string 9 | text string 10 | shortcut string = 'ctrl + h' 11 | x int = 100 12 | y int = 100 13 | width int = 400 14 | height int = 400 15 | } 16 | 17 | // Append colorbox to window 18 | pub fn messagebox_subwindow_add(mut w ui.Window, p MessageBoxSubWindowParams) { 19 | // only once 20 | if !ui.Layout(w).has_child_id(p.id) { 21 | subw := ui.subwindow( 22 | id: p.id 23 | x: p.x 24 | y: p.y 25 | layout: messagebox_stack( 26 | id: ui.component_id(p.id, 'msgbox') 27 | text: p.text 28 | width: p.width 29 | height: p.height 30 | on_click: fn (hc &MessageBoxComponent) { 31 | mut sw := hc.layout.ui.window.get_or_panic[ui.SubWindow](ui.component_parent_id(hc.id)) 32 | sw.set_visible(sw.hidden) 33 | } 34 | ) 35 | ) 36 | w.subwindows << subw 37 | mut sc := ui.Shortcutable(w) 38 | sc.add_shortcut(p.shortcut, fn (mut w ui.SubWindow) { 39 | w.set_visible(w.hidden) 40 | }) 41 | sc.add_shortcut_context(p.shortcut, subw) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /apps/v2048/2048_app_ui.v: -------------------------------------------------------------------------------- 1 | module v2048 2 | 3 | import ui 4 | import ui.component as uic 5 | 6 | @[heap] 7 | pub struct AppUI { 8 | pub mut: 9 | id string 10 | window &ui.Window = unsafe { nil } 11 | layout &ui.Layout = ui.empty_stack 12 | on_init ui.WindowFn = unsafe { nil } 13 | // s 14 | app &App = unsafe { nil } 15 | } 16 | 17 | @[params] 18 | pub struct AppUIParams { 19 | pub mut: 20 | id string = 'v2048' 21 | app &App = unsafe { nil } 22 | } 23 | 24 | pub fn new(p AppUIParams) &AppUI { 25 | mut app := &AppUI{ 26 | id: p.id 27 | } 28 | app.make_layout() 29 | return app 30 | } 31 | 32 | pub fn app(p AppUIParams) &ui.Application { 33 | app := new(p) 34 | return &ui.Application(app) 35 | } 36 | 37 | pub fn (mut app AppUI) make_layout() { 38 | app.app = new_ui_app() 39 | app.layout = uic.gg_canvaslayout( 40 | id: ui.id(app.id, 'ui_app') 41 | app: app.app 42 | ) 43 | app.on_init = fn (w &ui.Window) { 44 | // // add shortcut for hmenu 45 | // uic.hideable_add_shortcut(w, 'ctrl + o', fn [mut app] (w &ui.Window) { 46 | // uic.hideable_toggle(w, ui.id(app.id, 'hmenu')) 47 | // }) 48 | // // At first hmenu open 49 | // uic.hideable_show(w, ui.id(app.id, 'hmenu')) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /examples/change_title.v: -------------------------------------------------------------------------------- 1 | import ui 2 | 3 | const win_width = 450 4 | const win_height = 120 5 | 6 | @[heap] 7 | struct App { 8 | mut: 9 | window &ui.Window = unsafe { nil } 10 | title_box_text string 11 | } 12 | 13 | fn main() { 14 | mut app := &App{} 15 | app.window = ui.window( 16 | width: win_width 17 | height: win_height 18 | title: 'Name' 19 | children: [ 20 | ui.column( 21 | spacing: 20 22 | margin: ui.Margin{30, 30, 30, 30} 23 | // uncomment if you don't set the width of the button 24 | // widths: [ui.stretch,150] 25 | children: [ 26 | ui.row( 27 | spacing: 10 28 | alignment: .center 29 | children: [ 30 | ui.label(text: 'Title name: '), 31 | ui.textbox( 32 | max_len: 20 33 | width: 300 34 | placeholder: 'Please enter new title name' 35 | text: &app.title_box_text 36 | is_focused: true 37 | ), 38 | ] 39 | ), 40 | ui.button(text: 'Change title', on_click: app.btn_change_title, width: 150), 41 | ] 42 | ), 43 | ] 44 | ) 45 | ui.run(app.window) 46 | } 47 | 48 | fn (mut app App) btn_change_title(btn &ui.Button) { 49 | app.window.set_title(app.title_box_text) 50 | } 51 | -------------------------------------------------------------------------------- /src/ui_darwin.c.v: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2022 Alexander Medvednikov. All rights reserved. 2 | // Use of this source code is governed by a MIT license 3 | // that can be found in the LICENSE file. 4 | module ui 5 | 6 | #include "@VROOT/src/ui_darwin.m" 7 | 8 | fn C.vui_message_box(s string) 9 | 10 | fn C.vui_notify(title string, msg string) 11 | 12 | fn C.vui_wait_events() 13 | 14 | fn C.vui_bundle_path() string 15 | 16 | // fn C.vui_take_screenshot(string) 17 | 18 | fn C.vui_screenshot(voidptr, string) 19 | 20 | // fn C.darwin_draw_string(s string) 21 | pub fn message_box(s string) { 22 | C.vui_message_box(s) 23 | } 24 | 25 | pub fn notify(title string, msg string) { 26 | C.vui_notify(title, msg) 27 | } 28 | 29 | /* 30 | pub fn text_width(s string) int { 31 | return 0 32 | } 33 | */ 34 | pub fn bundle_path() string { 35 | return C.vui_bundle_path() 36 | } 37 | 38 | pub fn wait_events() { 39 | C.vui_wait_events() 40 | } 41 | 42 | fn C.sapp_macos_get_window() voidptr 43 | 44 | fn C.vui_minimize_window(voidptr) 45 | fn C.vui_deminimize_window(voidptr) 46 | fn C.vui_focus_window(voidptr) 47 | 48 | // pub fn take_snapshot(s string) { 49 | // win := sapp.macos_get_window() 50 | // // C.vui_take_screenshot( s) 51 | // C.vui_screenshot(win, s) 52 | // } 53 | -------------------------------------------------------------------------------- /examples/android/java/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 19 | 20 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/extra_text.v: -------------------------------------------------------------------------------- 1 | module ui 2 | 3 | // Initially inside ui_linux_c.v 4 | fn word_wrap_to_lines(s string, max_line_length int) []string { 5 | words := s.split(' ') 6 | mut line := []string{} 7 | mut line_len := 0 8 | mut text_lines := []string{} 9 | for word in words { 10 | word_len := word.runes().len 11 | if line_len + word_len < max_line_length { 12 | line << word 13 | line_len += word_len + 1 14 | continue 15 | } else { 16 | text_lines << line.join(' ') 17 | line = [] 18 | line_len = 0 19 | } 20 | } 21 | if line_len > 0 { 22 | text_lines << line.join(' ') 23 | } 24 | return text_lines 25 | } 26 | 27 | fn word_wrap_text_to_lines(s string, max_line_length int) []string { 28 | lines := s.split('\n') 29 | mut word_wrapped_lines := []string{} 30 | for line in lines { 31 | word_wrapped_lines << word_wrap_to_lines(line, max_line_length) 32 | } 33 | return word_wrapped_lines 34 | } 35 | 36 | fn text_lines_size(lines []string, u &UI) (int, int) { 37 | mut width, mut height := 0, 0 38 | mut tw, mut th := 0, 0 39 | dd := u.dd 40 | for line in lines { 41 | tw, th = dd.text_size(line) 42 | // println("tt line: $line -> ($tw, $th)") 43 | if tw > width { 44 | width = tw 45 | } 46 | height += th 47 | } 48 | return width, height 49 | } 50 | -------------------------------------------------------------------------------- /src/tool_coordinates.v: -------------------------------------------------------------------------------- 1 | module ui 2 | 3 | // allow to specify widgets with absolute coordinates (CanvasLayout and Window) 4 | pub fn at(x int, y int, w Widget) Widget { 5 | mut w2 := w 6 | w2.x, w2.y = x, y 7 | return w2 8 | } 9 | 10 | // on top_layer 11 | pub fn on_top_at(x int, y int, w Widget) Widget { 12 | mut w2 := w 13 | w2.x, w2.y = x, y 14 | w2.id = w2.id + id_append_top_layer // to detect 15 | return w2 16 | } 17 | 18 | fn offset_start(mut w Widget) { 19 | w.x += w.offset_x 20 | w.y += w.offset_y 21 | } 22 | 23 | fn offset_end(mut w Widget) { 24 | w.x -= w.offset_x 25 | w.y -= w.offset_y 26 | } 27 | 28 | //**** offset **** 29 | 30 | // set offset_x and offset_y for Widget 31 | pub fn set_offset(mut w Widget, ox int, oy int) { 32 | w.offset_x, w.offset_y = ox, oy 33 | if mut w is Layout { 34 | for mut child in w.get_children() { 35 | set_offset(mut child, ox, oy) 36 | } 37 | } 38 | // if mut w is Stack { 39 | // for mut child in w.children { 40 | // set_offset(mut child, ox, oy) 41 | // } 42 | //} else if mut w is Group { 43 | // for mut child in w.children { 44 | // set_offset(mut child, ox, oy) 45 | // } 46 | //} else if mut w is CanvasLayout { 47 | // for mut child in w.children { 48 | // set_offset(mut child, ox, oy) 49 | // } 50 | //} 51 | } 52 | -------------------------------------------------------------------------------- /examples/rectangles.v: -------------------------------------------------------------------------------- 1 | import ui 2 | import gg 3 | 4 | const win_width = 64 * 4 + 25 5 | const win_height = 74 6 | 7 | fn main() { 8 | rect := ui.rectangle( 9 | height: 64 10 | width: 64 11 | color: gg.rgb(255, 100, 100) 12 | ) 13 | window := ui.window( 14 | width: win_width 15 | height: win_height 16 | title: 'V UI: Rectangles' 17 | // on_key_down: fn(e ui.KeyEvent, wnd &ui.Window) { 18 | // println('key down') 19 | //} 20 | children: [ 21 | ui.row( 22 | alignment: .center 23 | spacing: 5 24 | margin: ui.Margin{5, 5, 5, 5} 25 | children: [ 26 | rect, 27 | /* 28 | { rect | color: gg.rgb(100, 255, 100), border: true, border_color: gg.black } 29 | { rect | color: gg.rgb(100, 100, 255), radius: 24 } 30 | { rect | color: gg.rgb(255, 100, 255), radius: 24, border: true, border_color: gg.black } 31 | */ 32 | ui.rectangle( 33 | height: 64 34 | width: 64 35 | color: gg.rgb(100, 255, 100) 36 | ), 37 | ui.rectangle( 38 | height: 64 39 | width: 64 40 | color: gg.rgb(100, 100, 255) 41 | ), 42 | ui.rectangle( 43 | height: 64 44 | width: 64 45 | color: gg.rgb(255, 100, 255) 46 | ), 47 | ] 48 | ), 49 | ] 50 | ) 51 | ui.run(window) 52 | } 53 | -------------------------------------------------------------------------------- /src/interface_clipping.v: -------------------------------------------------------------------------------- 1 | module ui 2 | 3 | pub interface ClippingWidget { 4 | mut: 5 | clipping bool 6 | width int 7 | height int 8 | x int 9 | y int 10 | } 11 | 12 | type ClippingState = Rect 13 | 14 | fn clipping_start(c ClippingWidget, mut d DrawDevice) !ClippingState { 15 | if c.clipping { 16 | mut x, mut y := c.x, c.y 17 | if c is ScrollableWidget { 18 | if has_scrollview(c) { 19 | x, y = c.scrollview.orig_xy() 20 | } 21 | } 22 | existing := d.get_clipping() 23 | impose := Rect{ 24 | x: x 25 | y: y 26 | w: c.width 27 | h: c.height 28 | } 29 | intersection := existing.intersection(impose) 30 | if intersection.is_empty() { 31 | return error('widget is occluded and can not be drawn') 32 | } 33 | $if clipping_start ? { 34 | if c is ScrollableWidget { 35 | println('clipping start ${c.id} ${intersection} ${existing} ${impose}') 36 | } 37 | } 38 | d.set_clipping(intersection) 39 | return existing 40 | } else { 41 | return ClippingState{} 42 | } 43 | } 44 | 45 | fn clipping_end(c ClippingWidget, mut d DrawDevice, s ClippingState) { 46 | if c.clipping { 47 | $if clipping_end ? { 48 | if c is ScrollableWidget { 49 | println('clipping end ${c.id} ${s}') 50 | } 51 | } 52 | d.set_clipping(s) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/ui_android.c.v: -------------------------------------------------------------------------------- 1 | module ui 2 | 3 | import os 4 | import sokol.sapp 5 | 6 | #include 7 | 8 | enum AndroidConfig { 9 | orientation 10 | touchscreen 11 | screensize 12 | sdkversion 13 | } 14 | 15 | fn C.AConfiguration_new() voidptr 16 | fn C.AConfiguration_fromAssetManager(voidptr, voidptr) 17 | fn C.AConfiguration_delete(voidptr) 18 | fn C.AConfiguration_getOrientation(voidptr) u32 19 | fn C.AConfiguration_getTouchscreen(voidptr) u32 20 | fn C.AConfiguration_getScreenSize(voidptr) u32 21 | fn C.AConfiguration_getSdkVersion(voidptr) u32 22 | 23 | pub fn android_config(mode AndroidConfig) u32 { 24 | config := C.AConfiguration_new() 25 | activity := &os.NativeActivity(sapp.android_get_native_activity()) 26 | C.AConfiguration_fromAssetManager(config, activity.assetManager) 27 | mut cfg := u32(0) 28 | match mode { 29 | .orientation { 30 | cfg = C.AConfiguration_getOrientation(config) 31 | } 32 | .touchscreen { 33 | cfg = C.AConfiguration_getTouchscreen(config) 34 | } 35 | .screensize { 36 | cfg = C.AConfiguration_getScreenSize(config) 37 | } 38 | .sdkversion { 39 | cfg = C.AConfiguration_getSdkVersion(config) 40 | } 41 | } 42 | C.AConfiguration_delete(config) 43 | return cfg 44 | } 45 | 46 | pub fn message_box(s string) { 47 | // TODO: Toasted message box 48 | } 49 | -------------------------------------------------------------------------------- /examples/child_window.v: -------------------------------------------------------------------------------- 1 | import ui 2 | 3 | const win_width = 450 4 | const win_height = 120 5 | 6 | @[heap] 7 | struct App { 8 | mut: 9 | window &ui.Window = unsafe { nil } 10 | title_box_text string 11 | name string 12 | } 13 | 14 | fn main() { 15 | mut app := &App{} 16 | app.window = ui.window( 17 | width: win_width 18 | height: win_height 19 | title: 'Child window' 20 | children: [ 21 | ui.column( 22 | margin_: 10 23 | children: [ 24 | ui.button(text: 'Create a window', on_click: app.btn_click, width: 150), 25 | ui.textbox(placeholder: 'Test textbox'), 26 | ] 27 | ), 28 | ] 29 | ) 30 | ui.run(app.window) 31 | } 32 | 33 | fn (mut app App) btn_click(btn &ui.Button) { 34 | app.window.child_window( 35 | children: [ 36 | ui.column( 37 | margin_: 10 38 | spacing: 5 39 | children: [ 40 | ui.textbox(placeholder: 'Name', text: &app.name), 41 | ui.checkbox(id: 'cb_genre', text: 'Check me if woman'), 42 | ui.button(text: 'Greet me', on_click: app.btn_greet_click, width: 150), 43 | ] 44 | ), 45 | ] 46 | ) 47 | } 48 | 49 | fn (mut app App) btn_greet_click(btn &ui.Button) { 50 | genre := if btn.ui.window.get_or_panic[ui.CheckBox]('cb_genre').checked { 51 | 'miss' 52 | } else { 53 | 'mister' 54 | } 55 | ui.message_box('Hello, ${genre} ${app.name}!') 56 | } 57 | -------------------------------------------------------------------------------- /src/tool_rect.v: -------------------------------------------------------------------------------- 1 | module ui 2 | 3 | import math 4 | 5 | struct Rect { 6 | pub mut: 7 | x int 8 | y int 9 | w int 10 | h int 11 | } 12 | 13 | // return the ui.Rect which is the intersection of this and the other ui.Rect 14 | pub fn (r Rect) intersection(o Rect) Rect { 15 | // top left and bottom right points 16 | x1, y1 := math.max(r.x, o.x), math.max(r.y, o.y) 17 | x2, y2 := math.min(r.x + r.w, o.x + o.w), math.min(r.y + r.h, o.y + o.h) 18 | // intersection 19 | return Rect{x1, y1, math.max(0, x2 - x1), math.max(0, y2 - y1)} 20 | } 21 | 22 | // test if this ui.Rect is empty 23 | pub fn (r Rect) is_empty() bool { 24 | return r.w <= 0 || r.h <= 0 25 | } 26 | 27 | // return the smallest ui.Rect which contains both this and the other ui.Rect 28 | pub fn (r Rect) combine(o Rect) Rect { 29 | if o.is_empty() { 30 | return r 31 | } 32 | if r.is_empty() { 33 | return o 34 | } 35 | // top left and bottom right points 36 | x1, y1 := math.min(r.x, o.x), math.min(r.y, o.y) 37 | x2, y2 := math.max(r.x + r.w, o.x + o.w), math.max(r.y + r.h, o.y + o.h) 38 | // smallest containing rect 39 | return Rect{x1, y1, math.max(0, x2 - x1), math.max(0, y2 - y1)} 40 | } 41 | 42 | // returns true if this ui.Rect contains the other ui.Rect 43 | pub fn (r Rect) contains_rect(o Rect) bool { 44 | return r.x <= o.x && r.y <= o.y && r.x + r.w >= o.x + o.w && r.y + r.h >= o.y + o.h 45 | } 46 | -------------------------------------------------------------------------------- /.github/workflows/macos.yml: -------------------------------------------------------------------------------- 1 | name: macOS CI 2 | 3 | on: 4 | workflow_call: 5 | 6 | env: 7 | # Path where the module is installed for usage as V module. 8 | MOD_PATH: $HOME/.vmodules/ui 9 | 10 | jobs: 11 | setup: 12 | runs-on: macos-latest 13 | steps: 14 | - name: Setup V 15 | uses: vlang/setup-v@v1.3 16 | with: 17 | check-latest: true 18 | - uses: actions/checkout@v4 19 | with: 20 | path: ui 21 | - name: Setup V UI module 22 | run: mv ui ${{ env.MOD_PATH }} 23 | - name: Cache 24 | uses: actions/cache/save@v3 25 | with: 26 | path: | 27 | vlang 28 | ~/.vmodules 29 | key: ${{ runner.os }}-${{ github.sha }} 30 | 31 | build: 32 | needs: setup 33 | runs-on: macos-latest 34 | steps: 35 | - name: Restore V cache 36 | uses: actions/cache/restore@v3 37 | with: 38 | path: | 39 | vlang 40 | ~/.vmodules 41 | key: ${{ runner.os }}-${{ github.sha }} 42 | fail-on-cache-miss: true 43 | - name: Setup V 44 | uses: vlang/setup-v@v1.3 45 | with: 46 | check-latest: true 47 | - name: Build UI examples 48 | run: VJOBS=20 v run ${{ env.MOD_PATH }}/examples/build_examples.vsh 49 | - name: Build users.v with -prod 50 | run: VJOBS=20 v -prod ${{ env.MOD_PATH }}/examples/users.v 51 | -------------------------------------------------------------------------------- /component/settings.v: -------------------------------------------------------------------------------- 1 | module component 2 | 3 | import ui 4 | // import gg 5 | 6 | // TODO: documentation 7 | pub fn setting_color(param string) { 8 | } 9 | 10 | pub struct SettingFont { 11 | param string 12 | lb_text string 13 | mut: 14 | layout &ui.Stack = unsafe { nil } 15 | lb_param &ui.Label = unsafe { nil } 16 | lb_font &ui.Label = unsafe { nil } 17 | btn_font &ui.Button = unsafe { nil } 18 | } 19 | 20 | @[params] 21 | pub struct SettingFontParams { 22 | pub: 23 | id string 24 | param string 25 | text string 26 | } 27 | 28 | // TODO: documentation 29 | pub fn setting_font(s SettingFontParams) &ui.Stack { 30 | lb_param := ui.label(text: s.text) 31 | lb_font := ui.label(text: s.id) 32 | btn_font := fontbutton(text: 'font', dtw: lb_font) 33 | layout := ui.row( 34 | widths: [100.0, 100, 20] 35 | heights: 20.0 36 | children: [lb_param, lb_font, btn_font] 37 | ) 38 | sf := &SettingFont{ 39 | layout: layout 40 | lb_param: lb_param 41 | lb_font: lb_font 42 | btn_font: btn_font 43 | } 44 | ui.component_connect(sf, layout, lb_param, lb_font) 45 | return layout 46 | } 47 | 48 | // TODO: documentation 49 | pub fn setting_int(param string) &ui.Stack { 50 | return ui.row() 51 | } 52 | 53 | // TODO: documentation 54 | pub fn setting_f32(param string) &ui.Stack { 55 | return ui.row() 56 | } 57 | 58 | // TODO: documentation 59 | pub fn settings_bool(param string) &ui.Stack { 60 | return ui.row() 61 | } 62 | -------------------------------------------------------------------------------- /examples/slider.v: -------------------------------------------------------------------------------- 1 | import ui 2 | 3 | const win_width = 300 4 | const win_height = 250 5 | 6 | @[heap] 7 | struct App { 8 | mut: 9 | hor_slider &ui.Slider = unsafe { nil } 10 | vert_slider &ui.Slider = unsafe { nil } 11 | window &ui.Window = unsafe { nil } 12 | } 13 | 14 | fn main() { 15 | mut app := &App{} 16 | app.hor_slider = ui.slider( 17 | width: 200 18 | height: 20 19 | orientation: .horizontal 20 | max: 100 21 | val: 0 22 | on_value_changed: app.on_hor_value_changed 23 | ) 24 | app.vert_slider = ui.slider( 25 | width: 20 26 | height: 200 27 | orientation: .vertical 28 | max: 100 29 | val: 0 30 | on_value_changed: app.on_vert_value_changed 31 | ) 32 | app.window = ui.window( 33 | width: win_width 34 | height: win_height 35 | title: 'Slider Example' 36 | children: [ 37 | ui.row( 38 | alignment: .center 39 | widths: [.1, .9] 40 | heights: [.9, .1] 41 | margin: ui.Margin{25, 25, 25, 25} 42 | spacing: 10 43 | children: [app.vert_slider, app.hor_slider] 44 | ), 45 | ] 46 | ) 47 | ui.run(app.window) 48 | } 49 | 50 | fn (mut app App) on_hor_value_changed(slider &ui.Slider) { 51 | app.hor_slider.val = app.hor_slider.val 52 | } 53 | 54 | fn (mut app App) on_vert_value_changed(slider &ui.Slider) { 55 | app.vert_slider.val = app.vert_slider.val 56 | } 57 | -------------------------------------------------------------------------------- /examples/7guis/cells.v: -------------------------------------------------------------------------------- 1 | import ui 2 | import ui.component as uic 3 | 4 | fn main() { 5 | // A 6 | mut vars := { 7 | 'A': uic.GridData([''].repeat(100)) 8 | } 9 | // from B to Z 10 | for i in 66 .. (66 + 25) { 11 | vars[[u8(i)].bytestr()] = uic.GridData([''].repeat(100)) 12 | } 13 | 14 | // Init some values 15 | mut v_a := vars['A'] or { []string{} } 16 | if mut v_a is []string { 17 | v_a[0] = 'Sum B2:C5 = ' 18 | } 19 | mut v_b := vars['B'] or { []string{} } 20 | if mut v_b is []string { 21 | v_b[1] = '12' 22 | v_b[2] = '1' 23 | v_b[3] = '1' 24 | v_b[4] = '23' 25 | } 26 | mut v_c := vars['C'] or { []string{} } 27 | if mut v_c is []string { 28 | v_c[1] = '13' 29 | v_c[2] = '-1' 30 | v_c[3] = '31' 31 | } 32 | mut v_d := vars['D'] or { []string{} } 33 | if mut v_d is []string { 34 | v_d[1] = '3' 35 | v_d[2] = '10' 36 | v_d[3] = '1' 37 | v_d[4] = '24' 38 | } 39 | window := ui.window( 40 | width: 600 41 | height: 400 42 | title: 'Cells' 43 | mode: .resizable 44 | layout: ui.row( 45 | spacing: 5 46 | margin_: 10 47 | widths: ui.stretch 48 | heights: ui.stretch 49 | children: [ 50 | uic.datagrid_stack( 51 | id: 'dgs' 52 | vars: vars 53 | formulas: { 54 | 'B1': '=sum(B2:C5, D2)' 55 | 'C5': '=sum(D2:D5)' 56 | 'A4': '=sum(B4:D4)' 57 | } 58 | is_focused: true 59 | ), 60 | ] 61 | ) 62 | ) 63 | ui.run(window) 64 | } 65 | -------------------------------------------------------------------------------- /.github/workflows/windows.yml: -------------------------------------------------------------------------------- 1 | name: Windows CI 2 | 3 | on: 4 | workflow_call: 5 | 6 | env: 7 | # Path where the module is installed for usage as V module. 8 | MOD_PATH: $HOME/.vmodules/ui 9 | 10 | jobs: 11 | setup: 12 | runs-on: windows-latest 13 | steps: 14 | - name: Setup V 15 | uses: vlang/setup-v@v1.3 16 | with: 17 | check-latest: true 18 | - uses: actions/checkout@v4 19 | with: 20 | path: ui 21 | - name: Setup V UI module 22 | shell: bash 23 | run: mv ui ${{ env.MOD_PATH }} 24 | - name: Cache 25 | uses: actions/cache/save@v3 26 | with: 27 | path: | 28 | vlang 29 | ~/.vmodules 30 | key: ${{ runner.os }}-${{ github.sha }} 31 | 32 | build: 33 | needs: setup 34 | runs-on: windows-latest 35 | steps: 36 | - name: Restore V cache 37 | uses: actions/cache/restore@v3 38 | with: 39 | path: | 40 | vlang 41 | ~/.vmodules 42 | key: ${{ runner.os }}-${{ github.sha }} 43 | fail-on-cache-miss: true 44 | - name: Setup V 45 | uses: vlang/setup-v@v1.3 46 | with: 47 | check-latest: true 48 | - name: Build UI examples 49 | run: v run ${{ env.MOD_PATH }}/examples/build_examples.vsh 50 | - name: Build users.v with -prod 51 | run: v -prod ${{ env.MOD_PATH }}/examples/users.v 52 | -------------------------------------------------------------------------------- /component/subwindow_colorbox.v: -------------------------------------------------------------------------------- 1 | module component 2 | 3 | import ui 4 | import gg 5 | 6 | const colorbox_subwindow_id = '_sw_cbox' 7 | const colorbox_subwindow_layout_id = ui.component_id('_sw_cbox', 'layout') 8 | 9 | // Append colorbox to window 10 | pub fn colorbox_subwindow_add(mut w ui.Window) { 11 | // only once 12 | if !ui.Layout(w).has_child_id(colorbox_subwindow_id) { 13 | w.subwindows << ui.subwindow( 14 | id: colorbox_subwindow_id 15 | layout: colorbox_stack(id: colorbox_subwindow_id, light: false, hsl: false) 16 | ) 17 | } 18 | } 19 | 20 | pub enum ShowMode { 21 | show 22 | hide 23 | toggle 24 | } 25 | 26 | // to connect the colorbox to gg.Color reference 27 | pub fn colorbox_subwindow_connect(w &ui.Window, col &gg.Color, colbtn &ColorButtonComponent, show ShowMode) { 28 | mut s := w.get_or_panic[ui.SubWindow](colorbox_subwindow_id) 29 | cb_layout := w.get_or_panic[ui.Stack](colorbox_subwindow_layout_id) 30 | mut cb := colorbox_component(cb_layout) 31 | if unsafe { col != 0 } { 32 | cb.connect(col) 33 | cb.update_from_rgb(col.r, col.g, col.b) 34 | cb.update_cur_color(true) 35 | } 36 | // connect also the colbtn of cb 37 | if unsafe { colbtn != 0 } { 38 | // println("connect ${colbtn.widget.id} ${colbtn.on_changed != ColorButtonChangedFn(0)}") 39 | cb.connect_colorbutton(colbtn) 40 | } 41 | s.set_visible(match show { 42 | .toggle { s.hidden } 43 | .show { true } 44 | .hide { false } 45 | }) 46 | s.update_layout() 47 | } 48 | -------------------------------------------------------------------------------- /examples/component/rgb_color.v: -------------------------------------------------------------------------------- 1 | import ui 2 | import ui.component as uic 3 | import gg 4 | 5 | const win_width = 200 6 | const win_height = 400 7 | 8 | fn main() { 9 | mut orientation := ui.Orientation.vertical 10 | $if horiz ? { 11 | orientation = .horizontal 12 | } 13 | color := gg.rgb(128, 128, 128) 14 | rect := ui.rectangle( 15 | id: 'rgb_rect' 16 | border: true 17 | color: color 18 | ) 19 | window := ui.window( 20 | width: win_width 21 | height: win_height 22 | title: 'RGB color displayed in rectangle' 23 | mode: .resizable 24 | layout: ui.column( 25 | margin_: 10 26 | spacing: 5 27 | heights: [ui.stretch, 2 * ui.stretch, 7 * ui.stretch] 28 | children: [ 29 | ui.button( 30 | id: 'rgb_btn' 31 | text: 'Show rgb color' 32 | on_click: btn_click 33 | ), 34 | rect, 35 | uic.colorsliders_stack( 36 | id: 'colorsliders' 37 | color: color 38 | orientation: orientation 39 | on_changed: on_rgb_changed 40 | ), 41 | ] 42 | ) 43 | ) 44 | ui.run(window) 45 | } 46 | 47 | fn btn_click(b &ui.Button) { 48 | cs := uic.colorsliders_component_from_id(b.ui.window, 'colorsliders') 49 | txt := 'gg.rgb(${cs.r_textbox_text},${cs.g_textbox_text},${cs.b_textbox_text})' 50 | ui.message_box(txt) 51 | } 52 | 53 | fn on_rgb_changed(cs &uic.ColorSlidersComponent) { 54 | mut rect := cs.layout.ui.window.get_or_panic[ui.Rectangle]('rgb_rect') 55 | rect.style.color = cs.color() 56 | } 57 | -------------------------------------------------------------------------------- /examples/component/double_listbox.v: -------------------------------------------------------------------------------- 1 | import ui 2 | import ui.component as uic 3 | 4 | const win_width = 600 5 | const win_height = 400 6 | 7 | fn main() { 8 | window := ui.window( 9 | width: win_width 10 | height: win_height 11 | title: 'V UI: Composable Widget' 12 | mode: .resizable 13 | native_message: false 14 | layout: ui.column( 15 | margin_: .05 16 | spacing: .05 17 | heights: [8 * ui.stretch, ui.stretch, ui.stretch] 18 | children: [ 19 | ui.row( 20 | spacing: .1 21 | margin_: 5 22 | widths: ui.stretch 23 | children: [ 24 | uic.doublelistbox_stack( 25 | id: 'dlb1' 26 | title: 'dlb1' 27 | items: [ 28 | 'totto', 29 | 'titi', 30 | ] 31 | ), 32 | uic.doublelistbox_stack( 33 | id: 'dlb2' 34 | title: 'dlb2' 35 | items: [ 36 | 'tottoooo', 37 | 'titi', 38 | 'tototta', 39 | ] 40 | ), 41 | ] 42 | ), 43 | ui.button(id: 'btn1', text: 'get values for dlb1', on_click: btn_click), 44 | ui.button(id: 'btn2', text: 'get values for dlb2', on_click: btn_click), 45 | ] 46 | ) 47 | ) 48 | ui.run(window) 49 | } 50 | 51 | fn btn_click(b &ui.Button) { 52 | dlb := uic.doublelistbox_component_from_id(b.ui.window, if b.id == 'btn1' { 53 | 'dlb1' 54 | } else { 55 | 'dlb2' 56 | }) 57 | res := 'result(s) : ${dlb.values()}' 58 | println(res) 59 | b.ui.window.message(res) 60 | } 61 | -------------------------------------------------------------------------------- /src/window_style.v: -------------------------------------------------------------------------------- 1 | module ui 2 | 3 | import gg 4 | import toml 5 | 6 | // Window 7 | 8 | pub struct WindowStyle { 9 | pub mut: 10 | bg_color gg.Color 11 | } 12 | 13 | @[params] 14 | pub struct WindowStyleParams { 15 | mut: 16 | style string = no_style 17 | bg_color gg.Color = no_color 18 | } 19 | 20 | pub fn (w WindowStyle) to_toml() string { 21 | mut toml_ := map[string]toml.Any{} 22 | toml_['bg_color'] = hex_color(w.bg_color) 23 | return toml_.to_toml() 24 | } 25 | 26 | pub fn (mut w WindowStyle) from_toml(a toml.Any) { 27 | w.bg_color = HexColor(a.value('bg_color').string()).color() 28 | } 29 | 30 | pub fn (mut w Window) load_style() { 31 | mut style := w.theme_style 32 | if w.style_params.style != no_style { 33 | style = w.style_params.style 34 | } 35 | w.update_theme_style(style) 36 | // println("w bg: $w.bg_color") 37 | w.update_style(w.style_params) 38 | // println("w2 bg: $w.bg_color") 39 | mut gui := w.ui 40 | gui.dd.set_bg_color(w.bg_color) 41 | // mut l := Layout(w) 42 | // l.update_theme_style(style) 43 | } 44 | 45 | pub fn (mut w Window) update_theme_style(theme string) { 46 | // println("update_style <$p.style>") 47 | style := if theme == '' { 'default' } else { theme } 48 | if style != no_style && style in w.ui.styles { 49 | ws := w.ui.styles[style].win 50 | w.theme_style = theme 51 | w.bg_color = ws.bg_color 52 | } 53 | } 54 | 55 | fn (mut w Window) update_style(p WindowStyleParams) { 56 | if p.bg_color != no_color { 57 | w.bg_color = p.bg_color 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /examples/demo_style_4colors.v: -------------------------------------------------------------------------------- 1 | import ui 2 | import ui.component as uic 3 | import gg 4 | 5 | fn main() { 6 | mut win := ui.window( 7 | title: 'Four colors' 8 | mode: .resizable 9 | on_init: win_init 10 | height: 600 11 | layout: ui.column( 12 | heights: [100.0, ui.stretch] 13 | children: [ 14 | ui.row( 15 | widths: ui.stretch 16 | children: [ 17 | uic.colorbutton( 18 | id: 'color0' 19 | on_changed: on_changed 20 | ), 21 | uic.colorbutton( 22 | id: 'color1' 23 | on_changed: on_changed 24 | ), 25 | uic.colorbutton( 26 | id: 'color2' 27 | on_changed: on_changed 28 | ), 29 | uic.colorbutton( 30 | id: 'color3' 31 | on_changed: on_changed 32 | ), 33 | ] 34 | ), 35 | uic.demo_stack(), 36 | ] 37 | ) 38 | ) 39 | uic.colorbox_subwindow_add(mut win) 40 | ui.run(win) 41 | } 42 | 43 | fn on_changed(mut cbc uic.ColorButtonComponent) { 44 | mut gui := cbc.widget.ui 45 | i := cbc.widget.id[5..].int() 46 | // println("$cbc.widget.id changed -> $i") 47 | // println(gui.style_colors) 48 | gui.style_colors[i] = cbc.bg_color 49 | gui.window.load_4colors_style(gui.style_colors) 50 | } 51 | 52 | fn win_init(w &ui.Window) { 53 | mut gui := w.ui 54 | gui.window.load_4colors_style([gg.white, gg.light_gray, gg.light_blue, gg.black]) 55 | for i in 0 .. 4 { 56 | mut cbc := uic.colorbutton_component_from_id(w, 'color${i}') 57 | cbc.bg_color = gui.style_colors[i] 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /examples/component/grid.v: -------------------------------------------------------------------------------- 1 | import ui 2 | import ui.component as uic 3 | import gg 4 | 5 | const win_width = 600 6 | const win_height = 600 7 | 8 | fn main() { 9 | n := 1000000 10 | window := ui.window( 11 | width: win_width 12 | height: win_height 13 | title: 'V UI: Grid' 14 | native_message: false 15 | mode: .resizable 16 | bg_color: gg.white 17 | on_init: win_init 18 | layout: uic.datagrid_stack( 19 | id: 'grid' 20 | is_focused: true 21 | vars: { 22 | 'v1': ['toto', 'titi', 'tata'].repeat(n) 23 | 'v2': ['toti', 'tito', 'tato'].repeat(n) 24 | 'sex': uic.Factor{ 25 | levels: ['Male', 'Female'] 26 | values: [0, 0, 1].repeat(n) 27 | } 28 | 'csp': uic.Factor{ 29 | levels: ['job1', 'job2', 'other'] 30 | values: [0, 1, 2].repeat(n) 31 | } 32 | 'v3': ['toto', 'titi', 'tata'].repeat(n) 33 | 'v4': ['toti', 'tito', 'tato'].repeat(n) 34 | 'sex2': uic.Factor{ 35 | levels: ['Male', 'Female'] 36 | values: [0, 0, 1].repeat(n) 37 | } 38 | 'csp2': uic.Factor{ 39 | levels: ['job1', 'job2', 'other'] 40 | values: [0, 1, 2].repeat(n) 41 | } 42 | } 43 | ) 44 | ) 45 | ui.run(window) 46 | } 47 | 48 | fn win_init(w &ui.Window) { 49 | // mut g := uic.grid_component_from_id(w, "grid") 50 | // g.init_ranked_grid_data([2, 0], [1, -1]) 51 | 52 | // mut gs := uic.gridsettings_component_from_id(w, "gs") 53 | // println("gs id: <$gs.id> ${typeof(gs).name} $gsl.id") 54 | // gs.update_sorted_vars() 55 | } 56 | -------------------------------------------------------------------------------- /.github/workflows/linux.yml: -------------------------------------------------------------------------------- 1 | name: Linux CI 2 | 3 | on: 4 | workflow_call: 5 | 6 | env: 7 | # Path where the module is installed for usage as V module. 8 | MOD_PATH: $HOME/.vmodules/ui 9 | 10 | jobs: 11 | setup: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Setup V 15 | uses: vlang/setup-v@v1.3 16 | with: 17 | check-latest: true 18 | - uses: actions/checkout@v4 19 | with: 20 | path: ui 21 | - name: Setup V UI module 22 | run: mv ui ${{ env.MOD_PATH }} 23 | - name: Cache 24 | uses: actions/cache/save@v3 25 | with: 26 | path: | 27 | vlang 28 | ~/.vmodules 29 | key: ${{ runner.os }}-${{ github.sha }} 30 | 31 | build: 32 | needs: setup 33 | runs-on: ubuntu-latest 34 | steps: 35 | - name: Restore V cache 36 | uses: actions/cache/restore@v3 37 | with: 38 | path: | 39 | vlang 40 | ~/.vmodules 41 | key: ${{ runner.os }}-${{ github.sha }} 42 | fail-on-cache-miss: true 43 | - name: Setup V 44 | uses: vlang/setup-v@v1.3 45 | with: 46 | check-latest: true 47 | - name: Install dependencies 48 | run: sudo apt update && sudo apt install --quiet -y libglfw3-dev libxi-dev libxcursor-dev 49 | - name: Build UI examples 50 | run: VJOBS=20 v run ${{ env.MOD_PATH }}/examples/build_examples.vsh 51 | - name: Build users.v with -prod 52 | run: VJOBS=20 v -prod ${{ env.MOD_PATH }}/examples/users.v 53 | -------------------------------------------------------------------------------- /examples/component/grid_boxlayout.v: -------------------------------------------------------------------------------- 1 | import ui 2 | import ui.component as uic 3 | import gg 4 | 5 | const win_width = 600 6 | const win_height = 600 7 | 8 | fn main() { 9 | n := 1000000 10 | window := ui.window( 11 | width: win_width 12 | height: win_height 13 | title: 'V UI: Grid' 14 | native_message: false 15 | mode: .resizable 16 | bg_color: gg.white 17 | on_init: win_init 18 | layout: uic.datagrid_boxlayout( 19 | id: 'grid' 20 | is_focused: true 21 | vars: { 22 | 'v1': ['toto', 'titi', 'tata'].repeat(n) 23 | 'v2': ['toti', 'tito', 'tato'].repeat(n) 24 | 'sex': uic.Factor{ 25 | levels: ['Male', 'Female'] 26 | values: [0, 0, 1].repeat(n) 27 | } 28 | 'csp': uic.Factor{ 29 | levels: ['job1', 'job2', 'other'] 30 | values: [0, 1, 2].repeat(n) 31 | } 32 | 'v3': ['toto', 'titi', 'tata'].repeat(n) 33 | 'v4': ['toti', 'tito', 'tato'].repeat(n) 34 | 'sex2': uic.Factor{ 35 | levels: ['Male', 'Female'] 36 | values: [0, 0, 1].repeat(n) 37 | } 38 | 'csp2': uic.Factor{ 39 | levels: ['job1', 'job2', 'other'] 40 | values: [0, 1, 2].repeat(n) 41 | } 42 | } 43 | ) 44 | ) 45 | ui.run(window) 46 | } 47 | 48 | fn win_init(w &ui.Window) { 49 | // mut g := uic.grid_component_from_id(w, "grid") 50 | // g.init_ranked_grid_data([2, 0], [1, -1]) 51 | 52 | // mut gs := uic.gridsettings_component_from_id(w, "gs") 53 | // println("gs id: <$gs.id> ${typeof(gs).name} $gsl.id") 54 | // gs.update_sorted_vars() 55 | } 56 | -------------------------------------------------------------------------------- /src/interface_adjustable.v: -------------------------------------------------------------------------------- 1 | module ui 2 | 3 | import math 4 | 5 | pub interface AdjustableWidget { 6 | size() (int, int) 7 | mut: 8 | id string 9 | justify []f64 // 0.0 means left, 0.5 center and 1.0 right (0.25 means 1/4 of the free space between size and adjusted size) 10 | x int 11 | y int 12 | ax int // offset for adjusted x 13 | ay int // offset for adjusted x 14 | set_pos(int, int) 15 | adj_size() (int, int) 16 | } 17 | 18 | // TODO: documentation 19 | pub fn (mut w AdjustableWidget) get_align_offset(aw f64, ah f64) (int, int) { 20 | width, height := w.size() 21 | adj_width, adj_height := w.adj_size() 22 | $if aw_gao ? { 23 | if w.id in env('UI_IDS').split(',') { 24 | println('aw gao: ${w.id} (${width}, ${height}) vs (${adj_width}, ${adj_height})') 25 | } 26 | } 27 | dw := math.max(width - adj_width, 0) 28 | dh := math.max(height - adj_height, 0) 29 | return int(aw * dw), int(ah * dh) 30 | } 31 | 32 | fn (mut w AdjustableWidget) set_adjusted_pos(x int, y int) { 33 | w.ax, w.ay = w.get_align_offset(w.justify[0], w.justify[1]) 34 | w.ax += x 35 | w.ay += y 36 | w.set_pos(x, y) 37 | } 38 | 39 | fn (w &AdjustableWidget) get_adjusted_pos() (int, int) { 40 | return w.ax, w.ay 41 | } 42 | 43 | pub const top_left = [0.0, 0.0] 44 | pub const top_center = [0.5, 0.0] 45 | pub const top_right = [1.0, 0.0] 46 | pub const center_left = [0.0, 0.5] 47 | pub const center = [0.5, 0.5] 48 | pub const center_center = [0.5, 0.5] 49 | pub const center_right = [1.0, 0.5] 50 | pub const bottom_left = [0.0, 1.0] 51 | pub const bottom_center = [0.5, 1.0] 52 | pub const bottom_right = [1.0, 1.0] 53 | -------------------------------------------------------------------------------- /examples/layout/box_layout_with_textbox.v: -------------------------------------------------------------------------------- 1 | import ui 2 | import gg 3 | 4 | const win_width = 400 5 | const win_height = 300 6 | 7 | struct App { 8 | mut: 9 | text string 10 | btn_cb map[string]fn (&ui.Button) 11 | } 12 | 13 | fn make_tb(mut app App, has_row bool) ui.Widget { 14 | tb := ui.textbox( 15 | mode: .multiline 16 | bg_color: gg.yellow 17 | text: &app.text 18 | ) 19 | return if has_row { 20 | ui.Widget(ui.row( 21 | widths: ui.stretch 22 | children: [ 23 | tb, 24 | ] 25 | )) 26 | } else { 27 | ui.Widget(tb) 28 | } 29 | } 30 | 31 | fn (mut app App) make_btn() ui.Widget { 32 | app.btn_cb['btn_click'] = fn (_ &ui.Button) { 33 | ui.message_box('coucou toto!') 34 | } 35 | return ui.button( 36 | text: 'toto' 37 | on_click: app.btn_cb['btn_click'] 38 | ) 39 | } 40 | 41 | fn main() { 42 | mut with_row := false 43 | $if with_row ? { 44 | with_row = true 45 | } 46 | mut app := App{ 47 | text: 'blah blah blah\n'.repeat(10) 48 | } 49 | ui.run(ui.window( 50 | width: win_width 51 | height: win_height 52 | title: 'V UI: Rectangles inside BoxLayout' 53 | mode: .resizable 54 | layout: ui.box_layout( 55 | id: 'bl' 56 | children: { 57 | 'id1: (0,0) ++ (30,30)': ui.rectangle( 58 | color: gg.rgb(255, 100, 100) 59 | ) 60 | 'id2: (30,30) -> (-30.5,-30.5)': ui.rectangle( 61 | color: gg.rgb(100, 255, 100) 62 | ) 63 | 'id3: (50%,50%) -> (100%,100%)': make_tb(mut app, with_row) 64 | 'id4: (-30.5, -30.5) ++ (30,30)': ui.rectangle( 65 | color: gg.white 66 | ) 67 | 'id5: (70%,20%) ++ (50,20)': app.make_btn() 68 | } 69 | ) 70 | )) 71 | } 72 | -------------------------------------------------------------------------------- /examples/component/treeview.v: -------------------------------------------------------------------------------- 1 | import ui 2 | import ui.component as uic 3 | import gg 4 | 5 | const win_width = 800 6 | const win_height = 600 7 | 8 | fn main() { 9 | window := ui.window( 10 | width: win_width 11 | height: win_height 12 | title: 'V UI: TreeView' 13 | native_message: false 14 | mode: .resizable 15 | layout: ui.column( 16 | scrollview: true 17 | heights: ui.compact 18 | children: [ 19 | uic.treeview_stack( 20 | id: 'demo' 21 | incr_mode: true 22 | trees: [ 23 | uic.Tree{ 24 | title: 'toto1' 25 | items: [ 26 | uic.TreeItem('file: ftftyty1'), 27 | 'file: hgyfyf1', 28 | uic.Tree{ 29 | title: 'tttytyty1' 30 | items: [ 31 | uic.TreeItem('file: tutu2'), 32 | 'file: ytytyy2', 33 | ] 34 | }, 35 | ] 36 | }, 37 | uic.Tree{ 38 | title: 'toto2' 39 | items: [ 40 | uic.TreeItem('file: ftftyty1'), 41 | 'file: hgyfyf1111', 42 | ] 43 | }, 44 | uic.Tree{ 45 | title: 'toto3' 46 | items: [ 47 | uic.TreeItem('file: ftftyty2'), 48 | 'file: hgyfyf2222', 49 | ] 50 | }, 51 | ] 52 | icons: { 53 | 'folder': 'tata' 54 | 'file': 'toto' 55 | } 56 | text_color: gg.blue 57 | on_click: treeview_on_click 58 | ), 59 | ] 60 | ) 61 | ) 62 | ui.run(window) 63 | } 64 | 65 | fn treeview_on_click(c &ui.CanvasLayout, mut tv uic.TreeViewComponent) { 66 | selected := c.id 67 | println('${selected} selected with title: ${tv.titles[selected]}!') 68 | } 69 | -------------------------------------------------------------------------------- /examples/demo_radio.v: -------------------------------------------------------------------------------- 1 | import ui 2 | 3 | fn main() { 4 | c := ui.column( 5 | widths: ui.stretch 6 | margin_: 5 7 | spacing: 10 8 | children: [ 9 | ui.row( 10 | spacing: 5 11 | children: [ 12 | ui.label(text: 'Compact'), 13 | ui.switcher(open: true, on_click: on_switch_click), 14 | ] 15 | ), 16 | ui.radio( 17 | id: 'rh1' 18 | horizontal: true 19 | compact: true 20 | values: [ 21 | 'United States', 22 | 'Canada', 23 | 'United Kingdom', 24 | 'Australia', 25 | ] 26 | title: 'Country' 27 | ), 28 | ui.radio( 29 | values: [ 30 | 'United States', 31 | 'Canada', 32 | 'United Kingdom', 33 | 'Australia', 34 | ] 35 | title: 'Country' 36 | ), 37 | ui.row( 38 | widths: [ 39 | ui.compact, 40 | ui.stretch, 41 | ] 42 | children: [ 43 | ui.label(text: 'Country:'), 44 | ui.radio( 45 | id: 'rh2' 46 | horizontal: true 47 | compact: true 48 | values: ['United States', 'Canada', 'United Kingdom', 'Australia'] 49 | ), 50 | ] 51 | ), 52 | ] 53 | ) 54 | w := ui.window( 55 | width: 500 56 | height: 300 57 | mode: .resizable 58 | layout: c 59 | ) 60 | ui.run(w) 61 | } 62 | 63 | fn on_switch_click(switcher &ui.Switch) { 64 | // switcher_state := if switcher.open { 'Enabled' } else { 'Disabled' } 65 | // app.label.set_text(switcher_state) 66 | mut rh1 := switcher.ui.window.get_or_panic[ui.Radio]('rh1') 67 | rh1.compact = !rh1.compact 68 | mut rh2 := switcher.ui.window.get_or_panic[ui.Radio]('rh2') 69 | rh2.compact = !rh2.compact 70 | switcher.ui.window.update_layout() 71 | } 72 | -------------------------------------------------------------------------------- /examples/demo_scrollview.v: -------------------------------------------------------------------------------- 1 | import ui 2 | import gg 3 | 4 | struct App { 5 | mut: 6 | window &ui.Window = unsafe { nil } 7 | text string 8 | info string 9 | } 10 | 11 | fn main() { 12 | mut app := &App{} 13 | mut s := '' 14 | for i in 0 .. 100 { 15 | s += 'line (${i})'.repeat(5) 16 | s += '\n' 17 | } 18 | app.text = s 19 | app.window = ui.window( 20 | width: 800 21 | height: 600 22 | title: 'V UI: Scrollview' 23 | mode: .resizable 24 | on_init: fn (win &ui.Window) { 25 | $if test_textwidth ? { 26 | mut tb := win.get_or_panic[ui.TextBox]('info') 27 | tb.tv.test_textwidth('abcdefghijklmnrputwxyz &éèdzefzefzef') 28 | } 29 | } 30 | layout: ui.row( 31 | widths: ui.stretch 32 | heights: ui.stretch 33 | children: [ 34 | ui.textbox( 35 | id: 'info' 36 | mode: .multiline | .read_only 37 | text: &app.info 38 | text_size: 24 39 | ), 40 | ui.textbox( 41 | id: 'text' 42 | mode: .multiline | .read_only 43 | bg_color: gg.hex(0xfcf4e4ff) 44 | text: &app.text 45 | text_size: 24 46 | on_scroll_change: on_scroll_change 47 | ), 48 | ] 49 | ) 50 | ) 51 | ui.run(app.window) 52 | } 53 | 54 | fn on_scroll_change(sw ui.ScrollableWidget) { 55 | mut tb := sw.ui.window.get_or_panic[ui.TextBox]('info') 56 | mut s := '' 57 | sv := sw.scrollview 58 | ox, oy := sv.orig_xy() 59 | s += 'textbox ${sw.id} has scrollview? ${sw.has_scrollview}' 60 | s += '\nat (${sw.x}, ${sw.y}) orig: (${ox}, ${oy})' 61 | s += '\nwith scrollview offset: (${sv.offset_x}, ${sv.offset_y})' 62 | s += '\nwith btn: (${sv.btn_x}, ${sv.btn_y})' 63 | tb.set_text(s) 64 | } 65 | -------------------------------------------------------------------------------- /examples/group2.v: -------------------------------------------------------------------------------- 1 | import ui 2 | 3 | const win_width = 600 4 | const win_height = 300 5 | 6 | struct App { 7 | mut: 8 | window &ui.Window = unsafe { nil } 9 | first_ipsum string 10 | second_ipsum string 11 | full_name string 12 | } 13 | 14 | fn main() { 15 | mut app := &App{} 16 | app.window = ui.window( 17 | width: win_width 18 | height: win_height 19 | title: 'Group 2 Demo' 20 | children: [ 21 | ui.column( 22 | margin: ui.Margin{10, 10, 10, 10} 23 | children: [ 24 | ui.row( 25 | spacing: 20 26 | children: [ 27 | ui.group( 28 | title: 'First group' 29 | children: [ 30 | ui.textbox( 31 | max_len: 20 32 | width: 200 33 | placeholder: 'Lorem ipsum' 34 | text: &app.first_ipsum 35 | ), 36 | ui.textbox( 37 | max_len: 20 38 | width: 200 39 | placeholder: 'dolor sit amet' 40 | text: &app.second_ipsum 41 | ), 42 | ui.button( 43 | text: 'More ipsum!' 44 | on_click: fn (b &ui.Button) { 45 | ui.open_url('https://lipsum.com/feed/html') 46 | } 47 | ), 48 | ] 49 | ), 50 | ui.group( 51 | title: 'Second group' 52 | children: [ 53 | ui.textbox( 54 | max_len: 20 55 | width: 200 56 | placeholder: 'Full name' 57 | text: &app.full_name 58 | ), 59 | ui.checkbox(checked: true, text: 'Do you like V?'), 60 | ui.button(text: 'Submit'), 61 | ] 62 | ), 63 | ] 64 | ), 65 | ] 66 | ), 67 | ] 68 | ) 69 | ui.run(app.window) 70 | } 71 | -------------------------------------------------------------------------------- /examples/canvas_plus_gradient_texture.v: -------------------------------------------------------------------------------- 1 | // A small demo of how to draw arbitrary images to a custom canvas, 2 | // by using ui.create_dynamic_texture and c.draw_texture . 3 | // The gradient is generated by calculating the color of each pixel in the canvas, 4 | // then blitting the resulting image/texture to the canvas at once at the end. 5 | import ui 6 | import gg 7 | import sokol.gfx 8 | 9 | @[heap] 10 | struct App { 11 | mut: 12 | window &ui.Window = unsafe { nil } 13 | buf &u8 = unsafe { nil } 14 | texture gfx.Image 15 | sampler gfx.Sampler 16 | } 17 | 18 | fn main() { 19 | mut app := App{} 20 | app.window = ui.window( 21 | width: 600 22 | height: 400 23 | title: 'gradient' 24 | on_init: app.init_texture 25 | children: [ 26 | ui.canvas_plus( 27 | id: 'canvas_gradient' 28 | on_draw: app.draw_gradient 29 | ), 30 | ] 31 | ) 32 | ui.run(app.window) 33 | } 34 | 35 | fn (mut app App) init_texture(w &ui.Window) { 36 | app.texture = ui.create_dynamic_texture(256, 256) 37 | app.sampler = ui.create_image_sampler() 38 | app.buf = unsafe { malloc(256 * 256 * 4) } 39 | } 40 | 41 | fn (app &App) draw_gradient(mut d ui.DrawDevice, c &ui.CanvasLayout) { 42 | target_hue, _, _ := ui.rgb_to_hsv(gg.rgb(255, 0, 0)) 43 | mut i := 0 44 | for y in 0 .. 256 { 45 | for x in 0 .. 256 { 46 | saturation := f32(y) / 255.0 47 | value := f32(255 - x) / 255.0 48 | rgb_color := ui.hsv_to_rgb(target_hue, saturation, value) 49 | unsafe { 50 | app.buf[i] = rgb_color.r 51 | app.buf[i + 1] = rgb_color.g 52 | app.buf[i + 2] = rgb_color.b 53 | app.buf[i + 3] = 255 54 | i += 4 55 | } 56 | } 57 | } 58 | ui.update_text_texture(app.texture, 256, 256, app.buf) 59 | c.draw_texture(app.texture, app.sampler) 60 | } 61 | -------------------------------------------------------------------------------- /examples/component/tabs.v: -------------------------------------------------------------------------------- 1 | import ui 2 | import ui.component as uic 3 | import gg 4 | 5 | const win_width = 600 6 | const win_height = 400 7 | 8 | fn main() { 9 | cb_layout := uic.colorbox_stack(id: 'cbox', light: false, hsl: false) 10 | rect := ui.rectangle( 11 | text: 'Here a simple ui rectangle ' 12 | color: gg.blue 13 | // align: gg.align_left 14 | text_size: 30 15 | ) 16 | window := ui.window( 17 | width: win_width 18 | height: win_height 19 | title: 'V UI: Toolbar' 20 | mode: .resizable 21 | native_message: false 22 | layout: ui.column( 23 | margin_: .05 24 | spacing: .05 25 | children: [ 26 | uic.tabs_stack( 27 | id: 'tab' 28 | tabs: ['tab1', 'tab2', 'tab3'] 29 | pages: [ 30 | ui.column( 31 | heights: ui.compact 32 | widths: ui.compact 33 | bg_color: gg.rgb(200, 100, 200) 34 | children: [ 35 | ui.button(id: 'left1', text: 'toto', padding: .1, radius: .25), 36 | ui.button(id: 'left2', text: 'toto2'), 37 | ] 38 | ), 39 | ui.column( 40 | heights: ui.compact 41 | widths: ui.compact 42 | children: [ 43 | cb_layout, 44 | rect, 45 | ] 46 | ), 47 | ui.column( 48 | heights: 200.0 49 | widths: 300.0 50 | bg_color: gg.rgb(100, 200, 200) 51 | children: [ 52 | uic.doublelistbox_stack( 53 | id: 'dlb1' 54 | title: 'dlb1' 55 | items: [ 56 | 'totto', 57 | 'titi', 58 | ] 59 | ), 60 | ] 61 | ), 62 | ] 63 | ), 64 | ] 65 | ) 66 | ) 67 | mut cb := uic.colorbox_component(cb_layout) 68 | cb.connect(&rect.style.color) 69 | ui.run(window) 70 | } 71 | -------------------------------------------------------------------------------- /src/layout_row.v: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2022 Alexander Medvednikov. All rights reserved. 2 | // Use of this source code is governed by a MIT license 3 | // that can be found in the LICENSE file. 4 | module ui 5 | 6 | import gg 7 | 8 | @[params] 9 | pub struct RowParams { 10 | pub: 11 | id string 12 | width int 13 | height int 14 | alignment VerticalAlignment 15 | spacing f64 16 | spacings []f64 = []f64{} // Size = Size(0.0) // Spacing = Spacing(0) // int 17 | stretch bool 18 | margin_ f64 19 | margin Margin 20 | // children related 21 | widths Size //[]f64 // children sizes 22 | heights Size //[]f64 23 | align Alignments 24 | alignments VerticalAlignments 25 | bg_color gg.Color = no_color 26 | bg_radius f64 27 | title string 28 | scrollview bool 29 | clipping bool 30 | children []Widget 31 | hidden bool 32 | } 33 | 34 | pub fn row(c RowParams) &Stack { 35 | return stack( 36 | id: c.id 37 | height: c.height 38 | width: c.width 39 | vertical_alignment: c.alignment 40 | spacings: spacings(c.spacing, c.spacings, c.children.len - 1) 41 | stretch: c.stretch 42 | direction: .row 43 | margins: margins(c.margin_, c.margin) 44 | widths: c.widths.as_f32_array(c.children.len) //.map(f32(it)) 45 | heights: c.heights.as_f32_array(c.children.len) //.map(f32(it)) 46 | vertical_alignments: c.alignments 47 | align: c.align 48 | bg_color: c.bg_color 49 | bg_radius: f32(c.bg_radius) 50 | title: c.title 51 | scrollview: c.scrollview 52 | clipping: c.clipping 53 | children: c.children 54 | // hidden: c.hidden 55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /src/tool_align.v: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2022 Alexander Medvednikov. All rights reserved. 2 | // Use of this source code is governed by a MIT license 3 | // that can be found in the LICENSE file. 4 | module ui 5 | 6 | import math 7 | 8 | pub enum VerticalAlignment { 9 | top = 0 10 | center 11 | bottom 12 | } 13 | 14 | pub enum HorizontalAlignment { 15 | left = 0 16 | center 17 | right 18 | } 19 | 20 | pub struct HorizontalAlignments { 21 | pub: 22 | left []int 23 | center []int 24 | right []int 25 | } 26 | 27 | pub struct VerticalAlignments { 28 | pub: 29 | top []int 30 | center []int 31 | bottom []int 32 | } 33 | 34 | // Anticipating replacement of VerticalAlignments 35 | pub struct Alignments { 36 | pub: 37 | center []int 38 | left_top []int 39 | top []int 40 | right_top []int 41 | right []int 42 | right_bottom []int 43 | bottom []int 44 | left_bottom []int 45 | left []int 46 | } 47 | 48 | pub fn get_align_offset_from_parent(mut w Widget, aw f64, ah f64) (int, int) { 49 | width, height := w.size() 50 | parent := w.parent 51 | parent_width, parent_height := if parent is Stack { parent.free_size() } else { parent.size() } 52 | dw := math.max(parent_width - width, 0) 53 | dh := math.max(parent_height - height, 0) 54 | $if get_align ? { 55 | if w.id in env('UI_IDS').split(',') { 56 | println('align: ${w.id} int(${aw} * ${dw}), int(${ah} * ${dh})') 57 | println('${width}, ${height} ${parent_width}, ${parent_height}') 58 | } 59 | } 60 | return int(aw * dw), int(ah * dh) 61 | } 62 | 63 | pub fn get_align_offset_from_size(width int, height int, pwidth int, pheight int, aw f64, ah f64) (int, int) { 64 | dw := math.max(pwidth - width, 0) 65 | dh := math.max(pheight - height, 0) 66 | return int(aw * dw), int(ah * dh) 67 | } 68 | -------------------------------------------------------------------------------- /examples/build_examples.vsh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S v 2 | 3 | import os 4 | 5 | const vexe = os.quoted_path(@VEXE) 6 | 7 | fn println_one_of_many(msg string, entry_idx int, entries_len int) { 8 | eprintln('${entry_idx + 1:2}/${entries_len:-2} ${msg}') 9 | } 10 | 11 | println('v executable: ${vexe}') 12 | print('v version: ${execute('${vexe} version').output}') 13 | 14 | examples_dir := join_path(@VMODROOT, 'examples') 15 | mut all_entries := walk_ext(examples_dir, '.v') 16 | all_entries.sort() 17 | mut entries := []string{} 18 | 19 | for entry in all_entries { 20 | if entry.contains('textbox_input') { 21 | eprintln('skipping ${entry}, part of the folder based `textbox_input` example') 22 | continue 23 | } 24 | $if !macos { 25 | fname := file_name(entry) 26 | if fname == 'webview.v' { 27 | eprintln('skipping ${entry} on !macos') 28 | continue 29 | } 30 | } 31 | entries << entry 32 | } 33 | entries << join_path(examples_dir, 'textbox_input') 34 | 35 | mut err := 0 36 | mut failures := []string{} 37 | chdir(examples_dir)! 38 | for entry_idx, entry in entries { 39 | cmd := '${vexe} -N -W ${entry}' 40 | println_one_of_many('compile with: ${cmd}', entry_idx, entries.len) 41 | ret := execute(cmd) 42 | if ret.exit_code != 0 { 43 | err++ 44 | failures << cmd 45 | eprintln('>>> FAILURE') 46 | eprintln('>>> err:') 47 | eprintln('----------------------------------------------------------------------------------') 48 | eprintln(ret.output) 49 | eprintln('----------------------------------------------------------------------------------') 50 | } 51 | } 52 | 53 | if err > 0 { 54 | err_count := if err == 1 { '1 error' } else { '${err} errors' } 55 | for f in failures { 56 | eprintln('> failed compilation cmd: ${f}') 57 | } 58 | eprintln('\nFailed with ${err_count}.') 59 | exit(1) 60 | } 61 | -------------------------------------------------------------------------------- /src/draw_device_context.v: -------------------------------------------------------------------------------- 1 | module ui 2 | 3 | import gg 4 | 5 | pub struct DrawDeviceContext { 6 | gg.Context 7 | mut: 8 | clip_rect Rect 9 | } 10 | 11 | // TODO: documentation 12 | pub fn (mut d DrawDeviceContext) reset_clipping() { 13 | // no need to actually set scissor_rect, it is reset each frame anyway, but 14 | // we do need to reset the clip_rect 15 | size := gg.window_size() 16 | d.clip_rect = Rect{ 17 | x: 0 18 | y: 0 19 | w: size.width 20 | h: size.height 21 | } 22 | $if ui_clipping ? { 23 | println('clip: reset') 24 | } 25 | } 26 | 27 | // TODO: documentation 28 | pub fn (mut d DrawDeviceContext) set_clipping(rect Rect) { 29 | d.clip_rect = rect 30 | d.Context.scissor_rect(rect.x, rect.y, rect.w, rect.h) 31 | $if ui_clipping ? { 32 | println('clip: set ${rect.x} ${rect.y} ${rect.w} ${rect.h}') 33 | } 34 | } 35 | 36 | // TODO: documentation 37 | pub fn (d DrawDeviceContext) get_clipping() Rect { 38 | return d.clip_rect 39 | } 40 | 41 | // TODO: documentation 42 | pub fn (d DrawDeviceContext) text_width_additive(text string) f64 { 43 | ctx := d.Context 44 | adv := ctx.ft.fons.text_bounds(0, 0, text, &f32(unsafe { nil })) 45 | return adv / ctx.scale 46 | } 47 | 48 | pub fn (d DrawDeviceContext) text_bounds(x int, y int, text string) []f32 { 49 | ctx := d.Context 50 | mut buf := [4]f32{} 51 | ctx.ft.fons.text_bounds(x, y, text, &buf[0]) 52 | asc, desc, lineh := f32(0), f32(0), f32(0) 53 | ctx.ft.fons.vert_metrics(&asc, &desc, &lineh) 54 | return [buf[0], buf[1], (buf[2] - buf[0]) / ctx.scale, (buf[3] - buf[1]) / ctx.scale, 55 | asc / ctx.scale, desc / ctx.scale, lineh / ctx.scale] 56 | } 57 | 58 | @[deprecated: 'use `widget.clipping` flag instead'] 59 | pub fn (d &DrawDeviceContext) scissor_rect(x int, y int, w int, h int) { 60 | d.Context.scissor_rect(x, y, w, h) 61 | } 62 | -------------------------------------------------------------------------------- /webview/webview_linux.c.v: -------------------------------------------------------------------------------- 1 | module webview 2 | 3 | #flag linux -I /usr/include/harfbuzz 4 | #pkgconfig gtk4 5 | #pkgconfig webkit2gtk-4.0 6 | #include 7 | #include 8 | 9 | struct C.GtkWidget { 10 | } 11 | 12 | fn C.gtk_init(argc int, argv voidptr) 13 | 14 | fn C.gtk_window_new() &C.GtkWidget 15 | 16 | fn C.gtk_window_set_default_size(win C.GtkWidget, w int, h int) 17 | 18 | fn C.gtk_window_set_title(win C.GtkWidget, title &char) 19 | 20 | fn C.gtk_container_add(container voidptr, widget voidptr) 21 | 22 | fn C.gtk_widget_show_all(win C.GtkWidget) 23 | 24 | fn C.gtk_main() 25 | 26 | fn C.g_signal_connect(ins voidptr, signal string, cb voidptr, data voidptr) 27 | 28 | fn C.gtk_widget_destroy(widget voidptr) 29 | 30 | fn C.gtk_widget_grab_focus(widget voidptr) 31 | 32 | fn C.gtk_main_quit() 33 | 34 | struct C.WebKitWebView { 35 | } 36 | 37 | fn C.webkit_web_view_new() &C.WebKitWebView 38 | 39 | fn C.webkit_web_view_load_uri(webview voidptr, uri &char) 40 | 41 | fn create_linux_web_view(url string, title string) { 42 | C.gtk_init(0, unsafe { nil }) 43 | win := C.gtk_window_new() 44 | C.gtk_window_set_default_size(win, 1000, 600) 45 | C.gtk_window_set_title(win, &char(title.str)) 46 | webview := C.webkit_web_view_new() 47 | C.gtk_container_add(win, webview) 48 | C.g_signal_connect(win, 'destroy', destroy_window_cb, unsafe { nil }) 49 | C.g_signal_connect(webview, 'close', destroy_window_cb, win) 50 | C.webkit_web_view_load_uri(webview, &char(url.str)) 51 | C.gtk_widget_grab_focus(webview) 52 | C.gtk_widget_show_all(win) 53 | C.gtk_main() 54 | } 55 | 56 | fn destroy_window_cb(widget voidptr, window voidptr) { 57 | C.gtk_main_quit() 58 | } 59 | 60 | fn close_webview_cb(webview voidptr, window voidptr) bool { 61 | C.gtk_widget_destroy(window) 62 | return true 63 | } 64 | -------------------------------------------------------------------------------- /examples/demo_style_accent_color.v: -------------------------------------------------------------------------------- 1 | import ui 2 | import ui.component as uic 3 | import gg 4 | 5 | fn main() { 6 | win := ui.window( 7 | title: 'Accent color' 8 | mode: .resizable 9 | on_init: win_init 10 | height: 600 11 | layout: ui.column( 12 | heights: [100.0, ui.stretch] 13 | children: [ 14 | ui.row( 15 | widths: [6 * ui.stretch, 4 * ui.stretch] 16 | children: [ 17 | uic.colorsliders_stack( 18 | id: 'cs' 19 | orientation: .horizontal 20 | color: gg.white 21 | on_changed: on_accent_color_changed 22 | ), 23 | ui.row( 24 | margin_: 10 25 | spacing: 5 26 | bg_color: gg.white 27 | widths: ui.stretch 28 | children: [ 29 | ui.rectangle(id: 'rect0', text: '0', border: true), 30 | ui.rectangle(id: 'rect1', text: '1', border: true), 31 | ui.rectangle(id: 'rect2', text: '2', border: true), 32 | ui.rectangle(id: 'rect3', text: '3', border: true), 33 | ] 34 | ), 35 | ] 36 | ), 37 | uic.demo_stack(), 38 | ] 39 | ) 40 | ) 41 | ui.run(win) 42 | } 43 | 44 | fn on_accent_color_changed(mut cs uic.ColorSlidersComponent) { 45 | color := cs.color() 46 | mut gui := cs.layout.ui 47 | // load accnt color for the window 48 | gui.window.load_accent_color_style([int(color.r), color.g, color.b]) 49 | // get current accent colors 50 | colors := gui.style_colors 51 | // show the 4 accent colors 52 | for i in 0 .. 4 { 53 | mut rect := gui.window.get_or_panic[ui.Rectangle]('rect${i}') 54 | rect.update_style_params(color: colors[i]) 55 | } 56 | } 57 | 58 | fn win_init(w &ui.Window) { 59 | mut cs := uic.colorsliders_component_from_id(w, 'cs') 60 | ac := [100, 40, 150] 61 | cs.set_color(gg.rgb(u8(ac[0]), u8(ac[1]), u8(ac[2]))) 62 | on_accent_color_changed(mut cs) 63 | } 64 | -------------------------------------------------------------------------------- /examples/component/fontchooser.v: -------------------------------------------------------------------------------- 1 | import ui 2 | import ui.component as uic 3 | import gg 4 | 5 | struct App { 6 | mut: 7 | window &ui.Window = unsafe { nil } 8 | log string 9 | text string = 'il était une fois V ....\nLa vie est belle...' 10 | } 11 | 12 | fn main() { 13 | mut app := &App{} 14 | mut tb := ui.textbox( 15 | id: 'tb' 16 | text: &app.text 17 | mode: .multiline 18 | bg_color: gg.yellow 19 | ) 20 | mut dtw := ui.DrawTextWidget(tb) 21 | dtw.update_style(size: 30, color: gg.red) 22 | mut window := ui.window( 23 | mode: .resizable 24 | width: 800 25 | height: 600 26 | on_init: fn (win &ui.Window) { 27 | mut btn := win.get_or_panic[ui.Button]('txt_color') 28 | tb := win.get_or_panic[ui.TextBox]('tb') 29 | unsafe { 30 | (*btn.bg_color) = tb.text_styles.current.color 31 | } 32 | } 33 | layout: ui.column( 34 | margin_: 10 35 | heights: [20.0, ui.stretch] 36 | spacing: 10 37 | children: [ 38 | ui.row( 39 | widths: ui.compact 40 | spacing: 10 41 | children: [ 42 | uic.fontbutton( 43 | text: 'font' 44 | dtw: tb 45 | ), 46 | uic.colorbutton( 47 | id: 'txt_color' 48 | // bg_color: &tb.text_styles.current.color 49 | // DO NOT REMOVE: more general alternative with callback 50 | on_changed: fn (cbc &uic.ColorButtonComponent) { 51 | mut tv := cbc.widget.ui.window.get_or_panic[ui.TextBox]('tb').tv 52 | tv.update_style(color: cbc.bg_color) 53 | } 54 | ), 55 | uic.colorbutton( 56 | id: 'bg_color' 57 | bg_color: &tb.style.bg_color 58 | ), 59 | ] 60 | ), 61 | tb, 62 | ] 63 | ) 64 | ) 65 | app.window = window 66 | uic.fontchooser_subwindow_add(mut window) 67 | uic.colorbox_subwindow_add(mut window) 68 | ui.run(app.window) 69 | } 70 | -------------------------------------------------------------------------------- /examples/webview.v: -------------------------------------------------------------------------------- 1 | import ui.webview 2 | import ui 3 | 4 | @[heap] 5 | struct App { 6 | mut: 7 | webview &webview.WebView 8 | } 9 | 10 | fn main() { 11 | mut app := &App{ 12 | webview: webview.new_window( 13 | url: 'https://github.com/revosw/ui/tree/master' 14 | title: 'hello' 15 | ) 16 | } 17 | window := ui.window( 18 | width: 800 19 | height: 100 20 | title: 'V ui.webview demo' 21 | children: [ 22 | ui.row( 23 | // stretch: true 24 | margin_: 10 25 | height: 100 26 | children: [ 27 | ui.button( 28 | text: 'Open' 29 | width: 70 30 | height: 100 31 | on_click: app.btn_open_click 32 | ), 33 | ui.button( 34 | text: 'Navigate to google' 35 | on_click: fn (b &ui.Button) { 36 | // println("on_click google") 37 | // app.webview.navigate("https://google.com") 38 | } 39 | ), 40 | ui.button( 41 | text: 'Navigate to steam' 42 | on_click: fn (b &ui.Button) { 43 | // println("on_click steam") 44 | // app.webview.navigate("https://steampowered.com") 45 | } 46 | ), 47 | ui.button( 48 | text: 'Rig on_navigate' 49 | on_click: fn (b &ui.Button) { 50 | // println("on_click rig") 51 | // app.webview.on_navigate(fn (url string) { 52 | // exit(0) 53 | // }) 54 | } 55 | ), 56 | ui.button( 57 | text: 'Run javascript' 58 | on_click: fn (b &ui.Button) { 59 | // println("on_click javascript") 60 | // app.webview.exec("alert('Ran some javascript')") 61 | } 62 | ), 63 | ] 64 | ), 65 | ] 66 | ) 67 | ui.run(window) 68 | } 69 | 70 | fn (mut app App) btn_open_click(b &ui.Button) { 71 | // println("on_click open") 72 | app.webview.navigate('https://github.com/revosw/ui/tree/master') 73 | } 74 | -------------------------------------------------------------------------------- /examples/layout/box_layout_inside_row.v: -------------------------------------------------------------------------------- 1 | import ui 2 | import gg 3 | 4 | const win_width = 400 5 | const win_height = 300 6 | 7 | @[heap] 8 | struct App { 9 | mut: 10 | text string 11 | texts map[string]string 12 | window &ui.Window = unsafe { nil } 13 | } 14 | 15 | fn make_tb(mut app App, mut text []string, has_row bool) ui.Widget { 16 | app.texts['toto'] = 'blah3 blah blah\n'.repeat(10) 17 | tb := ui.textbox( 18 | mode: .multiline 19 | bg_color: gg.yellow 20 | text: unsafe { &(app.texts['toto']) } 21 | ) 22 | return if has_row { 23 | ui.Widget(ui.row( 24 | widths: ui.stretch 25 | children: [ 26 | tb, 27 | ] 28 | )) 29 | } else { 30 | ui.Widget(tb) 31 | } 32 | } 33 | 34 | fn main() { 35 | mut app := App{ 36 | text: 'blah blah blah\n'.repeat(10) 37 | } 38 | 39 | mut text := ['blah2 blah blah\n'.repeat(10)] 40 | app.window = ui.window( 41 | width: win_width 42 | height: win_height 43 | title: 'V UI: Rectangles inside BoxLayout' 44 | mode: .resizable 45 | layout: ui.row( 46 | margin_: 20 47 | widths: ui.stretch 48 | heights: ui.stretch 49 | children: [ 50 | ui.box_layout( 51 | id: 'bl' 52 | children: { 53 | 'id1: (0,0) ++ (30%,30%)': ui.rectangle( 54 | color: gg.rgb(255, 100, 100) 55 | ) 56 | 'id2: (0.3,0.3) ++ (40%,40%)': ui.rectangle( 57 | color: gg.rgb(100, 255, 100) 58 | ) 59 | 'id3: (70%,70%) ++ (30%,30%)': make_tb(mut app, mut text, false) 60 | 'btn: (70%,10%) ++ (50,20)': ui.button( 61 | text: 'switch' 62 | on_click: app.btn_click 63 | ) 64 | } 65 | ), 66 | ] 67 | ) 68 | ) 69 | ui.run(app.window) 70 | } 71 | 72 | fn (mut app App) btn_click(_ &ui.Button) { 73 | mut bl := app.window.get_or_panic[ui.BoxLayout]('bl') 74 | bl.update_boundings('id3: (80%,80%) ++ (20%,20%)') 75 | } 76 | -------------------------------------------------------------------------------- /examples/group2_resizable.v: -------------------------------------------------------------------------------- 1 | import ui 2 | import gg 3 | 4 | const win_width = 600 5 | const win_height = 300 6 | 7 | struct App { 8 | mut: 9 | window &ui.Window = unsafe { nil } 10 | first_ipsum string 11 | second_ipsum string 12 | full_name string 13 | } 14 | 15 | fn main() { 16 | mut app := &App{} 17 | app.window = ui.window( 18 | width: win_width 19 | height: win_height 20 | title: 'Group 2 Demo' 21 | mode: .resizable 22 | children: [ 23 | ui.column( 24 | margin_: 10 25 | bg_color: gg.rgb(100, 100, 100) 26 | children: [ 27 | ui.row( 28 | spacing: 20 29 | children: [ 30 | ui.group( 31 | title: 'First group' 32 | clipping: true 33 | children: [ 34 | ui.textbox( 35 | max_len: 20 36 | width: 200 37 | placeholder: 'Lorem ipsum' 38 | text: &app.first_ipsum 39 | ), 40 | ui.textbox( 41 | max_len: 20 42 | width: 200 43 | placeholder: 'dolor sit amet' 44 | text: &app.second_ipsum 45 | ), 46 | ui.button( 47 | text: 'More ipsum!' 48 | on_click: fn (b &ui.Button) { 49 | ui.open_url('https://lipsum.com/feed/html') 50 | } 51 | ), 52 | ] 53 | ), 54 | ui.group( 55 | title: 'Second group' 56 | clipping: true 57 | children: [ 58 | ui.textbox( 59 | max_len: 20 60 | width: 200 61 | placeholder: 'Full name' 62 | text: &app.full_name 63 | ), 64 | ui.checkbox(checked: true, text: 'Do you like V?'), 65 | ui.button(text: 'Submit'), 66 | ] 67 | ), 68 | ] 69 | ), 70 | ] 71 | ), 72 | ] 73 | ) 74 | ui.run(app.window) 75 | } 76 | -------------------------------------------------------------------------------- /src/layout_column.v: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2022 Alexander Medvednikov. All rights reserved. 2 | // Use of this source code is governed by a MIT license 3 | // that can be found in the LICENSE file. 4 | module ui 5 | 6 | import gg 7 | 8 | @[params] 9 | pub struct ColumnParams { 10 | pub: 11 | id string 12 | width int // To remove soon 13 | height int // To remove soon 14 | alignment HorizontalAlignment 15 | spacing f64 // Size = Size(0.0) // Spacing = Spacing(0) // int 16 | spacings []f64 = []f64{} 17 | stretch bool // to remove ui.stretch doing the job from parent 18 | margin Margin 19 | margin_ f64 20 | // children related 21 | widths Size //[]f64 // children sizes 22 | heights Size //[]f64 23 | alignments HorizontalAlignments 24 | align Alignments 25 | bg_color gg.Color = no_color 26 | bg_radius f64 27 | title string 28 | scrollview bool 29 | clipping bool 30 | children []Widget 31 | } 32 | 33 | pub fn column(c ColumnParams) &Stack { 34 | return stack( 35 | id: c.id 36 | height: c.height 37 | width: c.width 38 | horizontal_alignment: c.alignment 39 | spacings: spacings(c.spacing, c.spacings, c.children.len - 1) 40 | stretch: c.stretch 41 | direction: .column 42 | margins: margins(c.margin_, c.margin) 43 | heights: c.heights.as_f32_array(c.children.len) //.map(f32(it)) 44 | widths: c.widths.as_f32_array(c.children.len) //.map(f32(it)) 45 | horizontal_alignments: c.alignments 46 | align: c.align 47 | bg_color: c.bg_color 48 | bg_radius: f32(c.bg_radius) 49 | title: c.title 50 | scrollview: c.scrollview 51 | clipping: c.clipping 52 | children: c.children 53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /examples/nested_scrollview_box_layout.v: -------------------------------------------------------------------------------- 1 | import ui 2 | import gg 3 | 4 | const win_width = 550 5 | const win_height = 300 6 | 7 | const box_width = 110 8 | const box_height = 90 9 | 10 | struct App { 11 | mut: 12 | box_text []string 13 | } 14 | 15 | fn make_scroll_area(mut app App) ui.Widget { 16 | mut kids, mut decl := map[string]ui.Widget{}, '' 17 | for r in 0 .. 5 { 18 | for c in 0 .. 5 { 19 | id := 'box${r}_${c}' 20 | app.box_text << 'box${r}${c}\n...\n...\n...\n...\n...\n...\n...\n...\n...' 21 | $if fixed ? { 22 | decl = '${id}: (${r * 110},${c * 90}) ++ (100,80)' 23 | } $else $if fixed_spacing ? { 24 | decl = '${id}: (${r * 1.0 / 5},${c * 1.0 / 5}) ++ (${1.0 / 5 - 0.01},${1.0 / 5 - .01})' 25 | } $else { 26 | decl = '${id}: (${r * 1.0 / 5},${c * 1.0 / 5}) ++ (${1.0 / 5 - 0.01},${1.0 / 5 - .01})' 27 | } 28 | kids[decl] = ui.textbox( 29 | width: box_width 30 | height: box_height 31 | bg_color: gg.white 32 | is_multiline: true 33 | text: &app.box_text[app.box_text.len - 1] 34 | ) 35 | } 36 | } 37 | 38 | return ui.box_layout( 39 | id: 'bl' 40 | children: kids 41 | ) 42 | } 43 | 44 | fn win_key_down(w &ui.Window, e ui.KeyEvent) { 45 | if e.key == .escape { 46 | // TODO: w.close() not implemented (no multi-window support yet!) 47 | if w.ui.dd is ui.DrawDeviceContext { 48 | w.ui.dd.quit() 49 | } 50 | } 51 | } 52 | 53 | fn main() { 54 | mut app := App{} 55 | mut win := ui.window( 56 | width: win_width 57 | height: win_height 58 | title: 'V nested scrollviews inside boxlayout ' 59 | on_key_down: win_key_down 60 | mode: .resizable 61 | layout: ui.column( 62 | scrollview: true 63 | widths: ui.stretch 64 | heights: ui.stretch 65 | bg_color: gg.yellow 66 | children: [ 67 | make_scroll_area(mut app), 68 | ] 69 | ) 70 | ) 71 | ui.run(win) 72 | } 73 | -------------------------------------------------------------------------------- /examples/component/splitpanel.v: -------------------------------------------------------------------------------- 1 | import ui 2 | import ui.component as uic 3 | import gg 4 | 5 | fn main() { 6 | n := 100 7 | tbm := 'toto bbub jhuui jkhuhui hubhuib\ntiti tutu toto\ntata tata'.repeat(1000) 8 | window := ui.window( 9 | width: 800 10 | height: 600 11 | title: 'V UI: SplitPanel' 12 | mode: .resizable 13 | layout: uic.splitpanel_stack( 14 | id: 'column' 15 | weight: 33 16 | direction: .column 17 | child1: ui.rectangle( 18 | color: gg.rgb(100, 255, 100) 19 | ) 20 | child2: uic.splitpanel_stack( 21 | weight: 25.0 22 | child1: ui.rectangle( 23 | color: gg.rgb(100, 255, 100) 24 | ) 25 | child2: uic.splitpanel_stack( 26 | id: 'row' 27 | // direction: .column 28 | weight: 33 29 | child2: uic.datagrid_stack( 30 | id: 'grid' 31 | is_focused: true 32 | vars: { 33 | 'v1': ['toto', 'titi', 'tata'].repeat(n) 34 | 'v2': ['toti', 'tito', 'tato'].repeat(n) 35 | 'sex': uic.Factor{ 36 | levels: ['Male', 'Female'] 37 | values: [0, 0, 1].repeat(n) 38 | } 39 | 'csp': uic.Factor{ 40 | levels: ['job1', 'job2', 'other'] 41 | values: [0, 1, 2].repeat(n) 42 | } 43 | 'v3': ['toto', 'titi', 'tata'].repeat(n) 44 | 'v4': ['toti', 'tito', 'tato'].repeat(n) 45 | 'sex2': uic.Factor{ 46 | levels: ['Male', 'Female'] 47 | values: [0, 0, 1].repeat(n) 48 | } 49 | 'csp2': uic.Factor{ 50 | levels: ['job1', 'job2', 'other'] 51 | values: [0, 1, 2].repeat(n) 52 | } 53 | } 54 | ) 55 | child1: ui.textbox( 56 | mode: .multiline 57 | id: 'tbm' 58 | text: &tbm 59 | height: 200 60 | text_size: 24 61 | bg_color: gg.hex(0xfcf4e4ff) // gg.rgb(252, 244, 228) 62 | ) 63 | ) 64 | ) 65 | ) 66 | ) 67 | ui.run(window) 68 | } 69 | -------------------------------------------------------------------------------- /component/gg_app.v: -------------------------------------------------------------------------------- 1 | module component 2 | 3 | import ui 4 | // import time 5 | import gg 6 | 7 | @[heap] 8 | struct GGComponent { 9 | id string 10 | pub mut: 11 | layout &ui.CanvasLayout = unsafe { nil } 12 | app ui.GGApplication 13 | } 14 | 15 | @[params] 16 | pub struct GGComponentParams { 17 | pub: 18 | id string = 'gg_app' 19 | app ui.GGApplication 20 | z_index int 21 | } 22 | 23 | pub fn gg_canvaslayout(p GGComponentParams) &ui.CanvasLayout { 24 | mut layout := ui.canvas_plus( 25 | id: ui.component_id(p.id, 'layout') 26 | delegate_evt_mngr: true 27 | on_draw: gg_draw 28 | on_delegate: gg_on_delegate 29 | on_bounding_change: gg_on_bounding_change 30 | z_index: p.z_index 31 | ) 32 | mut ggc := &GGComponent{ 33 | id: p.id 34 | layout: layout 35 | app: p.app 36 | } 37 | ui.component_connect(ggc, layout) 38 | layout.on_init = gg_init 39 | return layout 40 | } 41 | 42 | // component access 43 | pub fn gg_component(w ui.ComponentChild) &GGComponent { 44 | return unsafe { &GGComponent(w.component) } 45 | } 46 | 47 | pub fn gg_component_from_id(w ui.Window, id string) &GGComponent { 48 | return gg_component(w.get_or_panic[ui.Stack](ui.component_id(id, 'layout'))) 49 | } 50 | 51 | fn gg_init(layout &ui.CanvasLayout) { 52 | mut ggc := gg_component(layout) 53 | if layout.ui.dd is ui.DrawDeviceContext { 54 | ggc.app.gg = &layout.ui.dd.Context 55 | } 56 | mut app := ggc.app 57 | app.on_init() 58 | } 59 | 60 | fn gg_draw(mut d ui.DrawDevice, c &ui.CanvasLayout) { 61 | mut ggc := gg_component(c) 62 | mut app := ggc.app 63 | app.on_draw() 64 | } 65 | 66 | fn gg_on_delegate(c &ui.CanvasLayout, e &gg.Event) { 67 | mut ggc := gg_component(c) 68 | mut app := ggc.app 69 | app.on_delegate(e) 70 | } 71 | 72 | fn gg_on_bounding_change(c &ui.CanvasLayout, bb gg.Rect) { 73 | mut ggc := gg_component(c) 74 | mut app := ggc.app 75 | app.set_bounds(bb) 76 | } 77 | -------------------------------------------------------------------------------- /examples/demo_text_width_additive.v: -------------------------------------------------------------------------------- 1 | import ui 2 | import gg 3 | 4 | fn main() { 5 | win := ui.window( 6 | width: 1000 7 | height: 600 8 | title: 'V UI: Test text_width_additive' 9 | mode: .resizable 10 | layout: ui.column( 11 | widths: ui.stretch 12 | heights: [ui.compact, ui.stretch] 13 | margin_: 5 14 | spacing: 10 15 | children: [ 16 | ui.textbox( 17 | id: 'text' 18 | placeholder: 'Type text here to show textwidth below...' 19 | text_size: 20 20 | on_change: fn (tb_text &ui.TextBox) { 21 | mut tb := tb_text.ui.window.get_or_panic[ui.TextBox]('info') 22 | // that's weird text_width is not additive function 23 | ustr := tb_text.text.runes() 24 | mut total_twa, mut total_tw, mut total_ts := 0.0, 0.0, 0.0 25 | mut out := "text_width_additive vs text_width vs text_size:'\n\n" 26 | for i in 0 .. ustr.len { 27 | twa := ui.DrawTextWidget(tb).text_width_additive(ustr[i..(i + 1)].string()) 28 | total_twa += twa 29 | tw := ui.DrawTextWidget(tb).text_width(ustr[i..(i + 1)].string()) 30 | total_tw += tw 31 | ts, _ := ui.DrawTextWidget(tb).text_size(ustr[i..(i + 1)].string()) 32 | total_ts += ts 33 | full_twa := ui.DrawTextWidget(tb).text_width_additive(ustr[..i + 1].string()) 34 | full_tw := ui.DrawTextWidget(tb).text_width(ustr[..i + 1].string()) 35 | full_ts, _ := ui.DrawTextWidget(tb).text_size(ustr[..i + 1].string()) 36 | out += '${i}) ${ustr[i..(i + 1)].string()} (${twa} vs ${tw} vs ${ts}) (${total_twa} == ${full_twa} vs ${total_tw} == ${full_tw} vs ${total_ts} == ${full_ts}) \n' 37 | } 38 | tb.set_text(out) 39 | } 40 | ), 41 | ui.textbox( 42 | id: 'info' 43 | mode: .multiline | .read_only 44 | bg_color: gg.hex(0xfcf4e4ff) 45 | // text: &app.info 46 | text_size: 24 47 | ), 48 | ] 49 | ) 50 | ) 51 | ui.run(win) 52 | } 53 | -------------------------------------------------------------------------------- /src/tool_gg.v: -------------------------------------------------------------------------------- 1 | module ui 2 | 3 | import gg 4 | import math 5 | 6 | pub fn intersection_rect(r1 gg.Rect, r2 gg.Rect) gg.Rect { 7 | // top left and bottom right points 8 | tl_x, tl_y := math.max(r1.x, r2.x), math.max(r1.y, r2.y) 9 | br_x, br_y := math.min(r1.x + r1.width, r2.x + r2.width), math.min(r1.y + r1.height, 10 | r2.y + r2.height) 11 | // intersection 12 | r := gg.Rect{f32(tl_x), f32(tl_y), f32(br_x - tl_x), f32(br_y - tl_y)} 13 | return r 14 | } 15 | 16 | pub fn is_empty_intersection(r1 gg.Rect, r2 gg.Rect) bool { 17 | r := intersection_rect(r1, r2) 18 | return r.width < 0 || r.height < 0 19 | } 20 | 21 | pub fn union_rect(r1 gg.Rect, r2 gg.Rect) gg.Rect { 22 | // top left and bottom right points 23 | tl_x, tl_y := math.min(r1.x, r2.x), math.min(r1.y, r2.y) 24 | br_x, br_y := math.max(r1.x + r1.width, r2.x + r2.width), math.max(r1.y + r1.height, 25 | r2.y + r2.height) 26 | // intersection 27 | r := gg.Rect{f32(tl_x), f32(tl_y), f32(br_x - tl_x), f32(br_y - tl_y)} 28 | return r 29 | } 30 | 31 | pub fn inside_rect(r gg.Rect, c gg.Rect) bool { // c for container 32 | return r.x >= c.x && r.y >= c.y && r.x + r.width <= c.x + c.width 33 | && r.y + r.height <= c.y + c.height 34 | } 35 | 36 | pub fn is_rgb_valid(c int) bool { 37 | return if c >= 0 && c < 256 { true } else { false } 38 | } 39 | 40 | // Color 41 | type HexColor = string 42 | 43 | pub fn hex_rgba(r u8, g u8, b u8, a u8) string { 44 | return '#${r.hex()}${g.hex()}${b.hex()}${a.hex()}' 45 | } 46 | 47 | pub fn hex_color(c gg.Color) string { 48 | return '#${c.r.hex()}${c.g.hex()}${c.b.hex()}${c.a.hex()}' 49 | } 50 | 51 | pub fn (hs HexColor) rgba() (u8, u8, u8, u8) { 52 | u := ('0x' + hs[1..]).u32() 53 | return u8(u >> 24), u8(u >> 16), u8(u >> 8), u8(u) 54 | } 55 | 56 | pub fn (hs HexColor) color() gg.Color { 57 | u := ('0x' + hs[1..]).u32() 58 | return gg.rgba(u8(u >> 24), u8(u >> 16), u8(u >> 8), u8(u)) 59 | } 60 | 61 | pub fn alpha_colored(c gg.Color, a u8) gg.Color { 62 | return gg.rgba(c.r, c.g, c.b, a) 63 | } 64 | -------------------------------------------------------------------------------- /component/fontbutton.v: -------------------------------------------------------------------------------- 1 | module component 2 | 3 | import ui 4 | import gg 5 | 6 | @[heap] 7 | pub struct FontButtonComponent { 8 | pub mut: 9 | btn &ui.Button = unsafe { nil } 10 | dtw ui.DrawTextWidget 11 | } 12 | 13 | @[params] 14 | pub struct FontButtonParams { 15 | pub: 16 | id string 17 | dtw &ui.DrawTextWidget = ui.canvas_plus() 18 | text string 19 | height int 20 | width int 21 | z_index int 22 | tooltip string 23 | tooltip_side ui.Side = .top 24 | radius f64 = .25 25 | padding f64 26 | bg_color &gg.Color = unsafe { nil } 27 | } 28 | 29 | // TODO: documentation 30 | pub fn fontbutton(c FontButtonParams) &ui.Button { 31 | b := &ui.Button{ 32 | id: c.id 33 | text: c.text 34 | width_: c.width 35 | height_: c.height 36 | z_index: c.z_index 37 | bg_color: c.bg_color 38 | // theme_cfg: ui.no_theme 39 | tooltip: ui.TooltipMessage{c.tooltip, c.tooltip_side} 40 | on_click: font_button_click 41 | style_params: ui.button_style(radius: f32(c.radius)) 42 | padding: f32(c.padding) 43 | ui: unsafe { nil } 44 | } 45 | mut fb := &FontButtonComponent{ 46 | btn: b 47 | dtw: c.dtw 48 | } 49 | ui.component_connect(fb, b) 50 | return b 51 | } 52 | 53 | // TODO: documentation 54 | pub fn fontbutton_component(w ui.ComponentChild) &FontButtonComponent { 55 | return unsafe { &FontButtonComponent(w.component) } 56 | } 57 | 58 | // TODO: documentation 59 | pub fn fontbutton_component_from_id(w ui.Window, id string) &FontButtonComponent { 60 | return fontbutton_component(w.get_or_panic[ui.Button](id)) 61 | } 62 | 63 | fn font_button_click(mut b ui.Button) { 64 | fb := fontbutton_component(b) 65 | // println('fb_click $fb.dtw.id') 66 | fontchooser_connect(b.ui.window, fb.dtw) 67 | fontchooser_subwindow_visible(b.ui.window) 68 | mut s := b.ui.window.get_or_panic[ui.SubWindow](fontchooser_subwindow_id) 69 | if s.x == 0 && s.y == 0 { 70 | w, h := b.size() 71 | s.set_pos(b.x + w / 2, b.y + h / 2) 72 | s.update_layout() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /examples/7guis/temperature.v: -------------------------------------------------------------------------------- 1 | import ui 2 | import regex 3 | import gg 4 | import math 5 | 6 | const win_width = 400 7 | const win_height = 41 8 | 9 | fn main() { 10 | window := ui.window( 11 | width: win_width 12 | height: win_height 13 | title: 'Temperature Converter' 14 | mode: .resizable 15 | layout: ui.row( 16 | margin_: 10 17 | spacing: 10 18 | widths: [ui.stretch, ui.compact, ui.stretch, ui.compact] 19 | heights: 20.0 20 | children: [ 21 | ui.textbox( 22 | id: 'celsius' 23 | on_change: on_change_celsius 24 | ), 25 | ui.label(text: 'Celsius = '), 26 | ui.textbox( 27 | id: 'fahren' 28 | on_change: on_change_fahren 29 | ), 30 | ui.label(text: 'Fahrenheit'), 31 | ] 32 | ) 33 | ) 34 | ui.run(window) 35 | } 36 | 37 | fn on_change_celsius(mut tb_celsius ui.TextBox) { 38 | mut tb_fahren := tb_celsius.ui.window.get_or_panic[ui.TextBox]('fahren') 39 | if tb_celsius.text.len <= 0 { 40 | tb_fahren.set_text('0') 41 | return 42 | } 43 | if is_number(*(tb_celsius.text)) { 44 | celsius := (*(tb_celsius.text)).f64() 45 | fahren := celsius * (9.0 / 5.0) + 32.0 46 | tb_fahren.set_text((math.ceil(fahren * 100) / 100.0).str()) 47 | tb_celsius.update_style(bg_color: gg.white) 48 | } else { 49 | tb_celsius.update_style(bg_color: gg.orange) 50 | } 51 | } 52 | 53 | fn on_change_fahren(mut tb_fahren ui.TextBox) { 54 | mut tb_celsius := tb_fahren.ui.window.get_or_panic[ui.TextBox]('celsius') 55 | if tb_fahren.text.len <= 0 { 56 | tb_celsius.set_text('0') 57 | return 58 | } 59 | if is_number(*(tb_fahren.text)) { 60 | fah := (*tb_fahren.text).f64() 61 | cel := (fah - 32.0) * (5.0 / 9.0) 62 | tb_celsius.set_text((math.ceil(cel * 100) / 100.0).str()) 63 | tb_fahren.update_style(bg_color: gg.white) 64 | } else { 65 | tb_fahren.update_style(bg_color: gg.orange) 66 | } 67 | } 68 | 69 | fn is_number(txt string) bool { 70 | query := r'\-?(?P\d+)\.?(?P\d+)?' 71 | mut re := regex.regex_opt(query) or { panic(err) } 72 | return re.matches_string(txt) 73 | } 74 | -------------------------------------------------------------------------------- /examples/demo_event.v: -------------------------------------------------------------------------------- 1 | import ui 2 | import gg 3 | 4 | fn main() { 5 | window := ui.window( 6 | width: 600 7 | height: 600 8 | title: 'V UI: Event' 9 | mode: .resizable 10 | on_key_down: fn (w &ui.Window, e ui.KeyEvent) { 11 | mut tb := w.get_or_panic[ui.TextBox]('info') 12 | tb.set_text('key_down:\n${e}') 13 | } 14 | on_char: fn (w &ui.Window, e ui.KeyEvent) { 15 | mut tb := w.get_or_panic[ui.TextBox]('info') 16 | s := utf32_to_str(e.codepoint) 17 | tb.set_text('${*tb.text} \nchar: <${s}>\n${e}') 18 | } 19 | on_mouse_down: fn (w &ui.Window, e ui.MouseEvent) { 20 | mut tb := w.get_or_panic[ui.TextBox]('info') 21 | tb.set_text('mouse_down:\n${e}') 22 | } 23 | on_click: fn (w &ui.Window, e ui.MouseEvent) { 24 | mut tb := w.get_or_panic[ui.TextBox]('info') 25 | tb.set_text('${*tb.text} \nmouse_click:\n${e} \nnb_click: ${tb.ui.nb_click}') 26 | } 27 | on_mouse_up: fn (w &ui.Window, e ui.MouseEvent) { 28 | mut tb := w.get_or_panic[ui.TextBox]('info') 29 | tb.set_text('mouse_up:\n${e}') 30 | } 31 | on_mouse_move: fn (w &ui.Window, e ui.MouseMoveEvent) { 32 | mut tb := w.get_or_panic[ui.TextBox]('info') 33 | tb.set_text('mouse_move:\n${e}') 34 | } 35 | on_swipe: fn (w &ui.Window, e ui.MouseEvent) { 36 | mut tb := w.get_or_panic[ui.TextBox]('info') 37 | tb.set_text('swipe:\n${e}') 38 | } 39 | on_scroll: fn (w &ui.Window, e ui.ScrollEvent) { 40 | mut tb := w.get_or_panic[ui.TextBox]('info') 41 | tb.set_text('mouse_scroll\n${e}') 42 | } 43 | on_resize: fn (win &ui.Window, w int, h int) { 44 | mut tb := win.get_or_panic[ui.TextBox]('info') 45 | tb.set_text('resize:\n (${w}, ${h})') 46 | } 47 | layout: ui.row( 48 | widths: ui.stretch 49 | heights: ui.stretch 50 | children: [ 51 | ui.textbox( 52 | id: 'info' 53 | mode: .multiline | .read_only 54 | bg_color: gg.hex(0xfcf4e4ff) 55 | // text: &app.info 56 | text_size: 24 57 | ), 58 | ] 59 | ) 60 | ) 61 | ui.run(window) 62 | } 63 | -------------------------------------------------------------------------------- /component/messagebox.v: -------------------------------------------------------------------------------- 1 | module component 2 | 3 | import ui 4 | 5 | type MessageBoxFn = fn (&MessageBoxComponent) 6 | 7 | @[heap] 8 | pub struct MessageBoxComponent { 9 | id string 10 | layout &ui.Stack = unsafe { nil } 11 | tb &ui.TextBox = unsafe { nil } 12 | btn &ui.Button = unsafe { nil } 13 | text string 14 | on_click MessageBoxFn = unsafe { MessageBoxFn(0) } 15 | } 16 | 17 | @[params] 18 | pub struct MessageBoxParams { 19 | pub: 20 | id string 21 | text string 22 | on_click MessageBoxFn = unsafe { MessageBoxFn(0) } 23 | width int 24 | height int 25 | } 26 | 27 | // TODO: documentation 28 | pub fn messagebox_stack(p MessageBoxParams) &ui.Stack { 29 | mut tb := ui.textbox( 30 | id: ui.component_id(p.id, 'textbox') 31 | mode: .multiline | .read_only 32 | text_size: 24 33 | bg_color: ui.color_solaris_transparent 34 | ) 35 | ok_btn := ui.button( 36 | id: ui.component_id(p.id, 'ok_btn') 37 | text: 'Ok' 38 | on_click: messagebox_ok_click 39 | ) 40 | layout := ui.column( 41 | id: ui.component_id(p.id, 'layout') 42 | width: p.width 43 | height: p.height 44 | heights: [ui.stretch, 30] 45 | children: [tb, ok_btn] 46 | ) 47 | hc := &MessageBoxComponent{ 48 | id: p.id 49 | layout: layout 50 | text: p.text 51 | tb: tb 52 | btn: ok_btn 53 | on_click: p.on_click 54 | } 55 | unsafe { 56 | tb.text = &hc.text 57 | } 58 | ui.component_connect(hc, layout, tb, ok_btn) 59 | return layout 60 | } 61 | 62 | // component access 63 | pub fn messagebox_component(w ui.ComponentChild) &MessageBoxComponent { 64 | return unsafe { &MessageBoxComponent(w.component) } 65 | } 66 | 67 | // TODO: documentation 68 | pub fn messagebox_component_from_id(w ui.Window, id string) &MessageBoxComponent { 69 | return messagebox_component(w.get_or_panic[ui.Stack](ui.component_id(id, 'layout'))) 70 | } 71 | 72 | fn messagebox_ok_click(b &ui.Button) { 73 | hc := messagebox_component(b) 74 | if hc.on_click != unsafe { MessageBoxFn(0) } { 75 | hc.on_click(hc) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/ui_default.c.v: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2022 Alexander Medvednikov. All rights reserved. 2 | // Use of this source code is governed by a MIT license 3 | // that can be found in the LICENSE file. 4 | module ui 5 | 6 | import os 7 | import rand 8 | 9 | // Note: sokol currently does not allow proper handling of multiple windows: 10 | // - closing the secondary window, causes closing of the primary one as well. 11 | // - closing the secondary window frequently leads to crashes as well, 12 | // depending on when the closing event is handled, in the secondary thread. 13 | // 14 | // Due to this, for now just implement a fallback to the wide spread program 15 | // ggmessage, followed by xmessage, even though its messages do look a little ugly. 16 | // 17 | // TODO: implement a simple X11 message box, directly with C calls, 18 | // instead of relying on external programs. 19 | 20 | // message_box shows a simple message box, containing a single text message, and an OK button 21 | pub fn message_box(s string) { 22 | // try several programs, in order from more modern to most likely installed but ugly: 23 | for cmd in ['ggmessage', 'xmessage'] { 24 | message_box_system(cmd, s) or { 25 | eprintln('message_box error: ${err}') 26 | continue 27 | } 28 | return 29 | } 30 | eprintln('-'.repeat(80)) 31 | eprintln('| neither xmessage or ggmessage were found; please install the `x11-utils` and `ggmessage` packages |') 32 | eprintln('-'.repeat(80)) 33 | eprintln(s) 34 | eprintln('-'.repeat(80)) 35 | } 36 | 37 | fn message_box_system(cmdname string, s string) ! { 38 | msgcmd := os.find_abs_path_of_executable(cmdname) or { 39 | return error('${cmdname} was not found') 40 | } 41 | sfilepath := os.join_path(os.temp_dir(), '${rand.ulid()}.txt') 42 | os.write_file(sfilepath, s) or {} 43 | defer { 44 | os.rm(sfilepath) or {} 45 | } 46 | mut other_options := ['-nearmouse'] 47 | if cmdname == 'ggmessage' { 48 | other_options << '-title "Message:"' 49 | } 50 | cmd := '${os.quoted_path(msgcmd)} ${other_options.join(' ')} -print -file ${os.quoted_path(sfilepath)}' 51 | os.system(cmd) 52 | } 53 | -------------------------------------------------------------------------------- /libvg/raster_ttf.v: -------------------------------------------------------------------------------- 1 | module libvg 2 | 3 | import x.ttf 4 | import os 5 | 6 | // TODO: documentation 7 | pub fn (mut r Raster) attach_bitmap() { 8 | bmp := ttf.BitMap{ 9 | tf: unsafe { nil } 10 | buf: r.data.data 11 | buf_size: r.width * r.height * r.channels 12 | width: r.width 13 | height: r.height 14 | bp: r.channels 15 | // space_cw: 1.0 16 | // space_mult: 1.0/16.0 17 | // use_font_metrics: false 18 | // justify: true 19 | // justify_fill_ratio: 0.75 20 | } 21 | r.bmp = &bmp 22 | } 23 | 24 | // TODO: documentation 25 | pub fn (mut r Raster) add_ttf(ttf_filename string) { 26 | mut ttf_font := ttf.TTF_File{} 27 | ttf_font.buf = os.read_bytes(ttf_filename) or { panic(err) } 28 | ttf_font.init() 29 | r.ttf_fonts[ttf_filename] = ttf_font 30 | } 31 | 32 | // TODO: documentation 33 | pub fn (mut r Raster) attach_ttf(ttf_filename string) { 34 | the_font_ptr := r.ttf_fonts[ttf_filename] 35 | r.ttf_font = &the_font_ptr 36 | r.bmp.tf = r.ttf_font 37 | } 38 | 39 | // TODO: documentation 40 | pub fn (r &Raster) get_info_string() { 41 | // print font info 42 | println(r.ttf_font.get_info_string()) 43 | } 44 | 45 | @[params] 46 | pub struct SetFontSizeParams { 47 | pub: 48 | font_size int 49 | device_dpi int = 72 50 | } 51 | 52 | // TODO: documentation 53 | pub fn (mut r Raster) set_font_size(p SetFontSizeParams) { 54 | // Formula for scale calculation 55 | // scaler := (font_size * device dpi) / (72dpi * em_unit) 56 | scale := f32(p.font_size * p.device_dpi) / f32(72 * r.ttf_font.units_per_em) 57 | r.bmp.scale = scale 58 | } 59 | 60 | // TODO: documentation 61 | pub fn (mut r Raster) init_style(ts BitmapTextStyle) { 62 | r.attach_ttf(ts.font_path) 63 | r.init_filler() 64 | r.set_font_size(font_size: ts.size, device_dpi: 72) 65 | r.color = ts.color 66 | r.bmp.justify = false // true 67 | r.bmp.align = .left 68 | r.style = .filled 69 | } 70 | 71 | // TODO: documentation 72 | pub fn (r &Raster) get_y_base() f32 { 73 | // height of the font to use in the buffer to separate the lines 74 | y_base := f32((r.ttf_font.y_max - r.ttf_font.y_min) * r.bmp.scale) 75 | return y_base 76 | } 77 | -------------------------------------------------------------------------------- /examples/component/grid2.v: -------------------------------------------------------------------------------- 1 | import ui 2 | import ui.component as uic 3 | import gg 4 | 5 | const win_width = 800 6 | const win_height = 600 7 | 8 | fn main() { 9 | n := 300 10 | window := ui.window( 11 | width: win_width 12 | height: win_height 13 | title: 'V UI: Grid 2' 14 | native_message: false 15 | mode: .resizable 16 | bg_color: gg.white 17 | on_init: win_init 18 | layout: ui.row( 19 | widths: [ui.stretch, 15 * ui.stretch] 20 | children: [ui.rectangle(color: gg.red), 21 | ui.column( 22 | // scrollview: true 23 | widths: ui.stretch 24 | heights: [ui.stretch, 15 * ui.stretch] 25 | children: [ui.rectangle(color: gg.red), 26 | uic.datagrid_stack( 27 | id: 'grid2' 28 | is_focused: true 29 | settings_bg_color: gg.hex(0xfcf4e4ff) 30 | // fixed_height: false 31 | vars: { 32 | 'v1': ['toto', 'titi', 'tata'].repeat(n) 33 | 'v2': ['toti', 'tito', 'tato'].repeat(n) 34 | 'sex': uic.Factor{ 35 | levels: ['Male', 'Female'] 36 | values: [0, 0, 1].repeat(n) 37 | } 38 | 'worker': [true, true, false].repeat(n) 39 | 'csp': uic.Factor{ 40 | levels: ['job1', 'job2', 'other'] 41 | values: [0, 1, 2].repeat(n) 42 | } 43 | 'v3': ['toto', 'titi', 'tata'].repeat(n) 44 | 'v4': ['toti', 'tito', 'tato'].repeat(n) 45 | 'sex2': uic.Factor{ 46 | levels: ['Male', 'Female'] 47 | values: [0, 0, 1].repeat(n) 48 | } 49 | 'csp2': uic.Factor{ 50 | levels: ['job1', 'job2', 'other'] 51 | values: [0, 1, 2].repeat(n) 52 | } 53 | } 54 | )] 55 | )] 56 | ) 57 | ) 58 | ui.run(window) 59 | } 60 | 61 | fn win_init(w &ui.Window) { 62 | // mut g := uic.grid_component_from_id(w, "grid") 63 | // g.init_ranked_grid_data([2, 0], [1, 2]) 64 | 65 | gc := uic.GridCell{12, 1208} 66 | // gc := uic.GridCell{0,1} 67 | ac := gc.alphacell() 68 | gc2 := uic.AlphaCell(ac).gridcell() 69 | println('${gc} -> ${ac} -> ${gc2}') 70 | } 71 | -------------------------------------------------------------------------------- /src/interface_components.v: -------------------------------------------------------------------------------- 1 | module ui 2 | 3 | //--- 4 | // A Composable Widget is concretely a Layout (Stack, Group or CanvasLayout) to be added to the window children tree (sort of DOM). 5 | // A Component is the struct gathering ComponentChild: 6 | // 1) the unique composable widget ( i.e this unique root layout) 7 | // 2) the children of this root layout (i.e. widgets or sub-components). N.B.: the other sub-layouts possibly used in the root layout children tree are not in the component struct. 8 | // All composable widget and children widgets/sub-components have a unique field `component` corresponding to this Component structure. 9 | // All members (layout and children) are then all connected as ComponentChild having `component` field. 10 | // Remark: To become possibly a member of a parent component, a component has to have this field `component` to be connected to 11 | //--- 12 | 13 | const component_sep = '/' // ':::' 14 | 15 | pub interface ComponentChild { 16 | mut: 17 | id string 18 | component voidptr 19 | } 20 | 21 | // TODO: documentation 22 | pub fn component_connect(comp voidptr, children ...ComponentChild) { 23 | mut c := children.clone() 24 | for mut child in c { 25 | child.component = comp 26 | } 27 | } 28 | 29 | // to ensure homogeneity for name related to component 30 | pub fn component_id(id string, parts ...string) string { 31 | mut part_id := [id] 32 | part_id << parts.clone() 33 | return part_id.join(component_sep) 34 | } 35 | 36 | // TODO: documentation 37 | pub fn component_parent_id(part_id string) string { 38 | return part_id.split(component_sep)#[..-1].join(component_sep) 39 | } 40 | 41 | // TODO: documentation 42 | pub fn component_id_from(from_id string, id string) string { 43 | return component_id(component_parent_id(from_id), id) 44 | } 45 | 46 | // TODO: documentation 47 | pub fn component_parent_id_by(part_id string, level int) string { 48 | return part_id.split(component_sep)#[..-level].join(component_sep) 49 | } 50 | 51 | // TODO: documentation 52 | pub fn component_id_from_by(from_id string, level int, id string) string { 53 | return component_id(component_parent_id_by(from_id, level), id) 54 | } 55 | -------------------------------------------------------------------------------- /src/style_label.v: -------------------------------------------------------------------------------- 1 | module ui 2 | 3 | import gg 4 | import toml 5 | 6 | pub struct LabelStyle { 7 | pub mut: 8 | text_font_name string = 'system' 9 | text_color gg.Color 10 | text_size int = 16 11 | text_align TextHorizontalAlign = .left 12 | text_vertical_align TextVerticalAlign = .top 13 | } 14 | 15 | pub struct LabelStyleParams { 16 | WidgetTextStyleParams 17 | pub mut: 18 | style string = no_style 19 | } 20 | 21 | pub fn (ls LabelStyle) to_toml() string { 22 | mut toml_ := map[string]toml.Any{} 23 | toml_['text_font_name'] = ls.text_font_name 24 | toml_['text_color'] = hex_color(ls.text_color) 25 | toml_['text_size'] = ls.text_size 26 | toml_['text_align'] = int(ls.text_align) 27 | toml_['text_vertical_align'] = int(ls.text_vertical_align) 28 | return toml_.to_toml() 29 | } 30 | 31 | pub fn (mut ls LabelStyle) from_toml(a toml.Any) { 32 | ls.text_font_name = a.value('text_font_name').string() 33 | ls.text_color = HexColor(a.value('text_color').string()).color() 34 | ls.text_size = a.value('text_size').int() 35 | ls.text_align = unsafe { TextHorizontalAlign(a.value('text_align').int()) } 36 | ls.text_vertical_align = unsafe { TextVerticalAlign(a.value('text_vertical_align').int()) } 37 | } 38 | 39 | pub fn (mut l Label) load_style() { 40 | // println("btn load style $rect.theme_style") 41 | mut style := if l.theme_style == '' { l.ui.window.theme_style } else { l.theme_style } 42 | if l.style_params.style != no_style { 43 | style = l.style_params.style 44 | } 45 | l.update_theme_style(style) 46 | // forced overload default style 47 | l.update_style(l.style_params) 48 | } 49 | 50 | pub fn (mut l Label) update_theme_style(theme string) { 51 | // println("update_style <$p.style>") 52 | style := if theme == '' { 'default' } else { theme } 53 | if style != no_style && style in l.ui.styles { 54 | ls := l.ui.styles[style].label 55 | l.theme_style = theme 56 | mut dtw := DrawTextWidget(l) 57 | dtw.update_theme_style(ls) 58 | } 59 | } 60 | 61 | pub fn (mut l Label) update_style(p LabelStyleParams) { 62 | mut dtw := DrawTextWidget(l) 63 | dtw.update_theme_style_params(p) 64 | } 65 | -------------------------------------------------------------------------------- /webview/webview.v: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2021 Alexander Medvednikov. All rights reserved. 2 | // Use of this source code is governed by a MIT license that can be found in the LICENSE file. 3 | module webview 4 | 5 | type NavFinishedFn = fn (url string) 6 | 7 | @[heap] 8 | pub struct WebView { 9 | // widget ui.Widget 10 | url string 11 | mut: 12 | nav_finished_fn NavFinishedFn = unsafe { NavFinishedFn(nil) } 13 | pub: 14 | obj voidptr 15 | } 16 | 17 | pub struct Config { 18 | pub: 19 | url string 20 | title string 21 | // parent &ui.Window 22 | pub mut: 23 | nav_finished_fn NavFinishedFn = unsafe { NavFinishedFn(nil) } 24 | js_on_init string 25 | } 26 | 27 | pub fn new_window(cfg Config) &WebView { 28 | mut obj := unsafe { nil } 29 | $if macos { 30 | obj = C.new_darwin_web_view(cfg.url, cfg.title, cfg.js_on_init) 31 | } 32 | $if linux { 33 | create_linux_web_view(cfg.url, cfg.title) 34 | } 35 | $if windows { 36 | obj = C.new_windows_web_view(cfg.url.to_wide(), cfg.title.to_wide()) 37 | } 38 | return &WebView{ 39 | url: cfg.url 40 | obj: obj 41 | nav_finished_fn: cfg.nav_finished_fn 42 | } 43 | } 44 | 45 | pub fn exec(scriptSource string) { 46 | $if windows { 47 | C.exec(scriptSource.str) 48 | } 49 | } 50 | 51 | pub fn get_global_js_val() string { 52 | $if macos { 53 | return C.darwin_get_webview_js_val() 54 | } 55 | return '' 56 | } 57 | 58 | pub fn get_global_cookie_val() string { 59 | $if macos { 60 | return C.darwin_get_webview_cookie_val() 61 | } 62 | return '' 63 | } 64 | 65 | pub fn (mut wv WebView) on_navigate_fn(nav_callback fn (url string)) { 66 | wv.nav_finished_fn = nav_callback 67 | } 68 | 69 | pub fn (mut wv WebView) on_navigate(url string) { 70 | if wv.nav_finished_fn != unsafe { NavFinishedFn(nil) } { 71 | wv.nav_finished_fn(url) 72 | } 73 | } 74 | 75 | pub fn (mut wv WebView) navigate(url string) { 76 | $if windows { 77 | C.navigate(url.to_wide()) 78 | } 79 | wv.on_navigate(url) 80 | } 81 | 82 | pub fn (w &WebView) close() { 83 | $if macos { 84 | C.darwin_webview_close() 85 | } 86 | $if linux { 87 | // Untested: not sure! 88 | C.gtk_main_quit() 89 | } 90 | $if windows { 91 | C.windows_webview_close() 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/style_drawtextwidget.v: -------------------------------------------------------------------------------- 1 | module ui 2 | 3 | import gg 4 | 5 | // Embedded in most Widget Styles 6 | 7 | pub struct WidgetTextStyle { 8 | pub mut: 9 | text_font_name string = 'system' 10 | text_color gg.Color 11 | text_size int = 16 12 | text_align TextHorizontalAlign = .center 13 | text_vertical_align TextVerticalAlign = .middle 14 | } 15 | 16 | @[params] 17 | pub struct WidgetTextStyleParams { 18 | pub mut: 19 | // text_style TextStyle 20 | text_font_name string 21 | text_color gg.Color = no_color 22 | text_size f64 23 | text_align TextHorizontalAlign = .@none 24 | text_vertical_align TextVerticalAlign = .@none 25 | cursor_color gg.Color = no_color 26 | } 27 | 28 | // Style with Text 29 | 30 | interface DrawTextWidgetStyle { 31 | mut: 32 | text_font_name string 33 | text_color gg.Color 34 | text_size int 35 | text_align TextHorizontalAlign 36 | text_vertical_align TextVerticalAlign 37 | } 38 | 39 | interface DrawTextWidgetStyleParams { 40 | text_font_name string 41 | text_color gg.Color 42 | text_size f64 43 | text_align TextHorizontalAlign 44 | text_vertical_align TextVerticalAlign 45 | } 46 | 47 | pub fn (mut dtw DrawTextWidget) update_theme_style(ds DrawTextWidgetStyle) { 48 | dtw.update_style( 49 | font_name: ds.text_font_name 50 | color: ds.text_color 51 | size: ds.text_size 52 | align: ds.text_align 53 | vertical_align: ds.text_vertical_align 54 | ) 55 | } 56 | 57 | pub fn (mut dtw DrawTextWidget) update_theme_style_params(ds DrawTextWidgetStyleParams) { 58 | if ds.text_size > 0 { 59 | dtw.update_text_size(ds.text_size) 60 | } 61 | mut ts, mut ok := TextStyleParams{}, false 62 | if ds.text_font_name != '' { 63 | ok = true 64 | ts.font_name = ds.text_font_name 65 | } 66 | if ds.text_color != no_color { 67 | ok = true 68 | ts.color = ds.text_color 69 | } 70 | if ds.text_align != .@none { 71 | ok = true 72 | ts.align = ds.text_align 73 | } 74 | if ds.text_vertical_align != .@none { 75 | ok = true 76 | ts.vertical_align = ds.text_vertical_align 77 | } 78 | if ok { 79 | dtw.update_style(ts) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /examples/demo_textbox.v: -------------------------------------------------------------------------------- 1 | import ui 2 | import gg 3 | 4 | struct State { 5 | mut: 6 | tb1 string 7 | tb2m string 8 | tb3m string 9 | } 10 | 11 | fn main() { 12 | mut app := &State{ 13 | tb1: 'hggyjgyguguglul' 14 | tb2m: 'toto bbub jhuui jkhuhui hubhuib\ntiti tutu toto\ntata tata'.repeat(1000) 15 | tb3m: 'toto bbub jhuui jkhuhui hubhuib\ntiti tutu toto\ntata tata'.repeat(3) 16 | } 17 | lines := app.tb2m.split('\n') 18 | mut s := '' 19 | for l in lines { 20 | s += '${l}\n' 21 | } 22 | app.tb2m = s 23 | c := ui.column( 24 | widths: ui.stretch 25 | heights: [ui.compact, ui.compact, ui.stretch, ui.stretch, ui.stretch] 26 | margin_: 5 27 | spacing: 10 28 | children: [ 29 | ui.textbox( 30 | id: 'tb1' 31 | text: &app.tb1 32 | fitted_height: true 33 | ), 34 | ui.row( 35 | spacing: 5 36 | children: [ 37 | ui.label(text: 'Word wrap'), 38 | ui.switcher(open: false, id: 'sw2', on_click: on_switch_click), 39 | ui.switcher(open: false, id: 'sw2bis', on_click: on_switch_click), 40 | ui.switcher(open: false, id: 'sw3', on_click: on_switch_click), 41 | ] 42 | ), 43 | ui.textbox( 44 | mode: .multiline 45 | id: 'tb2m' 46 | text: &app.tb2m 47 | height: 200 48 | text_size: 24 49 | bg_color: gg.hex(0xfcf4e4ff) // gg.rgb(252, 244, 228) 50 | ), 51 | ui.textbox( 52 | mode: .read_only | .multiline 53 | id: 'tb2m-bis' 54 | text: &app.tb2m 55 | height: 200 56 | text_size: 24 57 | on_scroll_change: on_scroll_change 58 | ), 59 | ui.textbox( 60 | mode: .read_only | .multiline 61 | scrollview: false 62 | id: 'tb3m' 63 | text: &app.tb3m 64 | height: 200 65 | text_size: 24 66 | ), 67 | ] 68 | ) 69 | w := ui.window( 70 | width: 500 71 | height: 300 72 | mode: .resizable 73 | layout: c 74 | ) 75 | ui.run(w) 76 | } 77 | 78 | fn on_switch_click(switcher &ui.Switch) { 79 | tbs := match switcher.id { 80 | 'sw2' { 'tb2m' } 81 | 'sw2bis' { 'tb2m-bis' } 82 | else { 'tb3m' } 83 | } 84 | mut tb := switcher.ui.window.get_or_panic[ui.TextBox](tbs) 85 | tb.tv.switch_wordwrap() 86 | } 87 | 88 | fn on_scroll_change(sw ui.ScrollableWidget) { 89 | // println('sw cb example: $sw.id has scrollview? $sw.has_scrollview with x: $sw.x and y: $sw.y') 90 | } 91 | -------------------------------------------------------------------------------- /bin/assets/demos.json: -------------------------------------------------------------------------------- 1 | {"layouts/box_layout":"tb := ui.textbox(\n\tmode: .multiline\n\tbg_color: gx.yellow\n\ttext_value: 'blah blah blah\\n'.repeat(10)\n)\nlayout = ui.box_layout(\n\tid: 'bl'\n\tchildren: {\n\t\t'id1: (0,0) ++ (30,30)': ui.rectangle(\n\t\t\tcolor: gx.rgb(255, 100, 100)\n\t\t)\n\t\t'id2: (30,30) -> (-30.5,-30.5)': ui.rectangle(\n\t\t\tcolor: gx.rgb(100, 255, 100)\n\t\t)\n\t\t'id3: (0.5,0.5) -> (1,1)': tb\n\t\t'id4: (-30.5, -30.5) ++ (30,30)': ui.rectangle(\n\t\t\tcolor: gx.white\n\t\t)\n\t}\n)","layouts/box_layout_clipping":"layout = ui.box_layout(\n\tchildren: {\n\t\t\"bl1: (0,0) -> (0.4, 0.5)\": ui.box_layout(\n\t\t\tchildren: {\n\t\t\t\t\"bl1/rect: (0, 0) ++ (300, 300)\": ui.rectangle(color: gx.yellow)\n\t\t\t\t\"bl1/lab: (0, 0) ++ (300, 300)\": ui.label(\n\t\t\t\t\ttext: \"loooonnnnnggggg ttteeeeeeexxxxxxxtttttttttt\\nwoulbe clipped inside a boxlayout when reducing the window\"\n\t\t\t\t)\n\t\t\t}\n\t\t)\n\t\t\"bl2: (0.5,0.5) -> (0.9, 1)\": ui.box_layout(\n\t\t\tchildren: {\n\t\t\t\t\"bl2/rect: (0, 0) ++ (300, 300)\": ui.rectangle(color: gx.orange)\n\t\t\t\t\"bl2/lab: (0, 0) ++ (300, 300)\": ui.label(\n\t\t\t\t\ttext: \"clipped loooonnnnnggggg ttteeeeeeexxxxxxxtttttttttt\\nwoulbe clipped inside a boxlayout when reducing the window\"\n\t\t\t\t\tclipping: true\n\t\t\t\t)\n\t\t\t}\n\t\t)\n\t}\n)","widgets/button":"btn_click := fn (_ &ui.Button) { \n\t\tui.message_box('coucou toto!')\n}\nlayout = ui.box_layout(\n children: {\n 'btn: (0.2, 0.4) -> (0.5,0.5)': ui.button(\n text: 'show'\n on_click: fn (btn &ui.Button) {\n ui.message_box('Hi everybody !')\n }\n )\n\t'btn2: (0.7, 0.2) ++ (40,20)': ui.button(\n text: 'show2'\n on_click: btn_click\n\t)\n }\n)","widgets/label":"layout = ui.box_layout(\n children: {\n\t'rect: (0.2, 0.4) -> (0.5,0.5)': ui.rectangle(\n\t\tcolor: ui.alpha_colored(gx.yellow,30)\n\t),\n\t'rect2: (0.5, 0.5) -> (1,1)': ui.rectangle(\n\t\tcolor: ui.alpha_colored(gx.blue, 30)\n\t)\n\t'rect3: (0.1, 0.1) -> (0.3,0.2)': ui.rectangle(\n\t\tcolor: ui.alpha_colored(gx.orange, 30)\n\t),\n\t'lab: (0.2, 0.4) -> (0.5,0.5)': ui.label(\n\t\ttext: 'Centered text'\n\t\tjustify: ui.center // [0.5, 0.5]\n\t),\n 'lab2: (0.5, 0.5) -> (1,1)': ui.label(\n\t\ttext: 'Centered text\\n2nd line\\n3rd line'\n\t\tjustify: ui.top_center // [0.0, 0.5]\n\t),\n\t'lab3: (0.1, 0.1) -> (0.3,0.2)': ui.label(\n\t\ttext: 'long texttttttttttttttttttttttttttttttttt'\n\t\tclipping: true\n\t),\n }\n)"} -------------------------------------------------------------------------------- /component/subwindow_filebrowser.v: -------------------------------------------------------------------------------- 1 | module component 2 | 3 | import ui 4 | 5 | const filebrowser_subwindow_id = '_sw_filebrowser' 6 | const newfilebrowser_subwindow_id = '_sw_newfilebrowser' 7 | 8 | // Subwindow 9 | @[params] 10 | pub struct FileBrowserSubWindowParams { 11 | FileBrowserParams 12 | pub: 13 | x int 14 | y int 15 | } 16 | 17 | // TODO: documentation 18 | pub fn filebrowser_subwindow_add(mut w ui.Window, p FileBrowserSubWindowParams) { //}, fontchooser_lb_change ui.ListBoxSelectionChangedFn) { 19 | id := p.FileBrowserParams.id 20 | // only once 21 | if !ui.Layout(w).has_child_id(ui.component_id(id, filebrowser_subwindow_id)) { 22 | w.subwindows << ui.subwindow( 23 | id: ui.component_id(id, filebrowser_subwindow_id) 24 | x: p.x 25 | y: p.y 26 | layout: filebrowser_stack(p.FileBrowserParams) 27 | ) 28 | } 29 | } 30 | 31 | // TODO: documentation 32 | pub fn filebrowser_subwindow_visible(w &ui.Window, id string) { 33 | mut s := w.get_or_panic[ui.SubWindow](ui.component_id(id, filebrowser_subwindow_id)) 34 | s.set_visible(s.hidden) 35 | s.update_layout() 36 | } 37 | 38 | // TODO: documentation 39 | pub fn filebrowser_subwindow_close(w &ui.Window, id string) { 40 | mut s := w.get_or_panic[ui.SubWindow](ui.component_id(id, filebrowser_subwindow_id)) 41 | s.set_visible(false) 42 | s.update_layout() 43 | } 44 | 45 | // NewFile Browser 46 | 47 | // TODO: documentation 48 | pub fn newfilebrowser_subwindow_add(mut w ui.Window, p FileBrowserSubWindowParams) { //}, fontchooser_lb_change ui.ListBoxSelectionChangedFn) { 49 | // only once 50 | if !ui.Layout(w).has_child_id(ui.component_id(p.id, newfilebrowser_subwindow_id)) { 51 | p2 := FileBrowserParams{ 52 | ...p.FileBrowserParams 53 | with_fpath: true 54 | text_ok: 'New' 55 | } 56 | w.subwindows << ui.subwindow( 57 | id: ui.component_id(p.id, newfilebrowser_subwindow_id) 58 | x: p.x 59 | y: p.y 60 | layout: filebrowser_stack(p2) 61 | ) 62 | } 63 | } 64 | 65 | // TODO: documentation 66 | pub fn newfilebrowser_subwindow_visible(w &ui.Window, id string) { 67 | mut s := w.get_or_panic[ui.SubWindow](ui.component_id(id, newfilebrowser_subwindow_id)) 68 | s.set_visible(s.hidden) 69 | s.update_layout() 70 | } 71 | 72 | // TODO: documentation 73 | pub fn newfilebrowser_subwindow_close(w &ui.Window, id string) { 74 | mut s := w.get_or_panic[ui.SubWindow](ui.component_id(id, newfilebrowser_subwindow_id)) 75 | s.set_visible(false) 76 | s.update_layout() 77 | } 78 | -------------------------------------------------------------------------------- /src/window_manager.v: -------------------------------------------------------------------------------- 1 | module ui 2 | 3 | import gg 4 | import os 5 | 6 | pub enum WMMode { 7 | subwindow 8 | free 9 | tiling 10 | } 11 | 12 | @[heap] 13 | pub struct WindowManager { 14 | pub mut: 15 | // inside an unique sokol Window 16 | window &Window = unsafe { nil } 17 | apps []Application 18 | layout &BoxLayout = unsafe { nil } 19 | windows []&SubWindow 20 | kind WMMode 21 | } 22 | 23 | @[params] 24 | pub struct WindowManagerParams { 25 | WindowParams 26 | pub: 27 | scrollview bool 28 | kind WMMode 29 | apps map[string]Application 30 | } 31 | 32 | // ui.Window would play the role of WindowManager 33 | 34 | pub fn wm(cfg WindowManagerParams) &WindowManager { 35 | mut wm := &WindowManager{ 36 | kind: cfg.kind 37 | } 38 | wm.layout = box_layout(id: 'wm_layout', scrollview: cfg.scrollview) 39 | wm.window = window(cfg.WindowParams) 40 | wm.window.resizable = true 41 | wm.window.bg_color = gg.orange 42 | wm.window.title = 'VWM' 43 | mut bg := rectangle(color: gg.orange) 44 | wm.layout.set_child_bounding('bg: stretch', mut bg) 45 | wm.window.children = [wm.layout] 46 | wm.window.on_init = fn [mut wm] (mut win Window) { 47 | // last subwindow as active 48 | if wm.kind == .subwindow { 49 | mut subw := wm.windows.last() 50 | subw.as_top_subwindow() 51 | } 52 | for mut app in wm.apps { 53 | if app.on_init != unsafe { WindowFn(0) } { 54 | app.on_init(win) 55 | } 56 | } 57 | win.update_layout() 58 | } 59 | // add declared app 60 | for key, app in cfg.apps { 61 | mut mut_app := app 62 | wm.add(key, mut mut_app) 63 | } 64 | return wm 65 | } 66 | 67 | pub fn (mut wm WindowManager) run() { 68 | run(wm.window) 69 | } 70 | 71 | pub fn (mut wm WindowManager) add(key string, mut app Application) { 72 | wm.apps << app 73 | match wm.kind { 74 | .subwindow { 75 | mut subw := subwindow(id: os.join_path(app.id, 'win'), layout: app.layout) 76 | subw.is_top_wm = true 77 | wm.window.is_wm_mode = true 78 | wm.windows << subw 79 | wm.layout.set_child_bounding(key, mut subw) 80 | } 81 | .free, .tiling { 82 | mut l := app.layout() 83 | wm.layout.set_child_bounding(key, mut l) 84 | } 85 | } 86 | } 87 | 88 | pub fn (mut wm WindowManager) add_window_shortcuts(shortcuts map[string]WindowFn) { 89 | mut sc := Shortcutable(wm.window) 90 | for shortcut, callback in shortcuts { 91 | sc.add_shortcut(shortcut, callback) 92 | } 93 | } 94 | 95 | pub fn id(id string, ids ...string) string { 96 | return os.join_path(id, ...ids) 97 | } 98 | -------------------------------------------------------------------------------- /component/fontchooser.v: -------------------------------------------------------------------------------- 1 | module component 2 | 3 | import ui 4 | import os 5 | 6 | const fontchooser_row_id = '_row_sw_font' 7 | const fontchooser_lb_id = '_lb_sw_font' 8 | 9 | @[heap] 10 | pub struct FontChooserComponent { 11 | pub mut: 12 | layout &ui.Stack = unsafe { nil } // required 13 | dtw ui.DrawTextWidget 14 | } 15 | 16 | @[params] 17 | pub struct FontChooserParams { 18 | pub: 19 | id string = fontchooser_lb_id 20 | draw_lines bool = true 21 | dtw &ui.DrawTextWidget = ui.canvas_plus() // since it requires an intialisation 22 | } 23 | 24 | // TODO: documentation 25 | pub fn fontchooser_stack(c FontChooserParams) &ui.Stack { 26 | mut lb := ui.listbox( 27 | id: c.id 28 | scrollview: true 29 | draw_lines: c.draw_lines 30 | on_change: fontchooser_lb_change 31 | ) 32 | fontchooser_add_fonts_items(mut lb) 33 | mut layout := ui.row( 34 | id: fontchooser_row_id 35 | widths: ui.stretch 36 | heights: 200.0 37 | children: [lb] 38 | ) 39 | mut fc := &FontChooserComponent{ 40 | layout: layout 41 | dtw: c.dtw 42 | } 43 | ui.component_connect(fc, layout, lb) 44 | layout.on_init = fontchooser_init 45 | return layout 46 | } 47 | 48 | // TODO: documentation 49 | pub fn fontchooser_component(w ui.ComponentChild) &FontChooserComponent { 50 | return unsafe { &FontChooserComponent(w.component) } 51 | } 52 | 53 | // TODO: documentation 54 | pub fn fontchooser_component_from_id(w ui.Window, id string) &FontChooserComponent { 55 | return fontchooser_component(w.get_or_panic[ui.Stack](ui.component_id(id, 'layout'))) 56 | } 57 | 58 | // TODO: documentation 59 | pub fn fontchooser_listbox(w &ui.Window) &ui.ListBox { 60 | return w.get_or_panic[ui.ListBox](fontchooser_lb_id) 61 | } 62 | 63 | fn fontchooser_init(mut layout ui.Stack) { 64 | // println("${layout.size()}") 65 | layout.update_layout() 66 | } 67 | 68 | fn fontchooser_add_fonts_items(mut lb ui.ListBox) { 69 | font_paths := ui.font_path_list() 70 | 71 | for fp in font_paths { 72 | lb.append_item(fp, os.file_name(fp), 0) 73 | } 74 | } 75 | 76 | // TODO: documentation 77 | pub fn fontchooser_connect(w &ui.Window, dtw ui.DrawTextWidget) { 78 | fc_layout := w.get_or_panic[ui.Stack](fontchooser_row_id) 79 | mut fc := fontchooser_component(fc_layout) 80 | fc.dtw = dtw 81 | } 82 | 83 | fn fontchooser_lb_change(lb &ui.ListBox) { 84 | mut w := lb.ui.window 85 | fc := fontchooser_component(lb) 86 | // println('fc_lb_change: $lb.id') 87 | mut dtw := ui.DrawTextWidget(fc.dtw) 88 | fp, id := lb.selected() or { 'classic', '' } 89 | // println("$id, $fp") 90 | w.add_font(id, fp) 91 | 92 | dtw.update_style(font_name: id) 93 | } 94 | -------------------------------------------------------------------------------- /src/style_progressbar.v: -------------------------------------------------------------------------------- 1 | module ui 2 | 3 | import gg 4 | import toml 5 | 6 | // ProgressBar 7 | 8 | pub struct ProgressBarStyle { 9 | pub mut: 10 | color gg.Color 11 | border_color gg.Color 12 | bg_color gg.Color 13 | bg_border_color gg.Color 14 | } 15 | 16 | @[params] 17 | pub struct ProgressBarStyleParams { 18 | pub mut: 19 | style string = no_style 20 | color gg.Color = no_color 21 | border_color gg.Color = no_color 22 | bg_color gg.Color = no_color 23 | bg_border_color gg.Color = no_color 24 | } 25 | 26 | pub fn progressbar_style(p ProgressBarStyleParams) ProgressBarStyleParams { 27 | return p 28 | } 29 | 30 | pub fn (pbs ProgressBarStyle) to_toml() string { 31 | mut toml_ := map[string]toml.Any{} 32 | toml_['color'] = hex_color(pbs.color) 33 | toml_['border_color'] = hex_color(pbs.border_color) 34 | toml_['bg_color'] = hex_color(pbs.bg_color) 35 | toml_['bg_border_color'] = hex_color(pbs.bg_border_color) 36 | return toml_.to_toml() 37 | } 38 | 39 | pub fn (mut pbs ProgressBarStyle) from_toml(a toml.Any) { 40 | pbs.color = HexColor(a.value('color').string()).color() 41 | pbs.border_color = HexColor(a.value('border_color').string()).color() 42 | pbs.bg_color = HexColor(a.value('bg_color').string()).color() 43 | pbs.bg_border_color = HexColor(a.value('bg_border_color').string()).color() 44 | } 45 | 46 | fn (mut pb ProgressBar) load_style() { 47 | // println("pgbar load style $pb.theme_style") 48 | mut style := if pb.theme_style == '' { pb.ui.window.theme_style } else { pb.theme_style } 49 | if pb.style_params.style != no_style { 50 | style = pb.style_params.style 51 | } 52 | pb.update_theme_style(style) 53 | // forced overload default style 54 | pb.update_style(pb.style_params) 55 | } 56 | 57 | pub fn (mut pb ProgressBar) update_theme_style(theme string) { 58 | // println("update_style <$p.style>") 59 | style := if theme == '' { 'default' } else { theme } 60 | if style != no_style && style in pb.ui.styles { 61 | pbs := pb.ui.styles[style].pgbar 62 | pb.theme_style = theme 63 | pb.style.color = pbs.color 64 | pb.style.border_color = pbs.border_color 65 | pb.style.bg_color = pbs.bg_color 66 | pb.style.bg_border_color = pbs.bg_border_color 67 | } 68 | } 69 | 70 | pub fn (mut pb ProgressBar) update_style(p ProgressBarStyleParams) { 71 | if p.color != no_color { 72 | pb.style.color = p.color 73 | } 74 | if p.border_color != no_color { 75 | pb.style.border_color = p.border_color 76 | } 77 | if p.bg_color != no_color { 78 | pb.style.bg_color = p.bg_color 79 | } 80 | if p.bg_border_color != no_color { 81 | pb.style.bg_border_color = p.bg_border_color 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/tool_calculate.v: -------------------------------------------------------------------------------- 1 | module ui 2 | 3 | import regex 4 | 5 | // calculation of very simple numeric expression based on regex 6 | // no parenthesis only digit real number 7 | 8 | pub struct MiniCalc { 9 | mut: 10 | op_re []regex.RE 11 | paren_re regex.RE 12 | pub mut: 13 | formula string 14 | res_str string 15 | res f32 16 | } 17 | 18 | pub fn mini_calc() MiniCalc { 19 | mut mc := MiniCalc{} 20 | for op in [r'\*', r'/', r'\+', r'\-'] { 21 | query := r'(\-?[\d\.]+)\s*(' + op + r')\s*(\-?[\d\.]+)' 22 | mc.op_re << regex.regex_opt(query) or { panic(err) } 23 | } 24 | mc.paren_re = regex.regex_opt(r'\(\s*([\d\.\+\-\*/]+)\s*\)') or { panic(err) } 25 | return mc 26 | } 27 | 28 | fn compute_repl(re regex.RE, in_txt string, start int, end int) string { 29 | left := re.get_group_by_id(in_txt, 0) 30 | op := re.get_group_by_id(in_txt, 1) 31 | right := re.get_group_by_id(in_txt, 2) 32 | // println("<$left> <$op> <$right>") 33 | res := match op { 34 | '*' { left.f32() * right.f32() } 35 | '/' { left.f32() / right.f32() } 36 | '+' { left.f32() + right.f32() } 37 | '-' { left.f32() - right.f32() } 38 | else { f32(0) } 39 | } 40 | return res.str() 41 | } 42 | 43 | pub fn (mut mc MiniCalc) calculate(formula string) f32 { 44 | mc.formula = formula 45 | mc.res_str = formula 46 | for { 47 | if mc.res_str.contains_any('()') { 48 | // simplify parenthesis 49 | mc.simplify_paren() 50 | } else { 51 | break 52 | } 53 | } 54 | // compute 55 | mc.compute_ops() 56 | mc.res = mc.res_str.f32() 57 | return mc.res 58 | } 59 | 60 | fn (mut mc MiniCalc) compute_ops() { 61 | for i in 0 .. 4 { 62 | for { 63 | // println("prop: $result $op ($query)") 64 | start, _ := mc.op_re[i].find(mc.res_str) 65 | if start >= 0 { 66 | $if mini_calc ? { 67 | print('res: ${['*', '/', '+', '-'][i]} -> ${mc.res_str}') 68 | } 69 | mc.res_str = mc.op_re[i].replace_by_fn(mc.res_str, compute_repl) 70 | $if mini_calc ? { 71 | println(' => ${mc.res_str}') 72 | } 73 | } else { 74 | break 75 | } 76 | } 77 | } 78 | } 79 | 80 | fn (mut mc MiniCalc) simplify_paren() { 81 | for { 82 | start, stop := mc.paren_re.find(mc.res_str) 83 | if start >= 0 { 84 | // print("res: $op -> $result") 85 | formula := mc.res_str[(start + 1)..(stop - 1)] 86 | // if formula.contains_any("+-*/") { 87 | mut mc_expr := mini_calc() 88 | _ := mc_expr.calculate(formula) 89 | mc.res_str = mc.paren_re.replace_simple(mc.res_str, mc_expr.res_str) 90 | // } else { 91 | // mc.res_str = mc.paren_re.replace_simple(mc.res_str,r'\0') 92 | // } 93 | // println("=> $result") 94 | } else { 95 | break 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/tool_message_dialog.v: -------------------------------------------------------------------------------- 1 | module ui 2 | 3 | import gg 4 | 5 | //=== Basic Message Dialog ===/ 6 | // Before sokol deals with multiple window (soon) 7 | 8 | fn (mut win Window) add_message_dialog() { 9 | mut dlg := column( 10 | id: '_msg_dlg_col' 11 | alignment: .center 12 | widths: compact 13 | heights: compact 14 | spacing: 10 15 | margin: Margin{5, 5, 5, 5} 16 | bg_color: gg.Color{140, 210, 240, 100} 17 | bg_radius: .3 18 | children: [ 19 | label(id: '_msg_dlg_lab', text: ' Hello World'), 20 | button( 21 | id: '_msg_dlg_btn' 22 | text: 'OK' 23 | width: 100 24 | radius: .3 25 | z_index: 1000 26 | on_click: message_dialog_click 27 | ), 28 | ] 29 | ) 30 | dlg.is_root_layout = false 31 | win.children << dlg 32 | dlg.set_visible(false) 33 | } 34 | 35 | fn message_dialog_click(b &Button) { 36 | mut dlg := b.ui.window.get_or_panic[Stack]('_msg_dlg_col') 37 | dlg.set_visible(false) 38 | } 39 | 40 | pub fn (win &Window) message(s string) { 41 | if win.native_message { 42 | message_box(s) 43 | } else { 44 | mut dlg := win.get_or_panic[Stack]('_msg_dlg_col') 45 | mut msg := win.get_or_panic[Label]('_msg_dlg_lab') 46 | msg.set_text(s) 47 | mut tw, mut th := text_lines_size(s.split('\n'), win.ui) 48 | msg.propose_size(tw, th) 49 | if tw < 200 { 50 | tw = 200 51 | } 52 | th += 50 53 | // println("msg: ($tw, $th) $s") 54 | dlg.propose_size(tw, th) 55 | ww, wh := win.size() 56 | dlg.set_pos(ww / 2 - tw / 2, wh / 2 - th / 2) 57 | dlg.set_visible(true) 58 | dlg.update_layout() 59 | } 60 | } 61 | 62 | /* 63 | // Playing with Styled Text 64 | 65 | struct TextChunk { 66 | text string 67 | start int 68 | stop int 69 | cfg gg.TextCfg 70 | } 71 | 72 | pub struct TextContext { 73 | chunks []TextChunk 74 | colors map[string]gg.Color 75 | styles map[string]gg.TextCfg 76 | } 77 | 78 | struct TextView { 79 | x int 80 | y int 81 | width int 82 | height int 83 | context &TextContext = unsafe { nil } 84 | } 85 | 86 | 87 | * default: {style: "", size: 10, color: black} 88 | 89 | * start: 90 | 91 | - style: normal "", italic {i], bold {b], underline {u] 92 | - size: uint8 (ex: {12]) 93 | - color: r,g,b,a or hexa (0x00000000) string lowercase (ex: {red]) 94 | - font-family: string capitalized 95 | 96 | - combined: {...|...|...] 97 | 98 | end: 99 | 100 | - idem with closing [...} or [...|...|...} 101 | - empty [} means last opened 102 | 103 | 104 | current: 105 | 106 | custom style: blurr 107 | 108 | stack of style operations: 109 | 110 | {b] {t] [b} [t} 111 | */ 112 | -------------------------------------------------------------------------------- /examples/textbox_input/textbox_demo_android.c.v: -------------------------------------------------------------------------------- 1 | import ui 2 | import jni 3 | import jni.auto 4 | 5 | const pkg = 'io.v.android.ui.VUIActivity' 6 | 7 | fn (mut app App) init(window &ui.Window) { 8 | // Pass app reference off to Java so we 9 | // can get it back in the V callback "on_soft_input" 10 | 11 | // TODO: test if this is still valid 12 | app_ref := i64(&app) // OLD: i64(window.state) 13 | auto.call_static_method(pkg + '.setVAppPointer(long) void', app_ref) 14 | 15 | app.show_soft_input() 16 | // show_soft_input(mut app) 17 | } 18 | 19 | @[export: 'JNI_OnLoad'] 20 | fn jni_on_load(vm &jni.JavaVM, reserved voidptr) int { 21 | jni.set_java_vm(vm) 22 | $if android { 23 | // V consts - can't be used since `JNI_OnLoad` 24 | // is called by the Java VM before the lib 25 | // with V's init code is loaded and called. 26 | jni.setup_android('io.v.android.ui.VUIActivity') 27 | } 28 | return int(jni.Version.v1_6) 29 | } 30 | 31 | // on_soft_input is exported to match the name for the native Java activity VUIActivity's method: 32 | // "public native void onSoftInput(long app, String s, int start, int before, int count);". 33 | // `app_ptr` is the pointer to the `struct App` instance pointer store in an `i64` (long in Java) 34 | // it needs to be cast back to it's original type since Java has no concept of pointers. 35 | // The method is called in Java to notify you that: 36 | // within `jstr`, the `count` characters beginning at `start` have just replaced old text that had `length` before. 37 | @[export: 'JNICALL Java_io_v_android_ui_VUIActivity_onSoftInput'] 38 | fn on_soft_input(env &jni.Env, thiz jni.JavaObject, app_ptr i64, jstr jni.JavaString, start int, before int, count int) { 39 | if app_ptr == 0 { 40 | return 41 | } 42 | 43 | mut app := &App(app_ptr) 44 | 45 | buffer := jni.j2v_string(env, jstr) 46 | println(@MOD + '.' + @FN + ': "${buffer}" (${start},${before},${count})') 47 | 48 | mut char_code := u8(0) 49 | mut char_literal := '' 50 | 51 | mut pos := start + before 52 | if pos >= 0 && pos <= buffer.len { 53 | char_code = u8(buffer[pos]) 54 | char_literal = char_code.ascii_str() 55 | } 56 | println(@MOD + '.' + @FN + ': input "${char_literal}"') 57 | 58 | app.soft_input_buffer = buffer 59 | app.soft_input_parsed_char = char_literal 60 | 61 | app.tb = app.soft_input_buffer 62 | app.window.refresh() 63 | } 64 | 65 | fn (mut a App) show_soft_input() { 66 | auto.call_static_method(pkg + '.showSoftInput()') 67 | auto.call_static_method(pkg + '.setSoftInputBuffer(string)', a.soft_input_buffer) 68 | a.soft_input_visible = true 69 | } 70 | 71 | fn (mut a App) hide_soft_input() { 72 | auto.call_static_method(pkg + '.hideSoftInput()') 73 | a.soft_input_visible = false 74 | } 75 | -------------------------------------------------------------------------------- /examples/transitions.v: -------------------------------------------------------------------------------- 1 | import ui 2 | import os 3 | 4 | const win_width = 500 5 | const win_height = 500 6 | const picture_width_and_height = 100 7 | 8 | @[heap] 9 | struct App { 10 | mut: 11 | window &ui.Window = unsafe { nil } 12 | x_transition &ui.Transition 13 | y_transition &ui.Transition 14 | picture &ui.Picture 15 | button &ui.Button = unsafe { nil } 16 | state int 17 | } 18 | 19 | fn main() { 20 | mut logo := os.resource_abs_path(os.join_path('../assets/img', 'logo.png')) 21 | $if android { 22 | logo = 'img/logo.png' 23 | } 24 | mut app := &App{ 25 | x_transition: ui.transition(duration: 750, easing: ui.easing(.ease_in_out_cubic)) 26 | y_transition: ui.transition(duration: 750, easing: ui.easing(.ease_in_out_quart)) 27 | picture: ui.picture( 28 | width: picture_width_and_height 29 | height: picture_width_and_height 30 | path: logo 31 | movable: true 32 | on_click: example_pic_click 33 | ) 34 | } 35 | app.button = ui.button(text: 'Slide', on_click: app.btn_toggle_click, movable: true) 36 | app.window = ui.window( 37 | width: win_width 38 | height: win_height 39 | title: 'V UI Demo' 40 | mode: .resizable 41 | children: [ 42 | ui.column( 43 | widths: ui.compact // or ui.compact 44 | margin: ui.Margin{25, 25, 25, 25} 45 | children: [app.button, app.picture] 46 | ), 47 | app.x_transition, 48 | app.y_transition, 49 | ] 50 | ) 51 | ui.run(app.window) 52 | } 53 | 54 | fn example_pic_click(pic &ui.Picture) { 55 | println('Clicked pic') 56 | } 57 | 58 | fn (mut app App) btn_toggle_click(button &ui.Button) { 59 | if app.x_transition.animated_value == 0 || app.y_transition.animated_value == 0 { 60 | app.x_transition.set_value(&app.picture.offset_x) 61 | app.y_transition.set_value(&app.picture.offset_y) 62 | } 63 | w, h := app.window.size() 64 | match app.state { 65 | 0 { 66 | app.x_transition.target_value = 32 67 | app.y_transition.target_value = 32 68 | app.state = 1 69 | } 70 | 1 { 71 | app.x_transition.target_value = w - (picture_width_and_height + 32) 72 | app.y_transition.target_value = h - (picture_width_and_height + 32) 73 | app.state = 2 74 | } 75 | 2 { 76 | app.x_transition.target_value = w - (picture_width_and_height + 32) 77 | app.y_transition.target_value = 32 78 | app.state = 3 79 | } 80 | 3 { 81 | app.x_transition.target_value = 32 82 | app.y_transition.target_value = h - (picture_width_and_height + 32) 83 | app.state = 4 84 | } 85 | 4 { 86 | app.x_transition.target_value = w / 2 - (picture_width_and_height / 2) 87 | app.y_transition.target_value = h / 2 - (picture_width_and_height / 2) 88 | app.state = 0 89 | } 90 | else { 91 | app.state = 0 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/tool_easing.v: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2022 Leah Lundqvist. All rights reserved. 2 | // Use of this source code is governed by a MIT license 3 | // that can be found in the LICENSE file. 4 | module ui 5 | 6 | pub type EasingFunction = fn (arg_1 f64) f64 7 | 8 | pub enum EasingType { 9 | linear 10 | ease_in_quad 11 | ease_out_quad 12 | ease_in_out_quad 13 | ease_in_cubic 14 | ease_out_cubic 15 | ease_in_out_cubic 16 | ease_in_quart 17 | ease_out_quart 18 | ease_in_out_quart 19 | ease_in_quint 20 | ease_out_quint 21 | ease_in_out_quint 22 | } 23 | 24 | fn linear(x f64) f64 { 25 | return x 26 | } 27 | 28 | fn ease_in_quad(x f64) f64 { 29 | return x * x 30 | } 31 | 32 | fn ease_out_quad(x f64) f64 { 33 | return x * (2.0 - x) 34 | } 35 | 36 | fn ease_in_out_quad(x f64) f64 { 37 | return if x < .5 { 2.0 * x * x } else { -1.0 + (4.0 - 2.0 * x) * x } 38 | } 39 | 40 | fn ease_in_cubic(x f64) f64 { 41 | return x * x * x 42 | } 43 | 44 | fn ease_out_cubic(x f64) f64 { 45 | return (x - 1.0) * (x - 1.0) * (x - 1.0) + 1 46 | } 47 | 48 | fn ease_in_out_cubic(x f64) f64 { 49 | return if x < .5 { 4.0 * x * x * x } else { (x - 1.0) * (2.0 * x - 2.0) * (2.0 * x - 2.0) + 1.0 } 50 | } 51 | 52 | fn ease_in_quart(x f64) f64 { 53 | return x * x * x * x 54 | } 55 | 56 | fn ease_out_quart(x f64) f64 { 57 | return 1.0 - (x - 1.0) * (x - 1.0) * (x - 1.0) * (x - 1.0) 58 | } 59 | 60 | fn ease_in_out_quart(x f64) f64 { 61 | return if x < 0.5 { 62 | 8.0 * x * x * x * x 63 | } else { 64 | 1.0 - 8.0 * (x - 1.0) * (x - 1.0) * (x - 1.0) * (x - 1.0) 65 | } 66 | } 67 | 68 | fn ease_in_quint(x f64) f64 { 69 | return x * x * x * x * x 70 | } 71 | 72 | fn ease_out_quint(x f64) f64 { 73 | return 1.0 + (x - 1.0) * (x - 1.0) * (x - 1.0) * (x - 1.0) * (x - 1.0) 74 | } 75 | 76 | fn ease_in_out_quint(x f64) f64 { 77 | return if x < 0.5 { 78 | 16.0 * x * x * x * x * x 79 | } else { 80 | 1.0 + 16.0 * (x - 1.0) * (x - 1.0) * (x - 1.0) * (x - 1.0) * (x - 1.0) 81 | } 82 | } 83 | 84 | pub fn easing(easingtype EasingType) EasingFunction { 85 | match easingtype { 86 | .linear { return linear } 87 | .ease_in_quad { return ease_in_quad } 88 | .ease_out_quad { return ease_out_quad } 89 | .ease_in_out_quad { return ease_in_out_quad } 90 | .ease_in_cubic { return ease_in_cubic } 91 | .ease_out_cubic { return ease_out_cubic } 92 | .ease_in_out_cubic { return ease_in_out_cubic } 93 | .ease_in_quart { return ease_in_quart } 94 | .ease_out_quart { return ease_out_quart } 95 | .ease_in_out_quart { return ease_in_out_quart } 96 | .ease_in_quint { return ease_in_quint } 97 | .ease_out_quint { return ease_out_quint } 98 | .ease_in_out_quint { return ease_in_out_quint } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/canvas.v: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2022 Alexander Medvednikov. All rights reserved. 2 | // Use of this source code is governed by a MIT license 3 | // that can be found in the LICENSE file. 4 | module ui 5 | 6 | import gg 7 | 8 | pub type DrawFn = fn (ctx &gg.Context, c &Canvas) // x_offset int, y_offset int) 9 | 10 | @[heap] 11 | pub struct Canvas { 12 | pub mut: 13 | id string 14 | width int 15 | height int 16 | x int 17 | y int 18 | offset_x int 19 | offset_y int 20 | z_index int 21 | ui &UI = unsafe { nil } 22 | hidden bool 23 | clipping bool 24 | // component state for composable widget 25 | component voidptr 26 | mut: 27 | parent Layout = empty_stack 28 | draw_fn DrawFn = unsafe { nil } 29 | } 30 | 31 | @[params] 32 | pub struct CanvasParams { 33 | pub: 34 | id string 35 | width int 36 | height int 37 | z_index int 38 | text string 39 | draw_fn DrawFn = unsafe { nil } 40 | clipping bool 41 | } 42 | 43 | pub fn canvas(c CanvasParams) &Canvas { 44 | mut canvas := &Canvas{ 45 | id: c.id 46 | width: c.width 47 | height: c.height 48 | z_index: c.z_index 49 | draw_fn: c.draw_fn 50 | clipping: c.clipping 51 | } 52 | return canvas 53 | } 54 | 55 | fn (mut c Canvas) init(parent Layout) { 56 | c.parent = parent 57 | u := parent.get_ui() 58 | c.ui = u 59 | } 60 | 61 | @[manualfree] 62 | pub fn (mut c Canvas) cleanup() { 63 | unsafe { c.free() } 64 | } 65 | 66 | @[unsafe] 67 | pub fn (c &Canvas) free() { 68 | $if free ? { 69 | print('canvas ${c.id}') 70 | } 71 | unsafe { 72 | c.id.free() 73 | free(c) 74 | } 75 | $if free ? { 76 | println(' -> freed') 77 | } 78 | } 79 | 80 | fn (mut c Canvas) set_pos(x int, y int) { 81 | c.x = x 82 | c.y = y 83 | } 84 | 85 | fn (mut c Canvas) size() (int, int) { 86 | return c.width, c.height 87 | } 88 | 89 | fn (mut c Canvas) propose_size(w int, h int) (int, int) { 90 | c.width = w 91 | c.height = h 92 | return c.width, c.height 93 | } 94 | 95 | fn (mut c Canvas) draw() { 96 | c.draw_device(mut c.ui.dd) 97 | } 98 | 99 | fn (mut c Canvas) draw_device(mut d DrawDevice) { 100 | if c.hidden { 101 | return 102 | } 103 | offset_start(mut c) 104 | defer { 105 | offset_end(mut c) 106 | } 107 | cstate := clipping_start(c, mut d) or { return } 108 | defer { 109 | clipping_end(c, mut d, cstate) 110 | } 111 | 112 | if c.draw_fn != unsafe { nil } { 113 | if mut c.ui.dd is DrawDeviceContext { 114 | c.draw_fn(&c.ui.dd.Context, c) 115 | } 116 | } 117 | } 118 | 119 | fn (mut c Canvas) set_visible(state bool) { 120 | c.hidden = !state 121 | } 122 | 123 | fn (c &Canvas) point_inside(x f64, y f64) bool { 124 | return point_inside(c, x, y) 125 | } 126 | -------------------------------------------------------------------------------- /src/interface_shortcut.v: -------------------------------------------------------------------------------- 1 | module ui 2 | 3 | // Adding shortcuts field for a Widget or Component (having id field) makes it react as user-dedined shortcuts 4 | // see tool_key for parsing shortcut as string 5 | 6 | pub interface Shortcutable { 7 | id string 8 | mut: 9 | shortcuts Shortcuts 10 | } 11 | 12 | // TODO: documentation 13 | pub fn (mut s Shortcutable) add_shortcut(shortcut string, key_fn ShortcutFn) { 14 | mods, code, key := parse_shortcut(shortcut) 15 | if code == 0 { 16 | s.shortcuts.chars[key] = Shortcut{ 17 | mods: mods 18 | key_fn: key_fn 19 | } 20 | } else { 21 | s.shortcuts.keys[code] = Shortcut{ 22 | mods: mods 23 | key_fn: key_fn 24 | } 25 | } 26 | } 27 | 28 | // TODO: documentation 29 | pub fn (mut s Shortcutable) add_shortcut_context(shortcut string, context voidptr) { 30 | _, code, key := parse_shortcut(shortcut) 31 | if code == 0 { 32 | unsafe { 33 | s.shortcuts.chars[key].context = context 34 | } 35 | } else { 36 | unsafe { 37 | s.shortcuts.keys[code].context = context 38 | } 39 | } 40 | } 41 | 42 | // TODO: documentation 43 | pub fn (mut s Shortcutable) add_shortcut_with_context(shortcut string, key_fn ShortcutFn, context voidptr) { 44 | s.add_shortcut(shortcut, key_fn) 45 | s.add_shortcut_context(shortcut, context) 46 | } 47 | 48 | // This provides user defined shortcut actions (see grid and grid_data as a use case) 49 | pub type ShortcutFn = fn (context voidptr) 50 | 51 | pub type KeyShortcuts = map[int]Shortcut 52 | pub type CharShortcuts = map[string]Shortcut 53 | 54 | pub struct Shortcuts { 55 | pub mut: 56 | keys KeyShortcuts 57 | chars CharShortcuts 58 | } 59 | 60 | pub struct Shortcut { 61 | pub mut: 62 | mods KeyMod 63 | key_fn ShortcutFn = unsafe { nil } 64 | context voidptr 65 | } 66 | 67 | // TODO: documentation 68 | pub fn char_shortcut(e KeyEvent, shortcuts Shortcuts, context voidptr) { 69 | // weirdly when .ctrl modifier the codepoint is differently interpreted 70 | mut s := utf32_to_str(e.codepoint) 71 | $if macos { 72 | if e.mods == .ctrl { 73 | s = rune(96 + e.codepoint).str() 74 | } 75 | } 76 | if sc := shortcuts.chars[s] { 77 | if has_key_mods(e.mods, sc.mods) { 78 | if sc.context != unsafe { nil } { 79 | sc.key_fn(sc.context) 80 | } else { 81 | sc.key_fn(context) 82 | } 83 | } 84 | } 85 | } 86 | 87 | // TODO: documentation 88 | pub fn key_shortcut(e KeyEvent, shortcuts Shortcuts, context voidptr) { 89 | // println("key_shortcut ${int(e.key)}") 90 | ikey := int(e.key) 91 | if sc := shortcuts.keys[ikey] { 92 | if has_key_mods(e.mods, sc.mods) { 93 | if sc.context != unsafe { nil } { 94 | sc.key_fn(sc.context) 95 | } else { 96 | sc.key_fn(context) 97 | } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/style_slider.v: -------------------------------------------------------------------------------- 1 | module ui 2 | 3 | import gg 4 | import toml 5 | 6 | // Slider 7 | 8 | pub struct SliderStyle { 9 | pub mut: 10 | thumb_color gg.Color = gg.rgb(87, 153, 245) 11 | bg_color gg.Color = gg.rgb(219, 219, 219) 12 | bg_border_color gg.Color = gg.rgb(191, 191, 191) 13 | focused_bg_border_color gg.Color = gg.rgb(255, 0, 0) 14 | } 15 | 16 | @[params] 17 | pub struct SliderStyleParams { 18 | pub mut: 19 | style string = no_style 20 | thumb_color gg.Color = no_color 21 | bg_color gg.Color = no_color 22 | bg_border_color gg.Color = no_color 23 | focused_bg_border_color gg.Color = no_color 24 | } 25 | 26 | pub fn slider_style(p SliderStyleParams) SliderStyleParams { 27 | return p 28 | } 29 | 30 | pub fn (ss SliderStyle) to_toml() string { 31 | mut toml_ := map[string]toml.Any{} 32 | toml_['thumb_color'] = hex_color(ss.thumb_color) 33 | toml_['bg_color'] = hex_color(ss.bg_color) 34 | toml_['bg_border_color'] = hex_color(ss.bg_border_color) 35 | toml_['focused_bg_border_color'] = hex_color(ss.focused_bg_border_color) 36 | return toml_.to_toml() 37 | } 38 | 39 | pub fn (mut ss SliderStyle) from_toml(a toml.Any) { 40 | ss.thumb_color = HexColor(a.value('thumb_color').string()).color() 41 | ss.bg_color = HexColor(a.value('bg_color').string()).color() 42 | ss.bg_border_color = HexColor(a.value('bg_border_color').string()).color() 43 | ss.focused_bg_border_color = HexColor(a.value('focused_bg_border_color').string()).color() 44 | } 45 | 46 | fn (mut s Slider) load_style() { 47 | // println("pgbar load style $s.theme_style") 48 | mut style := if s.theme_style == '' { s.ui.window.theme_style } else { s.theme_style } 49 | if s.style_params.style != no_style { 50 | style = s.style_params.style 51 | } 52 | s.update_theme_style(style) 53 | // forced overload default style 54 | s.update_style(s.style_params) 55 | } 56 | 57 | pub fn (mut s Slider) update_theme_style(theme string) { 58 | // println("update_style <$p.style>") 59 | style := if theme == '' { 'default' } else { theme } 60 | if style != no_style && style in s.ui.styles { 61 | ss := s.ui.styles[style].slider 62 | s.theme_style = theme 63 | s.style.thumb_color = ss.thumb_color 64 | s.style.bg_color = ss.bg_color 65 | s.style.bg_border_color = ss.bg_border_color 66 | s.style.focused_bg_border_color = ss.focused_bg_border_color 67 | } 68 | } 69 | 70 | pub fn (mut s Slider) update_style(p SliderStyleParams) { 71 | if p.thumb_color != no_color { 72 | s.style.thumb_color = p.thumb_color 73 | } 74 | if p.bg_color != no_color { 75 | s.style.bg_color = p.bg_color 76 | } 77 | if p.bg_border_color != no_color { 78 | s.style.bg_border_color = p.bg_border_color 79 | } 80 | if p.focused_bg_border_color != no_color { 81 | s.style.focused_bg_border_color = p.focused_bg_border_color 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /examples/7guis/timer.v: -------------------------------------------------------------------------------- 1 | import ui 2 | import time 3 | import math 4 | import gg 5 | 6 | const win_width = 287 7 | const win_height = 155 8 | const duration = 1 // ms 9 | 10 | const left = 60.0 11 | 12 | @[heap] 13 | struct App { 14 | mut: 15 | lbl_elapsed_value &ui.Label 16 | progress_bar &ui.ProgressBar 17 | slider &ui.Slider = unsafe { nil } 18 | window &ui.Window 19 | duration f64 = 15.0 20 | elapsed_time f64 = 0.0 21 | } 22 | 23 | fn main() { 24 | mut app := &App{ 25 | lbl_elapsed_value: ui.label(text: '00.0s', text_size: 1.0 / 10) 26 | progress_bar: ui.progressbar( 27 | height: 20 28 | val: 0 29 | max: 100 30 | color: gg.green 31 | border_color: gg.dark_green 32 | ) 33 | window: unsafe { nil } 34 | } 35 | app.slider = ui.slider( 36 | width: 180 37 | height: 20 38 | orientation: .horizontal 39 | max: 30 40 | min: 0 41 | val: 15.0 42 | on_value_changed: app.on_value_changed 43 | ) 44 | window := ui.window( 45 | width: win_width 46 | height: win_height 47 | title: 'Timer' 48 | mode: .resizable 49 | layout: ui.column( 50 | margin_: .05 51 | spacing: .05 52 | children: [ 53 | ui.row( 54 | spacing: .1 55 | widths: [left, ui.stretch] 56 | children: [ui.label(text: 'Elapsed Time:', text_size: 1.0 / 10), app.progress_bar] 57 | ), 58 | ui.row( 59 | spacing: .1 60 | widths: [left, ui.stretch] 61 | children: [ui.spacing(), app.lbl_elapsed_value] 62 | ), 63 | ui.row( 64 | spacing: .1 65 | widths: [left, ui.stretch] 66 | children: [ui.label(text: 'Duration:', text_size: 1.0 / 10), app.slider] 67 | ), 68 | ui.button(text: 'Reset', on_click: app.on_reset), 69 | ] 70 | ) 71 | ) 72 | app.window = window 73 | 74 | // go app.timer() 75 | ui.run(window) 76 | } 77 | 78 | fn (mut app App) on_value_changed(slider &ui.Slider) { 79 | app.duration = app.slider.val 80 | } 81 | 82 | fn (mut app App) on_reset(button &ui.Button) { 83 | app.elapsed_time = 0.0 84 | spawn app.timer() 85 | } 86 | 87 | fn (mut app App) timer() { 88 | for { 89 | if app.elapsed_time == app.duration { 90 | break 91 | } 92 | if app.elapsed_time > app.duration { 93 | app.elapsed_time = app.duration 94 | } else { 95 | app.elapsed_time += 0.1 * duration 96 | } 97 | app.lbl_elapsed_value.set_text('${math.ceil(app.elapsed_time * 100) / 100}s') 98 | if app.duration == 0 { 99 | app.progress_bar.val = 100 100 | } else { 101 | app.progress_bar.val = int(app.elapsed_time * 100.0 / app.duration) 102 | } 103 | time.sleep(100000 * duration * time.microsecond) 104 | app.window.refresh() 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /examples/nested_scrollview.v: -------------------------------------------------------------------------------- 1 | import ui 2 | 3 | const win_width = 550 4 | const win_height = 300 5 | 6 | const box_width = 110 7 | const box_height = 90 8 | 9 | const area_width = 600 10 | const area_height = 400 11 | const area_spacing = 10 12 | 13 | const instructions = 'Run v -d ui_scroll_nest and scroll inside/outside scrollviews' 14 | 15 | const single_column_of_boxes = false 16 | 17 | struct App { 18 | mut: 19 | box_text []string 20 | } 21 | 22 | fn make_scroll_area_box(mut app App, r string, c string) ui.Widget { 23 | app.box_text << 'box${r}${c}\n...\n...\n...\n...\n...\n...\n...\n...\n...' 24 | return ui.textbox( 25 | id: 'box${r}${c}' 26 | width: box_width 27 | height: box_height 28 | is_multiline: true 29 | text: &app.box_text[app.box_text.len - 1] 30 | ) 31 | } 32 | 33 | fn make_scroll_area_row(mut app App, r string) ui.Widget { 34 | return ui.row( 35 | spacing: area_spacing 36 | children: [ 37 | make_scroll_area_box(mut app, r, '-1'), 38 | make_scroll_area_box(mut app, r, '-2'), 39 | make_scroll_area_box(mut app, r, '-3'), 40 | make_scroll_area_box(mut app, r, '-4'), 41 | make_scroll_area_box(mut app, r, '-5'), 42 | ] 43 | ) 44 | } 45 | 46 | fn make_scroll_area(mut app App) ui.Widget { 47 | mut kids := []ui.Widget{} 48 | 49 | if single_column_of_boxes { 50 | kids << make_scroll_area_box(mut app, '', '-1') 51 | kids << make_scroll_area_box(mut app, '', '-2') 52 | kids << make_scroll_area_box(mut app, '', '-3') 53 | kids << make_scroll_area_box(mut app, '', '-4') 54 | kids << make_scroll_area_box(mut app, '', '-5') 55 | } else { 56 | kids << make_scroll_area_row(mut app, '-0') 57 | kids << make_scroll_area_row(mut app, '-1') 58 | kids << make_scroll_area_row(mut app, '-2') 59 | kids << make_scroll_area_row(mut app, '-3') 60 | kids << make_scroll_area_row(mut app, '-4') 61 | kids << make_scroll_area_row(mut app, '-5') 62 | } 63 | 64 | return ui.column( 65 | id: 'scroll-column' 66 | margin_: area_spacing 67 | spacing: area_spacing 68 | scrollview: true 69 | children: kids 70 | ) 71 | } 72 | 73 | fn win_key_down(w &ui.Window, e ui.KeyEvent) { 74 | if e.key == .escape { 75 | // TODO: w.close() not implemented (no multi-window support yet!) 76 | if w.ui.dd is ui.DrawDeviceContext { 77 | w.ui.dd.quit() 78 | } 79 | } 80 | } 81 | 82 | fn main() { 83 | mut app := App{} 84 | mut win := ui.window( 85 | width: win_width 86 | height: win_height 87 | title: 'V nested scrollviews' 88 | on_key_down: win_key_down 89 | mode: .resizable 90 | layout: ui.column( 91 | heights: [ui.stretch, 20.0] 92 | widths: ui.stretch 93 | children: [make_scroll_area(mut app), ui.label( 94 | text: &instructions 95 | )] 96 | ) 97 | ) 98 | ui.run(win) 99 | } 100 | -------------------------------------------------------------------------------- /src/interface_draw_device.v: -------------------------------------------------------------------------------- 1 | module ui 2 | 3 | import gg 4 | 5 | pub interface DrawDevice { 6 | // text style 7 | has_text_style() bool 8 | set_text_style(font_name string, font_path string, size int, color gg.Color, align int, vertical_align int) 9 | draw_text_default(x int, y int, text string) // (ui) default ui TextStyle 10 | // text 11 | draw_text(x int, y int, text string, cfg gg.TextCfg) 12 | draw_text_def(x int, y int, text string) // (gg.Context) use set_text_cfg 13 | set_text_cfg(gg.TextCfg) 14 | text_size(string) (int, int) 15 | text_width(string) int 16 | text_height(string) int 17 | // drawing methods 18 | draw_pixel(x f32, y f32, c gg.Color, params gg.DrawPixelConfig) 19 | draw_pixels(points []f32, c gg.Color, params gg.DrawPixelConfig) 20 | draw_image(x f32, y f32, width f32, height f32, img &gg.Image) 21 | draw_triangle_empty(x f32, y f32, x2 f32, y2 f32, x3 f32, y3 f32, color gg.Color) 22 | draw_triangle_filled(x f32, y f32, x2 f32, y2 f32, x3 f32, y3 f32, color gg.Color) 23 | draw_rect_empty(x f32, y f32, w f32, h f32, color gg.Color) 24 | draw_rect_filled(x f32, y f32, w f32, h f32, color gg.Color) 25 | draw_rounded_rect_filled(x f32, y f32, w f32, h f32, radius f32, color gg.Color) 26 | draw_rounded_rect_empty(x f32, y f32, w f32, h f32, radius f32, border_color gg.Color) 27 | draw_circle_line(x f32, y f32, r int, segments int, color gg.Color) 28 | draw_circle_empty(x f32, y f32, r f32, color gg.Color) 29 | draw_circle_filled(x f32, y f32, r f32, color gg.Color) 30 | draw_slice_empty(x f32, y f32, r f32, start_angle f32, end_angle f32, segments int, color gg.Color) 31 | draw_slice_filled(x f32, y f32, r f32, start_angle f32, end_angle f32, segments int, color gg.Color) 32 | draw_arc_empty(x f32, y f32, radius f32, thickness f32, start_angle f32, end_angle f32, segments int, color gg.Color) 33 | draw_arc_filled(x f32, y f32, radius f32, thickness f32, start_angle f32, end_angle f32, segments int, color gg.Color) 34 | draw_arc_line(x f32, y f32, radius f32, start_angle f32, end_angle f32, segments int, color gg.Color) 35 | draw_line(x f32, y f32, x2 f32, y2 f32, color gg.Color) 36 | draw_convex_poly(points []f32, color gg.Color) 37 | draw_poly_empty(points []f32, color gg.Color) 38 | // clipping 39 | get_clipping() Rect 40 | mut: 41 | reset_clipping() 42 | set_clipping(rect Rect) 43 | set_bg_color(color gg.Color) 44 | } 45 | 46 | fn (mut d DrawDevice) draw_window(mut w Window) { 47 | mut children := if unsafe { w.child_window == 0 } { w.children } else { w.child_window.children } 48 | 49 | for mut child in children { 50 | child.draw_device(mut d) 51 | } 52 | 53 | for mut sw in w.subwindows { 54 | sw.draw_device(mut d) 55 | } 56 | 57 | // draw dragger if active 58 | draw_dragger(mut w) 59 | // draw tooltip if active 60 | w.tooltip.draw_device(mut d) 61 | 62 | if w.on_draw != unsafe { nil } { 63 | w.on_draw(w) 64 | } 65 | 66 | w.mouse.draw_device(mut d) 67 | } 68 | -------------------------------------------------------------------------------- /src/interface_focusable.v: -------------------------------------------------------------------------------- 1 | module ui 2 | 3 | // Contains all the methods related to focus management 4 | // that is to say: 5 | // * Focusable interface and its methods 6 | // * methods for Window 7 | // * methods for Layout interface 8 | 9 | pub interface Focusable { 10 | ui &UI 11 | mut: 12 | id string 13 | hidden bool 14 | is_focused bool 15 | focus() 16 | unfocus() 17 | } 18 | 19 | // TODO: documentation 20 | pub fn (f Focusable) has_focusable() bool { 21 | mut focusable := true 22 | if f is TextBox { 23 | focusable = !f.read_only 24 | } 25 | $if focus ? { 26 | println('${f.id}.has_focusable(): ${focusable} && ${!f.hidden} && ${f.ui.window.unlocked_focus()} (locked_focus=<${f.ui.window.locked_focus}>)') 27 | } 28 | return focusable && !f.hidden && f.ui.window.unlocked_focus() 29 | } 30 | 31 | // Only one widget can have the focus inside a Window 32 | pub fn (mut f Focusable) set_focus() { 33 | mut w := f.ui.window 34 | if !w.unlocked_focus() { 35 | return 36 | } 37 | if f.is_focused { 38 | if mut w.ui.dd is DrawDeviceContext { 39 | $if focus ? { 40 | println('${f.id} already has focus at ${w.ui.dd.frame}') 41 | } 42 | } 43 | return 44 | } 45 | Layout(w).unfocus_all() 46 | if f.has_focusable() { 47 | f.is_focused = true 48 | if mut w.ui.dd is DrawDeviceContext { 49 | $if focus ? { 50 | println('${f.id} has focus at ${w.ui.dd.frame}') 51 | } 52 | } 53 | } 54 | // update drawing_children when focus is taken 55 | f.update_parent_drawing_children() 56 | } 57 | 58 | // Only one widget can have the focus inside a Window 59 | pub fn (mut f Focusable) force_focus() { 60 | mut w := f.ui.window 61 | if f.is_focused { 62 | if mut w.ui.dd is DrawDeviceContext { 63 | $if focus ? { 64 | println('${f.id} already has focus at ${w.ui.dd.frame}') 65 | } 66 | } 67 | return 68 | } 69 | Layout(w).unfocus_all() 70 | f.is_focused = true 71 | if mut w.ui.dd is DrawDeviceContext { 72 | $if focus ? { 73 | println('${f.id} has focus at ${w.ui.dd.frame}') 74 | } 75 | } 76 | } 77 | 78 | // TODO: documentation 79 | pub fn (f Focusable) lock_focus() { 80 | mut w := f.ui.window 81 | $if focus ? { 82 | println('${f.id} lock focus') 83 | } 84 | w.locked_focus = f.id 85 | } 86 | 87 | // TODO: documentation 88 | pub fn (f Focusable) unlock_focus() { 89 | mut w := f.ui.window 90 | if w.locked_focus == f.id { 91 | $if focus ? { 92 | println('${f.id} unlock focus') 93 | } 94 | w.locked_focus = '' 95 | } 96 | } 97 | 98 | // TODO: documentation 99 | pub fn (f Focusable) update_parent_drawing_children() { 100 | if f is Widget { 101 | w := f as Widget 102 | mut p := w.parent 103 | if mut p is CanvasLayout { 104 | p.set_drawing_children() 105 | } else if mut p is CanvasLayout { 106 | p.set_drawing_children() 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/style_textbox.v: -------------------------------------------------------------------------------- 1 | module ui 2 | 3 | import gg 4 | import toml 5 | 6 | // TextBox 7 | 8 | pub struct TextBoxShapeStyle { 9 | pub mut: 10 | bg_radius f32 11 | bg_color gg.Color = gg.white 12 | } 13 | 14 | pub struct TextBoxStyle { 15 | TextBoxShapeStyle // text_style TextStyle 16 | pub mut: 17 | text_font_name string = 'system' 18 | text_color gg.Color 19 | text_size int = 16 20 | text_align TextHorizontalAlign = .left 21 | text_vertical_align TextVerticalAlign = .top 22 | } 23 | 24 | @[params] 25 | pub struct TextBoxStyleParams { 26 | WidgetTextStyleParams 27 | pub mut: 28 | style string = no_style 29 | bg_radius f32 30 | bg_color gg.Color = no_color 31 | } 32 | 33 | pub fn textbox_style(p TextBoxStyleParams) TextBoxStyleParams { 34 | return p 35 | } 36 | 37 | pub fn (ts TextBoxStyle) to_toml() string { 38 | mut toml_ := map[string]toml.Any{} 39 | toml_['bg_radius'] = ts.bg_radius 40 | toml_['bg_color'] = hex_color(ts.bg_color) 41 | return toml_.to_toml() 42 | } 43 | 44 | pub fn (mut ts TextBoxStyle) from_toml(a toml.Any) { 45 | ts.bg_radius = a.value('bg_radius').f32() 46 | ts.bg_color = HexColor(a.value('bg_color').string()).color() 47 | } 48 | 49 | fn (mut t TextBox) load_style() { 50 | // println("pgbar load style $t.theme_style") 51 | mut style := if t.theme_style == '' { t.ui.window.theme_style } else { t.theme_style } 52 | if t.style_params.style != no_style { 53 | style = t.style_params.style 54 | } 55 | t.update_theme_style(style) 56 | // forced overload default style 57 | t.update_style(t.style_params) 58 | } 59 | 60 | pub fn (mut t TextBox) update_theme_style(theme string) { 61 | // println("update_style <$p.style>") 62 | style := if theme == '' { 'default' } else { theme } 63 | if style != no_style && style in t.ui.styles { 64 | ts := t.ui.styles[style].tb 65 | t.theme_style = theme 66 | t.update_shape_theme_style(ts) 67 | mut dtw := DrawTextWidget(t) 68 | dtw.update_theme_style(ts) 69 | } 70 | } 71 | 72 | pub fn (mut t TextBox) update_style(p TextBoxStyleParams) { 73 | t.update_shape_style(p) 74 | mut dtw := DrawTextWidget(t) 75 | dtw.update_theme_style_params(p) 76 | } 77 | 78 | pub fn (mut t TextBox) update_shape_theme_style(ts TextBoxStyle) { 79 | t.style.bg_radius = ts.bg_radius 80 | t.style.bg_color = ts.bg_color 81 | } 82 | 83 | pub fn (mut t TextBox) update_shape_style(p TextBoxStyleParams) { 84 | if p.bg_radius > 0 { 85 | t.style.bg_radius = p.bg_radius 86 | } 87 | if p.bg_color != no_color { 88 | t.style.bg_color = p.bg_color 89 | } 90 | } 91 | 92 | pub fn (mut t TextBox) update_style_params(p TextBoxStyleParams) { 93 | if p.bg_radius > 0 { 94 | t.style_params.bg_radius = p.bg_radius 95 | } 96 | if p.bg_color != no_color { 97 | t.style_params.bg_color = p.bg_color 98 | } 99 | mut dtw := DrawTextWidget(t) 100 | dtw.update_theme_style_params(p) 101 | } 102 | -------------------------------------------------------------------------------- /tools/demo_tools.v: -------------------------------------------------------------------------------- 1 | module tools 2 | 3 | import ui 4 | import os 5 | 6 | const demo_blocks = ['layout', 'main_pre', 'main_post', 'window_init'] // in the right order 7 | 8 | const demo_comment_block_delims = set_demo_comment_block_delims() 9 | 10 | @[heap] 11 | pub struct DemoTemplate { 12 | file string 13 | code string 14 | pub mut: 15 | template map[string]string 16 | blocks map[string]string 17 | tb &ui.TextBox = unsafe { nil } 18 | } 19 | 20 | pub fn demo_template(file string, mut tb ui.TextBox) &DemoTemplate { 21 | code := os.read_file(file) or { panic(err) } 22 | mut dt := &DemoTemplate{ 23 | file: file 24 | code: code 25 | tb: tb 26 | } 27 | dt.set_template() 28 | return dt 29 | } 30 | 31 | pub fn set_demo_comment_block_delims() map[string]string { 32 | mut delims_ := map[string]string{} 33 | for block_name in demo_blocks { 34 | delims_['begin_${block_name}'] = '// <>' 35 | delims_['end_${block_name}'] = '// <>' 36 | } 37 | return delims_ 38 | } 39 | 40 | fn complete_demo_ui_code(code string) string { 41 | mut new_code := code 42 | mut block_name := demo_blocks[0] 43 | if !code.contains(block_format(block_name)) { 44 | new_code = block_format(block_name) + '\n' + new_code 45 | } 46 | for i in 1 .. demo_blocks.len { 47 | block_name = demo_blocks[i] 48 | if !code.contains(block_format(block_name)) { 49 | new_code += '\n' + block_format(block_name) 50 | } 51 | } 52 | new_code += '\n' + block_format('end') 53 | println(new_code) 54 | return new_code 55 | } 56 | 57 | pub fn (mut dt DemoTemplate) update_blocks() { 58 | code := complete_demo_ui_code(dt.tb.get_text()) 59 | for _, block_name in demo_blocks[0..demo_blocks.len] { 60 | start := block_format(block_name) 61 | stop := block_format_delim['start'] // '[[${tools.demo_blocks[i + 1]}]]' 62 | dt.blocks[block_name] = if code.contains(start) && code.contains(stop) { 63 | code.find_between(start, stop) 64 | } else { 65 | '' 66 | } 67 | } 68 | } 69 | 70 | pub fn (mut dt DemoTemplate) set_template() { 71 | src := dt.code 72 | mut start, mut stop := '', demo_comment_block_delims['begin_${demo_blocks[0]}'] 73 | dt.template['pre_${demo_blocks[0]}'] = src.all_before(stop) + stop 74 | for i in 0 .. (demo_blocks.len - 1) { 75 | start = demo_comment_block_delims['end_${demo_blocks[i]}'] 76 | stop = demo_comment_block_delims['begin_${demo_blocks[i + 1]}'] 77 | dt.template['pre_${demo_blocks[i + 1]}'] = start + src.find_between(start, stop) + stop 78 | } 79 | start = demo_comment_block_delims['end_${demo_blocks[demo_blocks.len - 1]}'] 80 | dt.template['post'] = start + src.all_after(start) 81 | } 82 | 83 | pub fn (mut dt DemoTemplate) write_file() { 84 | dt.update_blocks() 85 | mut code := '' 86 | for i in 0 .. demo_blocks.len { 87 | code += dt.template['pre_${demo_blocks[i]}'] + dt.blocks[demo_blocks[i]] 88 | } 89 | code += '\n' + dt.template['post'] 90 | os.write_file(dt.file, code) or { panic(err) } 91 | } 92 | --------------------------------------------------------------------------------