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