From 201c8e535a1edee19809c4e0affcf05eec0454e7 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Thu, 10 Sep 2026 06:00:20 +0300 Subject: [PATCH 1/5] Add borderless transparent window example --- README.md | 7 ++ appkit/ui_macos_darwin.v | 2 +- examples/custom_window/README.md | 24 +++++ examples/custom_window/custom_window.vml | 73 ++++++++++++++ examples/custom_window/custom_window_darwin.v | 72 ++++++++++++++ examples/custom_window/custom_window_test.v | 47 +++++++++ .../custom_window/custom_window_windows.c.v | 17 ++++ .../custom_window/custom_window_windows.h | 49 +++++++++ examples/custom_window/main.v | 99 +++++++++++++++++++ examples/custom_window/platform.v | 44 +++++++++ ui/vml.v | 6 +- 11 files changed, 438 insertions(+), 2 deletions(-) create mode 100644 examples/custom_window/README.md create mode 100644 examples/custom_window/custom_window.vml create mode 100644 examples/custom_window/custom_window_darwin.v create mode 100644 examples/custom_window/custom_window_test.v create mode 100644 examples/custom_window/custom_window_windows.c.v create mode 100644 examples/custom_window/custom_window_windows.h create mode 100644 examples/custom_window/main.v create mode 100644 examples/custom_window/platform.v diff --git a/README.md b/README.md index af133e3..b6376c6 100644 --- a/README.md +++ b/README.md @@ -220,6 +220,10 @@ Rectangle { } ``` +Set `transparent: true` to omit a box's fill. It also works on the root +`Screen`; a transparent screen reveals the desktop when its native host window +is configured for transparency, as demonstrated by `examples/custom_window`. + `border_width` is shorthand for all four edges; an explicitly declared edge overrides it. The equivalent V API fields live directly on `BoxStyle`. @@ -565,6 +569,9 @@ Typed-VML ports from `v-ui` include: separators, disabled and checked rows, and nested submenus. - `v run examples/tray_icon/main.v` — a status area icon whose menu sets a status, toggles a setting, and docks or hides the icon again. +- `v run examples/custom_window/main.v` — a borderless desktop widget with + ui2-drawn chrome, draggable content, and genuinely transparent space between + its rounded surfaces on the native macOS and Windows backends. Run the calculator demo with: diff --git a/appkit/ui_macos_darwin.v b/appkit/ui_macos_darwin.v index 0ac46db..7317cad 100644 --- a/appkit/ui_macos_darwin.v +++ b/appkit/ui_macos_darwin.v @@ -838,7 +838,7 @@ fn render_root(declared Element) { st.textview_ids = map[u64]string{} st.textview_action_ids = map[u64]string{} st.scroll_ids = map[u64]string{} - native_set_background(st.root_view, root.box.bg) + native_set_box_background(st.root_view, root.box) mut active := map[string]bool{} render_children(st.root_view, root.children, '', mut active) remove_stale_nodes(active) diff --git a/examples/custom_window/README.md b/examples/custom_window/README.md new file mode 100644 index 0000000..d9371f7 --- /dev/null +++ b/examples/custom_window/README.md @@ -0,0 +1,24 @@ +# Custom window + +This example keeps the interface in ui2 while replacing the native desktop +frame with a small platform adapter: + +```sh +v run examples/custom_window +``` + +- macOS changes ui2's `NSWindow` to a borderless, non-opaque window and gives + the root `Screen` a transparent background. +- Windows changes ui2's `HWND` to a borderless layered window and color-keys + the otherwise unused `#010203` screen background. +- Linux and opt-in custom-rendered desktop builds show the same layout in a + normal window as a portable fallback. + +The header is an ordinary draggable ui2 `Rectangle`. Its pointer-down event is +handed to `performWindowDragWithEvent:` or `WM_NCLBUTTONDOWN`, so the operating +system still performs the move. The close button calls `ui2.quit()`. Everything +else—including the separated rounded surfaces and the clear space between +them—is declared in [`custom_window.vml`](custom_window.vml). + +For a resizable custom frame, keep the same pattern and add narrow draggable +ui2 regions at the edges, routing them to the platform's native resize action. diff --git a/examples/custom_window/custom_window.vml b/examples/custom_window/custom_window.vml new file mode 100644 index 0000000..d1d9681 --- /dev/null +++ b/examples/custom_window/custom_window.vml @@ -0,0 +1,73 @@ +Screen { + id: root + background: app.screen_background + transparent: app.transparent_screen + + Rectangle { + id: card_shadow + x: 16 + y: 14 + width: root.width - 32 + height: 122 + background: #020617 + corner_radius: 22 + } + + Rectangle { + id: card + x: 12 + y: 10 + width: root.width - 32 + height: 122 + background: #111827 + corner_radius: 22 + border_color: #334155 + border_width: 1 + + Rectangle { + id: window_drag + on_tap: "window_drag" + clickable: true + draggable: true + x: 0 + y: 0 + width: card.width + height: 48 + transparent: true + + Rectangle { x: 18 y: 18 width: 10 height: 10 background: #34D399 corner_radius: 5 } + Label { text: "FOCUS WIDGET" x: 38 y: 13 width: 200 height: 22 color: #E2E8F0 font_size: 12 bold: true } + Label { text: "drag anywhere on this header" x: 202 y: 14 width: card.width - 252 height: 20 align: right color: #64748B font_size: 10 } + } + + Button { id: close text: "×" on_tap: "close" x: card.width - 42 y: 10 width: 30 height: 30 background: #7F1D1D corner_radius: 15 color: #FEE2E2 font_size: 18 bold: true accessibility_label: "Close widget" } + Label { text: app.completed_label x: 20 y: 57 width: card.width - 40 height: 26 color: #F8FAFC font_size: 19 bold: true } + Label { text: app.platform_note x: 20 y: 91 width: card.width - 40 height: 18 color: #94A3B8 font_size: 10 } + } + + Rectangle { + id: action_shadow + x: root.width / 2 - 130 + y: 152 + width: 260 + height: 48 + background: #020617 + corner_radius: 24 + } + + Rectangle { + id: actions + x: root.width / 2 - 132 + y: 148 + width: 260 + height: 48 + background: #1E293B + corner_radius: 24 + border_color: #475569 + border_width: 1 + + Button { id: decrement text: "−" on_tap: "decrement" enabled: app.completed > 0 x: 8 y: 7 width: 50 height: 34 background: #334155 corner_radius: 17 color: #F8FAFC font_size: 18 bold: true accessibility_label: "Remove a completed session" } + Button { id: reset text: app.completed x: 68 y: 7 width: 120 height: 34 background: #0F172A corner_radius: 17 color: #A7F3D0 font_size: 16 bold: true accessibility_label: "Reset completed sessions" on_tap: "reset" } + Button { id: increment text: "+" on_tap: "increment" x: 198 y: 7 width: 54 height: 34 background: #047857 corner_radius: 17 color: #ECFDF5 font_size: 18 bold: true accessibility_label: "Complete a focus session" } + } +} diff --git a/examples/custom_window/custom_window_darwin.v b/examples/custom_window/custom_window_darwin.v new file mode 100644 index 0000000..7c88865 --- /dev/null +++ b/examples/custom_window/custom_window_darwin.v @@ -0,0 +1,72 @@ +module main + +import macos + +#flag darwin -framework AppKit + +const ns_window_style_borderless = u64(0) + +@[markused] +fn macos_configure_custom_window() bool { + window := macos_custom_window() + if macos_custom_window_is_nil(window) { + return false + } + // Borderless NSWindows normally refuse to become key windows. UI2 creates a + // small NSWindow subclass, so add the capability before dropping its frame. + window_class := macos.get_class('UI2Window') + if !macos_custom_window_is_nil(window_class) { + macos.add_method(window_class, 'canBecomeKey', voidptr(custom_window_can_become_key), 'B@:') + macos.add_method(window_class, 'canBecomeMainWindow', voidptr(custom_window_can_become_key), 'B@:') + } + macos.msg_void_u64(window, 'setStyleMask:', ns_window_style_borderless) + macos.msg_void_bool(window, 'setOpaque:', false) + macos.msg_void_bool(window, 'setHasShadow:', true) + macos.msg_void1(window, 'setBackgroundColor:', macos.msg_id(macos.get_class('NSColor'), 'clearColor')) + return true +} + +@[markused] +fn macos_drag_custom_window() { + window := macos_custom_window() + event := macos.msg_id(macos_custom_window_application(), 'currentEvent') + if macos_custom_window_is_nil(window) || macos_custom_window_is_nil(event) { + return + } + macos.msg_void1(window, 'performWindowDragWithEvent:', event) +} + +fn macos_custom_window() macos.Id { + app := macos_custom_window_application() + if macos_custom_window_is_nil(app) { + return macos_custom_window_nil() + } + for selector in ['keyWindow', 'mainWindow'] { + window := macos.msg_id(app, selector) + if !macos_custom_window_is_nil(window) { + return window + } + } + windows := macos.msg_id(app, 'windows') + if macos_custom_window_is_nil(windows) || macos.msg_u64(windows, 'count') == 0 { + return macos_custom_window_nil() + } + return macos.msg_id_u64(windows, 'objectAtIndex:', u64(0)) +} + +fn macos_custom_window_application() macos.Id { + return macos.msg_id(macos.get_class('NSApplication'), 'sharedApplication') +} + +fn macos_custom_window_is_nil(value macos.Id) bool { + return value == macos_custom_window_nil() +} + +fn macos_custom_window_nil() macos.Id { + return unsafe { nil } +} + +@[export: 'ui2_example_custom_window_can_become_key'] +fn custom_window_can_become_key(_self voidptr, _cmd voidptr) bool { + return true +} diff --git a/examples/custom_window/custom_window_test.v b/examples/custom_window/custom_window_test.v new file mode 100644 index 0000000..0ef1f93 --- /dev/null +++ b/examples/custom_window/custom_window_test.v @@ -0,0 +1,47 @@ +module main + +import ui2 + +fn find_custom_window_element(element ui2.Element, id string) ?ui2.Element { + if element.id == id { + return element + } + for child in element.children { + if found := find_custom_window_element(child, id) { + return found + } + } + return none +} + +fn test_custom_window_counter_never_becomes_negative() { + mut app := CustomWindowDemo{} + app.remove_completed() + assert app.completed == 0 + assert app.completed_label == 'Nothing completed yet' + app.add_completed() + assert app.completed == 1 + assert app.completed_label == '1 focus session completed' + app.add_completed() + assert app.completed_label == '2 focus sessions completed' + app.reset() + assert app.completed == 0 +} + +fn test_custom_window_vml_exposes_clear_space_and_custom_chrome() { + app := CustomWindowDemo{ + transparent_screen: true + screen_background: '#010203' + } + root := ui2.element_from_vml_model(custom_window_vml_source, app, ui2.rect(0, 0, custom_window_width, custom_window_height)) or { panic(err) } + ui2.validate_element_tree(root) or { panic(err) } + assert root.box.transparent + drag := find_custom_window_element(root, 'window_drag') or { panic('missing drag surface') } + assert drag.draggable && drag.clickable + assert drag.box.transparent + card := find_custom_window_element(root, 'card') or { panic('missing widget card') } + actions := find_custom_window_element(root, 'actions') or { panic('missing action island') } + assert card.frame.y + card.frame.height < actions.frame.y + assert card.box.radius == 22 + assert (find_custom_window_element(root, 'close') or { panic('missing close control') }).action_id == 'close' +} diff --git a/examples/custom_window/custom_window_windows.c.v b/examples/custom_window/custom_window_windows.c.v new file mode 100644 index 0000000..06f4705 --- /dev/null +++ b/examples/custom_window/custom_window_windows.c.v @@ -0,0 +1,17 @@ +module main + +#include + +#insert "@DIR/custom_window_windows.h" + +fn C.ui2_example_configure_custom_window(width int, height int) bool + +fn C.ui2_example_drag_custom_window() + +fn windows_configure_custom_window(width int, height int) bool { + return C.ui2_example_configure_custom_window(width, height) +} + +fn windows_drag_custom_window() { + C.ui2_example_drag_custom_window() +} diff --git a/examples/custom_window/custom_window_windows.h b/examples/custom_window/custom_window_windows.h new file mode 100644 index 0000000..724d67b --- /dev/null +++ b/examples/custom_window/custom_window_windows.h @@ -0,0 +1,49 @@ +#ifndef UI2_EXAMPLE_CUSTOM_WINDOW_WINDOWS_H +#define UI2_EXAMPLE_CUSTOM_WINDOW_WINDOWS_H + +#include + +static HWND ui2_example_custom_window = NULL; + +static BOOL CALLBACK ui2_example_find_custom_window(HWND window, LPARAM output) { + wchar_t class_name[32]; + DWORD process_id = 0; + GetWindowThreadProcessId(window, &process_id); + if (process_id != GetCurrentProcessId()) return TRUE; + if (GetClassNameW(window, class_name, 32) <= 0) return TRUE; + if (lstrcmpW(class_name, L"UI2Window") != 0) return TRUE; + *((HWND *)output) = window; + return FALSE; +} + +static HWND ui2_example_find_main_window(void) { + HWND window = NULL; + EnumWindows(ui2_example_find_custom_window, (LPARAM)&window); + return window; +} + +static inline bool ui2_example_configure_custom_window(int width, int height) { + HWND window = ui2_example_find_main_window(); + if (window == NULL) return false; + ui2_example_custom_window = window; + SetWindowLongPtrW(window, GWL_STYLE, WS_POPUP | WS_CLIPCHILDREN | WS_CLIPSIBLINGS); + SetWindowLongPtrW(window, GWL_EXSTYLE, + GetWindowLongPtrW(window, GWL_EXSTYLE) | WS_EX_LAYERED | WS_EX_APPWINDOW); + // ui2 paints #010203 outside the cards. The layered host removes precisely + // those pixels, leaving the independent rounded surfaces visible. + SetLayeredWindowAttributes(window, RGB(1, 2, 3), 0, LWA_COLORKEY); + SetWindowPos(window, NULL, 0, 0, width, height, + SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED); + return true; +} + +static inline void ui2_example_drag_custom_window(void) { + if (ui2_example_custom_window == NULL) { + ui2_example_custom_window = ui2_example_find_main_window(); + } + if (ui2_example_custom_window == NULL) return; + ReleaseCapture(); + SendMessageW(ui2_example_custom_window, WM_NCLBUTTONDOWN, HTCAPTION, 0); +} + +#endif diff --git a/examples/custom_window/main.v b/examples/custom_window/main.v new file mode 100644 index 0000000..3449a8f --- /dev/null +++ b/examples/custom_window/main.v @@ -0,0 +1,99 @@ +module main + +import ui2 + +const custom_window_width = 440 +const custom_window_height = 210 +const custom_window_vml_source = $embed_file('custom_window.vml').to_string() + +@[heap] +pub struct CustomWindowDemo { +pub mut: + completed int + completed_label string = 'Nothing completed yet' + platform_note string + transparent_screen bool + screen_background string = '#0F172A' +mut: + window_ready bool +} + +const custom_window_state = &CustomWindowDemo{} + +fn (mut app CustomWindowDemo) prepare_window() { + if !app.window_ready { + app.window_ready = configure_custom_window() + if !app.window_ready { + ui2.request_refresh() + } + } + app.transparent_screen = custom_window_uses_transparent_screen() + app.screen_background = custom_window_screen_background() + app.platform_note = custom_window_platform_note() +} + +pub fn (mut app CustomWindowDemo) add_completed() { + app.completed++ + app.update_completed_label() +} + +pub fn (mut app CustomWindowDemo) remove_completed() { + if app.completed > 0 { + app.completed-- + } + app.update_completed_label() +} + +pub fn (mut app CustomWindowDemo) reset() { + app.completed = 0 + app.update_completed_label() +} + +fn (mut app CustomWindowDemo) update_completed_label() { + app.completed_label = match app.completed { + 0 { 'Nothing completed yet' } + 1 { '1 focus session completed' } + else { '${app.completed} focus sessions completed' } + } +} + +fn custom_window_pointer(raw string) ?(string, string) { + parts := raw.split(':') + if parts.len < 3 || parts[0] != 'pointer' { + return none + } + return parts[1], parts[2] +} + +fn build_custom_window_screen() ui2.Element { + mut state := unsafe { custom_window_state } + state.prepare_window() + return ui2.element_from_vml_model(custom_window_vml_source, *state, ui2.bounds()) or { + eprintln('custom-window VML failed: ${err}') + ui2.screen(0x0f172a, []) + } +} + +fn handle_custom_window_event(event string) { + mut state := unsafe { custom_window_state } + match event { + 'increment' { state.add_completed() } + 'decrement' { state.remove_completed() } + 'reset' { state.reset() } + 'close' { + ui2.quit() + return + } + else { + phase, id := custom_window_pointer(event) or { return } + if phase == 'down' && id == 'window_drag' { + drag_custom_window() + } + } + } + ui2.refresh() +} + +fn main() { + ui2.run_window('ui2 custom window', custom_window_width, custom_window_height, build_custom_window_screen, handle_custom_window_event) +} diff --git a/examples/custom_window/platform.v b/examples/custom_window/platform.v new file mode 100644 index 0000000..f07f23b --- /dev/null +++ b/examples/custom_window/platform.v @@ -0,0 +1,44 @@ +module main + +fn configure_custom_window() bool { + $if macos && !ui2_custom_rendering ? { + return macos_configure_custom_window() + } $else $if windows && !ui2_custom_rendering ? { + return windows_configure_custom_window(custom_window_width, custom_window_height) + } $else { + return true + } +} + +fn drag_custom_window() { + $if macos && !ui2_custom_rendering ? { + macos_drag_custom_window() + } $else $if windows && !ui2_custom_rendering ? { + windows_drag_custom_window() + } +} + +fn custom_window_uses_transparent_screen() bool { + $if macos && !ui2_custom_rendering ? { + return true + } $else { + return false + } +} + +fn custom_window_screen_background() string { + $if windows && !ui2_custom_rendering ? { + // The Win32 adapter treats this otherwise-unused color as transparent. + return '#010203' + } $else { + return '#0F172A' + } +} + +fn custom_window_platform_note() string { + $if ( macos || windows ) && !ui2_custom_rendering ? { + return 'The empty area around these cards is the desktop.' + } $else { + return 'Frameless transparency is demonstrated by the native macOS and Windows backends.' + } +} diff --git a/ui/vml.v b/ui/vml.v index dc90405..41923ea 100644 --- a/ui/vml.v +++ b/ui/vml.v @@ -716,7 +716,10 @@ fn node_to_element_base(node &VNode, frame Rect) !Element { local := rect(0, 0, frame.width, frame.height) match node.tag { 'Screen' { - return screen(v_color(node, 'background', 0xffffff), v_children(node, local)!) + return Element{ + ...screen(v_color(node, 'background', 0xffffff), v_children(node, local)!) + box: v_box(node) + } } 'Column' { return v_column(node, frame)! @@ -1685,6 +1688,7 @@ fn v_box(node &VNode) BoxStyle { return BoxStyle{ bg: v_color(node, 'background', 0xffffff) radius: node.prop_or('corner_radius', node.prop_or('radius', '0')).f64() + transparent: node.prop_bool('transparent') border_color: v_color(node, 'border_color', 0) border_left: node.prop_or('border_left', border_width).f64() border_top: node.prop_or('border_top', border_width).f64() From c8910939d887ef4025d30efa3069d066324c88a7 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Thu, 10 Sep 2026 06:25:16 +0300 Subject: [PATCH 2/5] Fix transparent box rendering --- appkit/ui_macos_darwin.v | 84 +++++++++++-------- appkit/ui_macos_test.v | 55 +++++++++++- examples/custom_window/custom_window_darwin.v | 2 +- ui/ui.v | 6 ++ ui/ui_immediate.c.v | 37 ++++---- ui/ui_test.v | 8 ++ ui/vml_test.v | 14 ++++ 7 files changed, 152 insertions(+), 54 deletions(-) diff --git a/appkit/ui_macos_darwin.v b/appkit/ui_macos_darwin.v index 7317cad..ad24f36 100644 --- a/appkit/ui_macos_darwin.v +++ b/appkit/ui_macos_darwin.v @@ -1007,14 +1007,14 @@ fn render_element(parent NativeView, el Element, key string, mut active map[stri doc_h := content_height(el.children) + 16 mut doc := st.nodes[doc_key] or { native_nil_view() } if native_is_nil(doc) { - doc = native_new_flipped_view(native_rect(0, 0, el.frame.width, doc_h), el.box.bg) + doc = native_new_flipped_view(native_rect(0, 0, el.frame.width, doc_h), el.box) st.nodes[doc_key] = doc st.node_kinds[doc_key] = .view native_set_document_view(native, doc) macos.release(doc) } else { native_set_frame(doc, native_rect(0, 0, el.frame.width, doc_h)) - native_set_background(doc, el.box.bg) + native_set_box_background(doc, el.box) } render_children(doc, el.children, doc_key, mut active) } @@ -1158,7 +1158,7 @@ fn native_create_element(el Element) NativeView { native_new_view(element_rect(el.frame), el.box, element_interactive(el)) } .scroll { - native_new_scroll(element_rect(el.frame), el.box.bg, el.persistent_scrollbars) + native_new_scroll(element_rect(el.frame), el.box, el.persistent_scrollbars) } .label { native_new_label(element_rect(el.frame), el.text, el.text_style.color, el.text_style.size, el.text_style.bold, el.text_style.italic, el.text_style.underline, align_value(el.text_style.align), el.text_style.lines) @@ -1167,7 +1167,7 @@ fn native_create_element(el Element) NativeView { native_new_image(element_rect(el.frame), el.image_path, el.rotation) } .button { - native_new_button(element_rect(el.frame), el.text, el.box.bg, el.text_style.color, el.text_style.size, el.text_style.bold, el.text_style.italic, el.text_style.underline, el.box.radius, el.text_style.lines, el.image_path, el.native_style) + native_new_button(element_rect(el.frame), el.text, el.box, el.text_style.color, el.text_style.size, el.text_style.bold, el.text_style.italic, el.text_style.underline, el.text_style.lines, el.image_path, el.native_style) } .checkbox { native_new_checkbox(el) @@ -1203,7 +1203,7 @@ fn native_update_element(native NativeView, el Element, declared_text_changed bo } .scroll { native_set_frame(native, element_rect(el.frame)) - native_set_scroll_background(native, el.box.bg) + native_set_scroll_background(native, el.box) native_set_scrollbar_mode(native, el.persistent_scrollbars) } .label { @@ -1213,7 +1213,7 @@ fn native_update_element(native NativeView, el Element, declared_text_changed bo native_update_image(native, element_rect(el.frame), el.image_path, el.rotation) } .button { - native_update_button(native, element_rect(el.frame), el.text, el.box.bg, el.text_style.color, el.text_style.size, el.text_style.bold, el.text_style.italic, el.text_style.underline, el.box.radius, el.text_style.lines, el.image_path, el.native_style) + native_update_button(native, element_rect(el.frame), el.text, el.box, el.text_style.color, el.text_style.size, el.text_style.bold, el.text_style.italic, el.text_style.underline, el.text_style.lines, el.image_path, el.native_style) } .checkbox { native_update_checkbox(native, el) @@ -1229,7 +1229,7 @@ fn native_update_element(native NativeView, el Element, declared_text_changed bo } .text_field { native_update_text_field(native, element_rect(el.frame), el.placeholder, el.text, - el.box.bg, el.text_style.color, el.text_style.size, el.box.radius, + el.box, el.text_style.color, el.text_style.size, declared_text_changed, el.readonly, el.enabled) } .text_area { @@ -1687,9 +1687,9 @@ fn native_set_associated_object(obj NativeView, key voidptr, value NativeView) { macos.set_associated_object(obj, key, value, macos.assoc_retain_nonatomic) } -fn native_new_flipped_view(frame NativeRect, bg u32) NativeView { +fn native_new_flipped_view(frame NativeRect, box BoxStyle) NativeView { native := macos.msg_id_rect(macos.alloc('UI2FlippedView'), 'initWithFrame:', appkit_rect(frame)) - native_set_background(native, bg) + native_set_box_background(native, box) return native } @@ -1701,12 +1701,11 @@ fn native_new_view(frame NativeRect, box BoxStyle, interactive bool) NativeView return native } -fn native_new_scroll(frame NativeRect, bg u32, persistent_scrollbars bool) NativeView { +fn native_new_scroll(frame NativeRect, box BoxStyle, persistent_scrollbars bool) NativeView { scroll_view := macos.msg_id_rect(macos.alloc('NSScrollView'), 'initWithFrame:', appkit_rect(frame)) macos.msg_void_bool(scroll_view, 'setHasVerticalScroller:', true) native_set_scrollbar_mode(scroll_view, persistent_scrollbars) - macos.msg_void_bool(scroll_view, 'setDrawsBackground:', true) - native_set_scroll_background(scroll_view, bg) + native_set_scroll_background(scroll_view, box) return scroll_view } @@ -1720,9 +1719,12 @@ fn native_set_document_view(scroll NativeView, view NativeView) { macos.msg_void1(scroll, 'setDocumentView:', view) } -fn native_set_scroll_background(scroll NativeView, bg u32) { - macos.msg_void_bool(scroll, 'setDrawsBackground:', true) - macos.msg_void1(scroll, 'setBackgroundColor:', native_color(bg)) +fn native_set_scroll_background(scroll NativeView, box BoxStyle) { + draws_background := box_draws_fill(box) + macos.msg_void_bool(scroll, 'setDrawsBackground:', draws_background) + if draws_background { + macos.msg_void1(scroll, 'setBackgroundColor:', native_color(box.bg)) + } } fn native_new_image(frame NativeRect, path string, rotation f64) NativeView { @@ -1777,17 +1779,18 @@ fn native_update_label(label_view NativeView, frame NativeRect, text string, tex macos.msg_void_bool(cell, 'setUsesSingleLineMode:', lines == 1) } -fn native_new_button(frame NativeRect, title string, bg_hex u32, text_hex u32, size f64, bold bool, italic bool, underline bool, radius f64, lines int, image_name string, native_style bool) NativeView { +fn native_new_button(frame NativeRect, title string, box BoxStyle, text_hex u32, size f64, bold bool, italic bool, underline bool, lines int, image_name string, native_style bool) NativeView { button_view := macos.msg_id_rect(macos.alloc('NSButton'), 'initWithFrame:', appkit_rect(frame)) - native_update_button(button_view, frame, title, bg_hex, text_hex, size, bold, italic, underline, radius, lines, image_name, native_style) + native_update_button(button_view, frame, title, box, text_hex, size, bold, italic, underline, + lines, image_name, native_style) return button_view } -fn native_update_button(button_view NativeView, frame NativeRect, title string, bg_hex u32, text_hex u32, size f64, bold bool, italic bool, underline bool, radius f64, lines int, image_name string, native_style bool) { +fn native_update_button(button_view NativeView, frame NativeRect, title string, box BoxStyle, text_hex u32, size f64, bold bool, italic bool, underline bool, lines int, image_name string, native_style bool) { native_set_frame(button_view, frame) macos.msg_void1(button_view, 'setTitle:', macos.nsstring(title)) macos.msg_void_u64(button_view, 'setBezelStyle:', 1) - if native_style { + if native_style && box_draws_fill(box) { macos.msg_void_i64(button_view, 'setButtonType:', ns_button_type_momentary_push_in) macos.msg_void_bool(button_view, 'setBordered:', true) macos.msg_void_bool(button_view, 'setWantsLayer:', false) @@ -1796,8 +1799,8 @@ fn native_update_button(button_view NativeView, frame NativeRect, title string, macos.msg_void_bool(button_view, 'setBordered:', false) macos.msg_void1(button_view, 'setFont:', native_font(size, bold, italic)) native_control_set_attributed_title(button_view, title, text_hex, size, bold, italic, underline) - native_set_background(button_view, bg_hex) - native_set_corner_radius(button_view, radius) + native_set_box_background(button_view, box) + native_set_corner_radius(button_view, box.radius) } cell := macos.msg_id(button_view, 'cell') macos.msg_void_i64(cell, 'setLineBreakMode:', 4) @@ -1839,9 +1842,9 @@ fn native_update_switch_control(view NativeView, el Element) { } fn native_new_toggle_button(el Element) NativeView { - native := native_new_button(element_rect(el.frame), el.text, el.box.bg, el.text_style.color, + native := native_new_button(element_rect(el.frame), el.text, el.box, el.text_style.color, el.text_style.size, el.text_style.bold, el.text_style.italic, el.text_style.underline, - el.box.radius, el.text_style.lines, el.image_path, el.native_style) + el.text_style.lines, el.image_path, el.native_style) native_update_toggle_button(native, el) return native } @@ -1849,8 +1852,8 @@ fn native_new_toggle_button(el Element) NativeView { fn native_update_toggle_button(native NativeView, el Element) { box := if el.checked { el.toggle_down_box } else { el.box } style := if el.checked { el.toggle_down_text_style } else { el.text_style } - native_update_button(native, element_rect(el.frame), el.text, box.bg, style.color, style.size, - style.bold, style.italic, style.underline, box.radius, style.lines, el.image_path, + native_update_button(native, element_rect(el.frame), el.text, box, style.color, style.size, + style.bold, style.italic, style.underline, style.lines, el.image_path, el.native_style) macos.msg_void_i64(native, 'setButtonType:', ns_button_type_push_on_push_off) macos.msg_void_i64(native, 'setState:', if el.checked { i64(1) } else { i64(0) }) @@ -1920,7 +1923,7 @@ fn native_update_dropdown(popup NativeView, el Element) { } native_select_dropdown_item(popup, el.text) macos.msg_void1(popup, 'setFont:', native_font(el.text_style.size, el.text_style.bold, el.text_style.italic)) - macos.msg_void_bool(popup, 'setBordered:', true) + macos.msg_void_bool(popup, 'setBordered:', box_draws_fill(el.box)) macos.msg_void_u64(popup, 'setBezelStyle:', 1) } @@ -1928,12 +1931,12 @@ fn native_new_text_field(el Element) NativeView { frame := element_rect(el.frame) cls := if el.secure { 'NSSecureTextField' } else { 'NSTextField' } field := macos.msg_id_rect(macos.alloc(cls), 'initWithFrame:', appkit_rect(frame)) - native_update_text_field(field, frame, el.placeholder, el.text, el.box.bg, el.text_style.color, - el.text_style.size, el.box.radius, true, el.readonly, el.enabled) + native_update_text_field(field, frame, el.placeholder, el.text, el.box, el.text_style.color, + el.text_style.size, true, el.readonly, el.enabled) return field } -fn native_update_text_field(field NativeView, frame NativeRect, placeholder string, text string, bg_hex u32, text_hex u32, size f64, _radius f64, declared_text_changed bool, readonly bool, enabled bool) { +fn native_update_text_field(field NativeView, frame NativeRect, placeholder string, text string, box BoxStyle, text_hex u32, size f64, declared_text_changed bool, readonly bool, enabled bool) { native_set_frame(field, frame) // Unrelated refreshes preserve native edits. A changed declaration remains // controlled and is applied explicitly. @@ -1946,8 +1949,11 @@ fn native_update_text_field(field NativeView, frame NativeRect, placeholder stri macos.msg_void_bool(field, 'setBordered:', true) macos.msg_void_bool(field, 'setBezeled:', true) macos.msg_void_u64(field, 'setBezelStyle:', 1) - macos.msg_void_bool(field, 'setDrawsBackground:', true) - macos.msg_void1(field, 'setBackgroundColor:', native_color(bg_hex)) + draws_background := box_draws_fill(box) + macos.msg_void_bool(field, 'setDrawsBackground:', draws_background) + if draws_background { + macos.msg_void1(field, 'setBackgroundColor:', native_color(box.bg)) + } macos.msg_void_bool(field, 'setEditable:', !readonly && enabled) macos.msg_void_bool(field, 'setSelectable:', true) macos.msg_void_bool(field, 'setEnabled:', enabled) @@ -1964,6 +1970,7 @@ fn native_new_text_area(el Element) NativeView { scroll_view := macos.msg_id_rect(macos.alloc('NSScrollView'), 'initWithFrame:', appkit_rect(frame)) macos.msg_void_bool(scroll_view, 'setHasVerticalScroller:', true) macos.msg_void_bool(scroll_view, 'setAutohidesScrollers:', true) + native_set_scroll_background(scroll_view, el.box) tv := native_new_text_view(macos.rect(0, 0, frame.width, frame.height), el) macos.msg_void1(scroll_view, 'setDocumentView:', tv) macos.release(tv) @@ -1979,8 +1986,8 @@ fn native_new_text_view(frame macos.Rect, el Element) NativeView { macos.msg_void_bool(tv, 'setVerticallyResizable:', true) macos.msg_void_bool(tv, 'setHorizontallyResizable:', false) macos.msg_void_u64(tv, 'setAutoresizingMask:', 2) // NSViewWidthSizable - macos.msg_void_bool(tv, 'setDrawsBackground:', !el.box.transparent) - if !el.box.transparent { + macos.msg_void_bool(tv, 'setDrawsBackground:', box_draws_fill(el.box)) + if box_draws_fill(el.box) { macos.msg_void1(tv, 'setBackgroundColor:', native_color(el.box.bg)) } macos.msg_void1(tv, 'setTextColor:', native_color(el.text_style.color)) @@ -1999,6 +2006,7 @@ fn native_update_text_area(native NativeView, el Element, declared_text_changed if !el.disable_scroll { macos.msg_void_bool(native, 'setHasVerticalScroller:', true) macos.msg_void_bool(native, 'setAutohidesScrollers:', true) + native_set_scroll_background(native, el.box) } tv := text_area_text_view(native, el.disable_scroll) if native_is_nil(tv) { @@ -2008,8 +2016,8 @@ fn native_update_text_area(native NativeView, el Element, declared_text_changed macos.msg_void_bool(tv, 'setSelectable:', true) macos.msg_void1(tv, 'setFont:', native_text_style_font(el.text_style)) macos.msg_void1(tv, 'setTextColor:', native_color(el.text_style.color)) - macos.msg_void_bool(tv, 'setDrawsBackground:', !el.box.transparent) - if !el.box.transparent { + macos.msg_void_bool(tv, 'setDrawsBackground:', box_draws_fill(el.box)) + if box_draws_fill(el.box) { macos.msg_void1(tv, 'setBackgroundColor:', native_color(el.box.bg)) } current := macos.utf8_string(macos.msg_id(tv, 'string')) @@ -2133,7 +2141,7 @@ fn native_set_background(view NativeView, hex u32) { } fn native_set_box_background(view NativeView, box BoxStyle) { - if !box.transparent { + if box_draws_fill(box) { native_set_background(view, box.bg) return } @@ -2243,7 +2251,9 @@ fn ui2_app_did_finish_launching(_self voidptr, _cmd voidptr, _notification voidp // The app delegate doubles as window delegate for windowDidResize: macos.msg_void1(st.window, 'setDelegate:', st.app_delegate) root_frame := native_rect(0, 0, f64(st.run_config.width), f64(st.run_config.height)) - st.root_view = native_new_flipped_view(root_frame, 0xffffff) + st.root_view = native_new_flipped_view(root_frame, BoxStyle{ + bg: 0xffffff + }) native_register_drop_types(st.root_view) native_set_content_view(st.window, st.root_view) macos.release(st.root_view) diff --git a/appkit/ui_macos_test.v b/appkit/ui_macos_test.v index ea345a6..cea826c 100644 --- a/appkit/ui_macos_test.v +++ b/appkit/ui_macos_test.v @@ -155,8 +155,10 @@ fn test_macos_native_style_button_keeps_appkit_bezel_and_press_state() { defer { macos.release(pool) } - button_view := native_new_button(native_rect(0, 0, 96, 40), 'Count', 0x3478d4, - 0xffffff, 15, true, false, false, 7, 1, '', true) + button_view := native_new_button(native_rect(0, 0, 96, 40), 'Count', BoxStyle{ + bg: 0x3478d4 + radius: 7 + }, 0xffffff, 15, true, false, false, 1, '', true) defer { macos.release(button_view) } @@ -166,6 +168,55 @@ fn test_macos_native_style_button_keeps_appkit_bezel_and_press_state() { assert !macos.msg_bool(button_view, 'wantsLayer') } +fn test_macos_transparent_box_controls_disable_native_backgrounds() { + pool := macos.autorelease_pool_new() + defer { + macos.release(pool) + } + transparent_box := BoxStyle{ + bg: 0xff00ff + transparent: true + } + button_view := native_new_button(native_rect(0, 0, 96, 40), 'Clear', BoxStyle{}, + 0xffffff, 15, false, false, false, 0, '', true) + scroll_view := native_new_scroll(native_rect(0, 0, 120, 80), BoxStyle{}, false) + toggle_view := native_new_toggle_button(toggle_button( + title: 'Clear toggle' + box: transparent_box + native_style: true + )) + dropdown_view := native_new_dropdown(dropdown('clear-dropdown', 'One', ['One'], rect(0, + 0, 120, 32), transparent_box, TextStyle{})) + field := native_new_text_field(text_field('clear-field', '', '', rect(0, 0, 120, 32), + transparent_box, TextStyle{}, keyboard_default)) + text_area_view := native_new_text_area(text_area('clear-area', '', rect(0, 0, 120, 80), + transparent_box, TextStyle{})) + defer { + macos.release(button_view) + macos.release(scroll_view) + macos.release(toggle_view) + macos.release(dropdown_view) + macos.release(field) + macos.release(text_area_view) + } + + native_update_button(button_view, native_rect(0, 0, 96, 40), 'Clear', transparent_box, + 0xffffff, 15, false, false, false, 0, '', true) + native_set_scroll_background(scroll_view, transparent_box) + + assert !macos.msg_bool(button_view, 'isBordered') + assert macos.msg_bool(button_view, 'wantsLayer') + layer := macos.msg_id(button_view, 'layer') + assert native_is_nil(macos.msg_id(layer, 'backgroundColor')) + assert !macos.msg_bool(scroll_view, 'drawsBackground') + assert !macos.msg_bool(toggle_view, 'isBordered') + assert !macos.msg_bool(dropdown_view, 'isBordered') + assert !macos.msg_bool(field, 'drawsBackground') + assert !macos.msg_bool(text_area_view, 'drawsBackground') + text_view := text_area_text_view(text_area_view, false) + assert !macos.msg_bool(text_view, 'drawsBackground') +} + fn test_macos_text_field_uses_native_bezel_without_layer_mask() { pool := macos.autorelease_pool_new() defer { diff --git a/examples/custom_window/custom_window_darwin.v b/examples/custom_window/custom_window_darwin.v index 7c88865..4bf9977 100644 --- a/examples/custom_window/custom_window_darwin.v +++ b/examples/custom_window/custom_window_darwin.v @@ -16,7 +16,7 @@ fn macos_configure_custom_window() bool { // small NSWindow subclass, so add the capability before dropping its frame. window_class := macos.get_class('UI2Window') if !macos_custom_window_is_nil(window_class) { - macos.add_method(window_class, 'canBecomeKey', voidptr(custom_window_can_become_key), 'B@:') + macos.add_method(window_class, 'canBecomeKeyWindow', voidptr(custom_window_can_become_key), 'B@:') macos.add_method(window_class, 'canBecomeMainWindow', voidptr(custom_window_can_become_key), 'B@:') } macos.msg_void_u64(window, 'setStyleMask:', ns_window_style_borderless) diff --git a/ui/ui.v b/ui/ui.v index 35085fa..f5e6f65 100644 --- a/ui/ui.v +++ b/ui/ui.v @@ -119,6 +119,12 @@ pub: border_bottom f64 } +// box_draws_fill is shared by native and custom renderers so every element +// backed by BoxStyle gives transparent the same meaning. +fn box_draws_fill(box BoxStyle) bool { + return !box.transparent +} + // box_border_width keeps a declared border inside its element. Border widths // are logical units, just like Rect and corner radii; each backend is // responsible for mapping those units to its native device scale. diff --git a/ui/ui_immediate.c.v b/ui/ui_immediate.c.v index 172ba81..f71f31e 100644 --- a/ui/ui_immediate.c.v +++ b/ui/ui_immediate.c.v @@ -1383,7 +1383,7 @@ $if (android || linux || ((macos || windows) && ui2_custom_rendering ?)) && !ui2 .screen { w := f64(ctx.width) h := f64(ctx.height) - if !el.box.transparent { + if box_draws_fill(el.box) { draw_rect(ctx, off_x, off_y, w - off_x, h - off_y, el.box.bg, 0) } draw_box_borders(ctx, off_x, off_y, w - off_x, h - off_y, el.box) @@ -1394,7 +1394,7 @@ $if (android || linux || ((macos || windows) && ui2_custom_rendering ?)) && !ui2 .view { x := el.frame.x + off_x y := el.frame.y + off_y - if !el.box.transparent { + if box_draws_fill(el.box) { draw_rect(ctx, x, y, el.frame.width, el.frame.height, el.box.bg, el.box.radius) } draw_box_borders(ctx, x, y, el.frame.width, el.frame.height, el.box) @@ -1421,7 +1421,9 @@ $if (android || linux || ((macos || windows) && ui2_custom_rendering ?)) && !ui2 x := el.frame.x + off_x y := el.frame.y + off_y frame := rect(x, y, el.frame.width, el.frame.height) - draw_rect(ctx, x, y, el.frame.width, el.frame.height, el.box.bg, 0) + if box_draws_fill(el.box) { + draw_rect(ctx, x, y, el.frame.width, el.frame.height, el.box.bg, 0) + } draw_box_borders(ctx, x, y, el.frame.width, el.frame.height, el.box) mut content_h := 0.0 for child in el.children { @@ -1477,11 +1479,14 @@ $if (android || linux || ((macos || windows) && ui2_custom_rendering ?)) && !ui2 .button { x := el.frame.x + off_x y := el.frame.y + off_y - if el.native_style && el.box.bg == unstyled_box_bg { - draw_button_bezel(ctx, x, y, el.frame.width, el.frame.height, el.box.radius, - el.enabled) - } else { - draw_rect(ctx, x, y, el.frame.width, el.frame.height, el.box.bg, el.box.radius) + if box_draws_fill(el.box) { + if el.native_style && el.box.bg == unstyled_box_bg { + draw_button_bezel(ctx, x, y, el.frame.width, el.frame.height, el.box.radius, + el.enabled) + } else { + draw_rect(ctx, x, y, el.frame.width, el.frame.height, el.box.bg, + el.box.radius) + } } draw_box_borders(ctx, x, y, el.frame.width, el.frame.height, el.box) draw_text_centered(ctx, el.text, x, y, el.frame.width, el.frame.height, el.text_style) @@ -1519,11 +1524,13 @@ $if (android || linux || ((macos || windows) && ui2_custom_rendering ?)) && !ui2 } box := if pressed { el.toggle_down_box } else { el.box } style := if pressed { el.toggle_down_text_style } else { el.text_style } - if el.native_style && box.bg == unstyled_box_bg { - draw_button_bezel(ctx, x, y, el.frame.width, el.frame.height, box.radius, - el.enabled) - } else { - draw_rect(ctx, x, y, el.frame.width, el.frame.height, box.bg, box.radius) + if box_draws_fill(box) { + if el.native_style && box.bg == unstyled_box_bg { + draw_button_bezel(ctx, x, y, el.frame.width, el.frame.height, box.radius, + el.enabled) + } else { + draw_rect(ctx, x, y, el.frame.width, el.frame.height, box.bg, box.radius) + } } draw_box_borders(ctx, x, y, el.frame.width, el.frame.height, box) draw_text_centered(ctx, el.text, x, y, el.frame.width, el.frame.height, style) @@ -1972,7 +1979,9 @@ $if (android || linux || ((macos || windows) && ui2_custom_rendering ?)) && !ui2 } fn draw_control_surface(ctx &gg.Context, x f64, y f64, w f64, h f64, box BoxStyle, focused bool, enabled bool) { - draw_rect(ctx, x, y, w, h, box.bg, box.radius) + if box_draws_fill(box) { + draw_rect(ctx, x, y, w, h, box.bg, box.radius) + } border := if focused { u32(0x3478d4) } else if enabled { diff --git a/ui/ui_test.v b/ui/ui_test.v index f9ea233..c25e692 100644 --- a/ui/ui_test.v +++ b/ui/ui_test.v @@ -1,5 +1,13 @@ module ui2 +fn test_transparent_box_does_not_draw_a_fill() { + assert box_draws_fill(BoxStyle{}) + assert !box_draws_fill(BoxStyle{ + bg: 0xff00ff + transparent: true + }) +} + fn test_box_border_width_stays_inside_the_element() { assert box_border_width(-1, 20) == 0 assert box_border_width(0, 20) == 0 diff --git a/ui/vml_test.v b/ui/vml_test.v index 31e35ff..59b4c62 100644 --- a/ui/vml_test.v +++ b/ui/vml_test.v @@ -318,6 +318,20 @@ fn test_vml_scroll_preserves_box_borders() { assert el.box.border_right == 2 } +fn test_vml_transparency_reaches_box_backed_controls() { + for source in [ + 'Button { transparent: true }', + 'ToggleButton { transparent: true }', + 'Scroll { transparent: true }', + 'Dropdown { transparent: true }', + 'TextField { transparent: true }', + 'TextArea { transparent: true }', + ] { + el := element_from_vml(source, rect(0, 0, 120, 40)) or { panic(err) } + assert el.box.transparent, source + } +} + fn test_vml_applies_pointer_and_transform_properties() { node := parse_vml('Image { id: movable_logo From c2ffe8c773252c51bce867eb1aa1c9623b24cf10 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Thu, 10 Sep 2026 06:38:52 +0300 Subject: [PATCH 3/5] Honor transparent boxes in UIKit --- uikit/ui_ios.v | 78 ++++++++++++++++++++++++++------------------------ 1 file changed, 41 insertions(+), 37 deletions(-) diff --git a/uikit/ui_ios.v b/uikit/ui_ios.v index a1620f2..265a459 100644 --- a/uikit/ui_ios.v +++ b/uikit/ui_ios.v @@ -306,8 +306,13 @@ fn font(size f64, bold bool) macos.Id { return macos.msg_id_f64(macos.get_class('UIFont'), 'systemFontOfSize:', size) } -fn set_background(view View, hex u32) { - macos.msg_void1(view, 'setBackgroundColor:', ios.color(hex)) +fn set_box_background(view View, box BoxStyle) { + color := if box_draws_fill(box) { + ios.color(box.bg) + } else { + macos.msg_id(macos.get_class('UIColor'), 'clearColor') + } + macos.msg_void1(view, 'setBackgroundColor:', color) } fn set_corner_radius(view View, radius f64) { @@ -478,17 +483,17 @@ fn set_scroll_content_offset_y(scroll View, y f64) { sender(voidptr(scroll), voidptr(macos.sel('setContentOffset:animated:')), offset, false) } -fn new_native_view(frame Rect, bg_hex u32, radius f64) View { +fn new_native_view(frame Rect, box BoxStyle) View { view := macos.msg_id_rect(macos.alloc('UIView'), 'initWithFrame:', native_rect(frame)) - set_background(view, bg_hex) - set_corner_radius(view, radius) + set_box_background(view, box) + set_corner_radius(view, box.radius) return view } -fn new_scroll_view(frame Rect, bg_hex u32, radius f64) View { +fn new_scroll_view(frame Rect, box BoxStyle) View { scroll := macos.msg_id_rect(macos.alloc('UIScrollView'), 'initWithFrame:', native_rect(frame)) - set_background(scroll, bg_hex) - set_corner_radius(scroll, radius) + set_box_background(scroll, box) + set_corner_radius(scroll, box.radius) macos.msg_void_bool(scroll, 'setAlwaysBounceVertical:', true) macos.msg_void_i64(scroll, 'setKeyboardDismissMode:', 1) macos.msg_void_i64(scroll, 'setContentInsetAdjustmentBehavior:', 2) @@ -538,7 +543,7 @@ fn new_text_area_view(el Element) View { fn update_text_area_view(view View, el Element, declared_text_changed bool) { macos.msg_void_rect(view, 'setFrame:', native_rect(el.frame)) - set_background(view, el.box.bg) + set_box_background(view, el.box) macos.msg_void1(view, 'setTextColor:', ios.color(el.text_style.color)) macos.msg_void1(view, 'setFont:', font(el.text_style.size, el.text_style.bold)) macos.msg_void_bool(view, 'setEditable:', !el.readonly && el.enabled) @@ -551,7 +556,7 @@ fn update_text_area_view(view View, el Element, declared_text_changed bool) { } fn new_dropdown_view(el Element) View { - view := new_button_view(el.frame, el.text, el.box.bg, el.text_style.color, el.text_style.size, el.text_style.bold, el.box.radius, el.text_style.lines) + view := new_button_view(el.frame, el.text, el.box, el.text_style.color, el.text_style.size, el.text_style.bold, el.text_style.lines) update_dropdown_view(view, el, true) return view } @@ -561,28 +566,28 @@ fn update_dropdown_view(view View, el Element, declared_text_changed bool) { if declared_text_changed && macos.utf8_string(macos.msg_id(view, 'currentTitle')) != el.text { macos.msg_void2(view, 'setTitle:forState:', macos.nsstring(el.text), macos.Id(usize(0))) } - set_background(view, el.box.bg) + set_box_background(view, el.box) set_corner_radius(view, el.box.radius) native_configure_dropdown(view, el.menu) } -fn new_button_view(frame Rect, title string, bg_hex u32, text_hex u32, size f64, bold bool, radius f64, lines int) View { +fn new_button_view(frame Rect, title string, box BoxStyle, text_hex u32, size f64, bold bool, lines int) View { btn := macos.msg_id_u64(macos.get_class('UIButton'), 'buttonWithType:', u64(0)) // buttonWithType: is autoreleased. Retain so all native_create_element // results follow the same +1 ownership contract. macos.msg_id(btn, 'retain') - update_button_view(btn, frame, title, bg_hex, text_hex, size, bold, radius, lines) + update_button_view(btn, frame, title, box, text_hex, size, bold, lines) return btn } -fn update_button_view(btn View, frame Rect, title string, bg_hex u32, text_hex u32, size f64, bold bool, radius f64, lines int) { +fn update_button_view(btn View, frame Rect, title string, box BoxStyle, text_hex u32, size f64, bold bool, lines int) { macos.msg_void_rect(btn, 'setFrame:', native_rect(frame)) macos.msg_void2(btn, 'setTitle:forState:', macos.nsstring(title), macos.Id(usize(0))) macos.msg_void2(btn, 'setTitleColor:forState:', ios.color(text_hex), macos.Id(usize(0))) - set_background(btn, bg_hex) + set_box_background(btn, box) title_label := macos.msg_id(btn, 'titleLabel') macos.msg_void1(title_label, 'setFont:', font(size, bold)) - set_corner_radius(btn, radius) + set_corner_radius(btn, box.radius) macos.msg_void_i64(title_label, 'setNumberOfLines:', i64(lines)) macos.msg_void_i64(title_label, 'setTextAlignment:', 1) macos.msg_void_i64(title_label, 'setLineBreakMode:', 4) @@ -594,14 +599,13 @@ fn checkbox_title(el Element) string { } fn new_checkbox_view(el Element) View { - view := new_button_view(el.frame, checkbox_title(el), el.box.bg, el.text_style.color, el.text_style.size, el.text_style.bold, 0, el.text_style.lines) + view := new_button_view(el.frame, checkbox_title(el), el.box, el.text_style.color, el.text_style.size, el.text_style.bold, el.text_style.lines) update_checkbox_view(view, el) return view } fn update_checkbox_view(view View, el Element) { - update_button_view(view, el.frame, checkbox_title(el), el.box.bg, el.text_style.color, el.text_style.size, el.text_style.bold, 0, el.text_style.lines) - macos.msg_void1(view, 'setBackgroundColor:', macos.msg_id(macos.get_class('UIColor'), 'clearColor')) + update_button_view(view, el.frame, checkbox_title(el), el.box, el.text_style.color, el.text_style.size, el.text_style.bold, el.text_style.lines) macos.msg_void_i64(view, 'setContentHorizontalAlignment:', 1) } @@ -620,7 +624,7 @@ fn update_switch_control_view(view View, el Element) { } fn new_toggle_button_view(el Element) View { - view := new_button_view(el.frame, el.text, el.box.bg, el.text_style.color, el.text_style.size, el.text_style.bold, el.box.radius, el.text_style.lines) + view := new_button_view(el.frame, el.text, el.box, el.text_style.color, el.text_style.size, el.text_style.bold, el.text_style.lines) update_toggle_button_view(view, el) return view } @@ -628,7 +632,7 @@ fn new_toggle_button_view(el Element) View { fn update_toggle_button_view(view View, el Element) { box := if el.checked { el.toggle_down_box } else { el.box } style := if el.checked { el.toggle_down_text_style } else { el.text_style } - update_button_view(view, el.frame, el.text, box.bg, style.color, style.size, style.bold, box.radius, style.lines) + update_button_view(view, el.frame, el.text, box, style.color, style.size, style.bold, style.lines) macos.msg_void_bool(view, 'setSelected:', el.checked) } @@ -689,15 +693,15 @@ fn native_snap_slider_value(view View, spec SliderSpec) f64 { return value } -fn new_text_field_view(frame Rect, placeholder string, t string, bg_hex u32, text_hex u32, size f64, radius f64, keyboard int, secure bool) View { +fn new_text_field_view(frame Rect, placeholder string, t string, box BoxStyle, text_hex u32, size f64, keyboard int, secure bool) View { field := macos.msg_id_rect(macos.alloc('UITextField'), 'initWithFrame:', native_rect(frame)) - update_text_field_view(field, frame, placeholder, t, bg_hex, text_hex, size, radius, keyboard, secure, true, true, 12, false, true) + update_text_field_view(field, frame, placeholder, t, box, text_hex, size, keyboard, secure, true, true, 12, false, true) return field } -fn update_text_field_view(field View, frame Rect, placeholder string, t string, bg_hex u32, text_hex u32, size f64, radius f64, keyboard int, secure bool, autocorrect bool, declared_text_changed bool, padding_left f64, readonly bool, enabled bool) { +fn update_text_field_view(field View, frame Rect, placeholder string, t string, box BoxStyle, text_hex u32, size f64, keyboard int, secure bool, autocorrect bool, declared_text_changed bool, padding_left f64, readonly bool, enabled bool) { macos.msg_void_rect(field, 'setFrame:', native_rect(frame)) - set_background(field, bg_hex) + set_box_background(field, box) macos.msg_void1(field, 'setTextColor:', ios.color(text_hex)) macos.msg_void1(field, 'setFont:', font(size, false)) macos.msg_void1(field, 'setPlaceholder:', macos.nsstring(placeholder)) @@ -711,7 +715,7 @@ fn update_text_field_view(field View, frame Rect, placeholder string, t string, macos.msg_void_i64(field, 'setAutocorrectionType:', if autocorrect { i64(0) } else { i64(1) }) macos.msg_void_bool(field, 'setEnabled:', enabled && !readonly) macos.msg_void_i64(field, 'setClearButtonMode:', 1) - set_corner_radius(field, radius) + set_corner_radius(field, box.radius) mut pad := macos.msg_id(field, 'leftView') if pad == unsafe { nil } { pad = macos.msg_id_rect(macos.alloc('UIView'), 'initWithFrame:', macos.rect(0, 0, padding_left, frame.height)) @@ -821,7 +825,7 @@ fn render_root(declared Element) { g_toggle_ids = map[u64]string{} g_toggle_views = map[u64]View{} g_scroll_ids = map[string]bool{} - set_background(g_root_view, root.box.bg) + set_box_background(g_root_view, root.box) mut active := map[string]bool{} render_children(g_root_view, root.children, '', mut active) remove_stale_nodes(active) @@ -869,22 +873,22 @@ fn render_children(parent View, children []Element, parent_key string, mut activ fn native_create_element(el Element) View { return match el.kind { .screen { View(unsafe { nil }) } - .view { new_native_view(el.frame, el.box.bg, el.box.radius) } - .scroll { new_scroll_view(el.frame, el.box.bg, el.box.radius) } + .view { new_native_view(el.frame, el.box) } + .scroll { new_scroll_view(el.frame, el.box) } .label { new_label_view(el.frame, el.text, el.text_style.color, el.text_style.size, el.text_style.bold, align_value(el.text_style.align), el.text_style.lines) } .image { new_image_view(el.frame, el.image_path, el.rotation) } .button { - new_button_view(el.frame, el.text, el.box.bg, el.text_style.color, el.text_style.size, el.text_style.bold, el.box.radius, el.text_style.lines) + new_button_view(el.frame, el.text, el.box, el.text_style.color, el.text_style.size, el.text_style.bold, el.text_style.lines) } .checkbox { new_checkbox_view(el) } .switch_control { new_switch_control_view(el) } .toggle_button { new_toggle_button_view(el) } .dropdown { new_dropdown_view(el) } .text_field { - field := new_text_field_view(el.frame, el.placeholder, el.text, el.box.bg, el.text_style.color, el.text_style.size, el.box.radius, el.keyboard, el.secure) - update_text_field_view(field, el.frame, el.placeholder, el.text, el.box.bg, el.text_style.color, el.text_style.size, el.box.radius, el.keyboard, el.secure, el.autocorrect, true, el.padding_left, el.readonly, el.enabled) + field := new_text_field_view(el.frame, el.placeholder, el.text, el.box, el.text_style.color, el.text_style.size, el.keyboard, el.secure) + update_text_field_view(field, el.frame, el.placeholder, el.text, el.box, el.text_style.color, el.text_style.size, el.keyboard, el.secure, el.autocorrect, true, el.padding_left, el.readonly, el.enabled) field } .text_area { new_text_area_view(el) } @@ -897,12 +901,12 @@ fn native_update_element(native View, el Element, declared_text_changed bool) { .screen {} .view { macos.msg_void_rect(native, 'setFrame:', native_rect(el.frame)) - set_background(native, el.box.bg) + set_box_background(native, el.box) set_corner_radius(native, el.box.radius) } .scroll { macos.msg_void_rect(native, 'setFrame:', native_rect(el.frame)) - set_background(native, el.box.bg) + set_box_background(native, el.box) set_corner_radius(native, el.box.radius) } .label { @@ -910,14 +914,14 @@ fn native_update_element(native View, el Element, declared_text_changed bool) { } .image { update_image_view(native, el.frame, el.image_path, el.rotation) } .button { - update_button_view(native, el.frame, el.text, el.box.bg, el.text_style.color, el.text_style.size, el.text_style.bold, el.box.radius, el.text_style.lines) + update_button_view(native, el.frame, el.text, el.box, el.text_style.color, el.text_style.size, el.text_style.bold, el.text_style.lines) } .checkbox { update_checkbox_view(native, el) } .switch_control { update_switch_control_view(native, el) } .toggle_button { update_toggle_button_view(native, el) } .dropdown { update_dropdown_view(native, el, declared_text_changed) } .text_field { - update_text_field_view(native, el.frame, el.placeholder, el.text, el.box.bg, el.text_style.color, el.text_style.size, el.box.radius, el.keyboard, el.secure, el.autocorrect, declared_text_changed, el.padding_left, el.readonly, el.enabled) + update_text_field_view(native, el.frame, el.placeholder, el.text, el.box, el.text_style.color, el.text_style.size, el.keyboard, el.secure, el.autocorrect, declared_text_changed, el.padding_left, el.readonly, el.enabled) } .text_area { update_text_area_view(native, el, declared_text_changed) } .slider { update_slider_view(native, el) } @@ -1045,7 +1049,7 @@ fn register_native_handlers(native View, el Element) { fn render_element(parent View, el Element, key string, mut active map[string]bool) View { active[key] = true if el.kind == .screen { - set_background(parent, el.box.bg) + set_box_background(parent, el.box) render_children(parent, el.children, key, mut active) return parent } From 2dfb4bf8107487baf15d2d54c1148b2a4ac66046 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Thu, 10 Sep 2026 07:03:27 +0300 Subject: [PATCH 4/5] Fix transparent Windows child painting --- windows/native_helpers_windows.h | 99 ++++++++++++++++++++++---------- windows/ui_windows.v | 15 +++++ 2 files changed, 83 insertions(+), 31 deletions(-) diff --git a/windows/native_helpers_windows.h b/windows/native_helpers_windows.h index d53196c..6291e6e 100644 --- a/windows/native_helpers_windows.h +++ b/windows/native_helpers_windows.h @@ -29,6 +29,7 @@ #define UI2_WM_REFRESH (WM_APP + 77) #define UI2_WM_TRAY (WM_APP + 78) +#define UI2_WM_PAINT_BACKGROUND (WM_APP + 79) enum { UI2_WIN_VIEW = 1, @@ -1010,6 +1011,71 @@ static inline void ui2_win_paint_control_border(void *hwnd_ptr, unsigned int col ReleaseDC(hwnd, dc); } +static inline void ui2_win_fill_background(HDC dc, const RECT *rect, + unsigned int background, double radius) { + HBRUSH brush = CreateSolidBrush(ui2_win_color(background)); + if (brush == NULL) return; + if (radius > 0.5) { + HPEN pen = CreatePen(PS_NULL, 0, ui2_win_color(background)); + HGDIOBJ old_brush = SelectObject(dc, brush); + HGDIOBJ old_pen = SelectObject(dc, pen); + int diameter = (int)(radius * 2.0 + 0.5); + RoundRect(dc, rect->left, rect->top, rect->right, rect->bottom, diameter, diameter); + SelectObject(dc, old_pen); + SelectObject(dc, old_brush); + DeleteObject(pen); + } else { + FillRect(dc, rect, brush); + } + DeleteObject(brush); +} + +static inline void ui2_win_paint_background_into(void *hwnd_ptr, void *dc_ptr, + unsigned int background, double radius, int transparent, + unsigned int border_color, double border_left, double border_top, + double border_right, double border_bottom) { + HWND hwnd = (HWND)hwnd_ptr; + HDC dc = (HDC)dc_ptr; + if (hwnd == NULL || dc == NULL) return; + + // UI2Container parents paint with WS_CLIPCHILDREN, so a child cannot leave + // clear pixels untouched. Rebuild the ancestor backdrop in this DC first; + // this preserves both color-key pixels and rounded ancestor boundaries. + HWND parent = GetParent(hwnd); + if (parent != NULL) { + POINT parent_origin = {0, 0}; + MapWindowPoints(parent, hwnd, &parent_origin, 1); + int saved = SaveDC(dc); + OffsetViewportOrgEx(dc, parent_origin.x, parent_origin.y, NULL); + SendMessageW(parent, UI2_WM_PAINT_BACKGROUND, (WPARAM)dc, 0); + if (saved != 0) RestoreDC(dc, saved); + } + + RECT rect; + GetClientRect(hwnd, &rect); + if (parent == NULL && transparent) { + // A top-level window has no native surface to inherit from. Its declared + // background is the deterministic clear color (and may be a color key). + ui2_win_fill_background(dc, &rect, background, 0.0); + } + if (!transparent) { + ui2_win_fill_background(dc, &rect, background, radius); + } + HRGN clip = NULL; + if (radius > 0.5) { + int diameter = (int)(radius * 2.0 + 0.5); + clip = CreateRoundRectRgn(rect.left, rect.top, rect.right + 1, rect.bottom + 1, + diameter, diameter); + if (clip != NULL) SelectClipRgn(dc, clip); + } + ui2_win_draw_borders(dc, &rect, border_color, border_left, border_top, + border_right, border_bottom); + if (clip != NULL) { + SelectClipRgn(dc, NULL); + DeleteObject(clip); + } +} + static inline void ui2_win_paint_background(void *hwnd_ptr, unsigned int background, double radius, int transparent, unsigned int border_color, double border_left, double border_top, double border_right, double border_bottom) { @@ -1017,37 +1083,8 @@ static inline void ui2_win_paint_background(void *hwnd_ptr, unsigned int backgro PAINTSTRUCT paint; HDC dc = BeginPaint(hwnd, &paint); if (dc != NULL) { - RECT rect; - GetClientRect(hwnd, &rect); - if (!transparent) { - HBRUSH brush = CreateSolidBrush(ui2_win_color(background)); - if (radius > 0.5) { - HPEN pen = CreatePen(PS_NULL, 0, ui2_win_color(background)); - HGDIOBJ old_brush = SelectObject(dc, brush); - HGDIOBJ old_pen = SelectObject(dc, pen); - int diameter = (int)(radius * 2.0 + 0.5); - RoundRect(dc, rect.left, rect.top, rect.right, rect.bottom, diameter, diameter); - SelectObject(dc, old_pen); - SelectObject(dc, old_brush); - DeleteObject(pen); - } else { - FillRect(dc, &rect, brush); - } - DeleteObject(brush); - } - HRGN clip = NULL; - if (radius > 0.5) { - int diameter = (int)(radius * 2.0 + 0.5); - clip = CreateRoundRectRgn(rect.left, rect.top, rect.right + 1, rect.bottom + 1, - diameter, diameter); - if (clip != NULL) SelectClipRgn(dc, clip); - } - ui2_win_draw_borders(dc, &rect, border_color, border_left, border_top, - border_right, border_bottom); - if (clip != NULL) { - SelectClipRgn(dc, NULL); - DeleteObject(clip); - } + ui2_win_paint_background_into(hwnd, dc, background, radius, transparent, + border_color, border_left, border_top, border_right, border_bottom); } EndPaint(hwnd, &paint); } diff --git a/windows/ui_windows.v b/windows/ui_windows.v index 9b3c0bc..5c09cea 100644 --- a/windows/ui_windows.v +++ b/windows/ui_windows.v @@ -118,6 +118,8 @@ fn C.ui2_win_apply_control_colors(dc voidptr, foreground u32, background u32, tr fn C.ui2_win_paint_background(hwnd voidptr, background u32, radius f64, transparent int, border_color u32, border_left f64, border_top f64, border_right f64, border_bottom f64) +fn C.ui2_win_paint_background_into(hwnd voidptr, dc voidptr, background u32, radius f64, transparent int, border_color u32, border_left f64, border_top f64, border_right f64, border_bottom f64) + fn C.ui2_win_paint_control_border(hwnd voidptr, color u32, radius f64, left f64, top f64, right f64, bottom f64) fn C.ui2_win_invalidate(hwnd voidptr) @@ -183,6 +185,7 @@ const win_wm_lbutton_up = u32(0x0202) const win_wm_mouse_wheel = u32(0x020a) const win_wm_dropfiles = u32(0x0233) const win_wm_refresh = u32(0x8000 + 77) +const win_wm_paint_background = u32(0x8000 + 79) const win_bn_clicked = 0 const win_cbn_selchange = 1 @@ -1367,6 +1370,18 @@ fn ui2_windows_window_proc(hwnd voidptr, message u32, wparam usize, lparam isize .checkbox, ]), brush) } + win_wm_paint_background { + box := if hwnd == st.root { + st.node_boxes[''] or { BoxStyle{} } + } else { + key := st.handle_keys[windows_handle_id(hwnd)] or { return 0 } + st.node_boxes[key] or { return 0 } + } + C.ui2_win_paint_background_into(hwnd, voidptr(wparam), box.bg, box.radius, + windows_bool(box.transparent), box.border_color, box.border_left, + box.border_top, box.border_right, box.border_bottom) + return 0 + } win_wm_paint { if hwnd == st.root { box := st.node_boxes[''] or { BoxStyle{} } From c8353f53d1f1434da4c48172882b800271c60c02 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Thu, 10 Sep 2026 07:16:39 +0300 Subject: [PATCH 5/5] Paint transparent Windows buttons --- ui/vml.v | 1 + ui/vml_test.v | 3 ++ windows/native_helpers_windows.h | 86 +++++++++++++++++++++++++++++--- windows/ui_windows.v | 30 +++++++++++ windows/ui_windows_test.v | 10 ++++ 5 files changed, 122 insertions(+), 8 deletions(-) diff --git a/ui/vml.v b/ui/vml.v index 41923ea..acf2646 100644 --- a/ui/vml.v +++ b/ui/vml.v @@ -866,6 +866,7 @@ fn node_to_element_base(node &VNode, frame Rect) !Element { down_box: BoxStyle{ bg: v_color(node, 'down_background', 0x2563eb) radius: node.prop_or('down_corner_radius', normal_box.radius.str()).f64() + transparent: normal_box.transparent } text_style: v_text_style(node) down_text_style: TextStyle{ diff --git a/ui/vml_test.v b/ui/vml_test.v index 59b4c62..c4183e0 100644 --- a/ui/vml_test.v +++ b/ui/vml_test.v @@ -329,6 +329,9 @@ fn test_vml_transparency_reaches_box_backed_controls() { ] { el := element_from_vml(source, rect(0, 0, 120, 40)) or { panic(err) } assert el.box.transparent, source + if el.kind == .toggle_button { + assert el.toggle_down_box.transparent, source + } } } diff --git a/windows/native_helpers_windows.h b/windows/native_helpers_windows.h index 6291e6e..38dc61f 100644 --- a/windows/native_helpers_windows.h +++ b/windows/native_helpers_windows.h @@ -54,6 +54,8 @@ extern int ui2_windows_context_menu(void *hwnd, int screen_x, int screen_y); extern int ui2_windows_cursor(void *hwnd); extern void ui2_windows_control_pointer(void *hwnd, unsigned int message, int x, int y); extern void ui2_windows_control_border(void *hwnd); +extern int ui2_windows_is_transparent_button(void *hwnd); +extern int ui2_windows_paint_transparent_button(void *hwnd); static inline void ui2_win_refresh_text_font(HWND hwnd); @@ -143,7 +145,11 @@ static LRESULT CALLBACK ui2_win_control_subclass(HWND hwnd, UINT message, WPARAM ui2_windows_control_pointer(hwnd, message, GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam)); if (!IsWindow(hwnd)) return 0; } + if (message == WM_ERASEBKGND && ui2_windows_is_transparent_button(hwnd)) { + return 1; + } if (message == WM_PAINT) { + if (ui2_windows_paint_transparent_button(hwnd)) return 0; LRESULT result = DefSubclassProc(hwnd, message, wparam, lparam); ui2_win_draw_placeholder(hwnd); ui2_windows_control_border(hwnd); @@ -1030,6 +1036,21 @@ static inline void ui2_win_fill_background(HDC dc, const RECT *rect, DeleteObject(brush); } +static inline void ui2_win_paint_parent_background(HWND hwnd, HDC dc) { + HWND parent = GetParent(hwnd); + if (parent == NULL) return; + POINT parent_origin = {0, 0}; + MapWindowPoints(parent, hwnd, &parent_origin, 1); + int saved = SaveDC(dc); + OffsetViewportOrgEx(dc, parent_origin.x, parent_origin.y, NULL); + SendMessageW(parent, UI2_WM_PAINT_BACKGROUND, (WPARAM)dc, 0); + if (saved != 0) { + RestoreDC(dc, saved); + } else { + OffsetViewportOrgEx(dc, -parent_origin.x, -parent_origin.y, NULL); + } +} + static inline void ui2_win_paint_background_into(void *hwnd_ptr, void *dc_ptr, unsigned int background, double radius, int transparent, unsigned int border_color, double border_left, double border_top, @@ -1042,14 +1063,7 @@ static inline void ui2_win_paint_background_into(void *hwnd_ptr, void *dc_ptr, // clear pixels untouched. Rebuild the ancestor backdrop in this DC first; // this preserves both color-key pixels and rounded ancestor boundaries. HWND parent = GetParent(hwnd); - if (parent != NULL) { - POINT parent_origin = {0, 0}; - MapWindowPoints(parent, hwnd, &parent_origin, 1); - int saved = SaveDC(dc); - OffsetViewportOrgEx(dc, parent_origin.x, parent_origin.y, NULL); - SendMessageW(parent, UI2_WM_PAINT_BACKGROUND, (WPARAM)dc, 0); - if (saved != 0) RestoreDC(dc, saved); - } + ui2_win_paint_parent_background(hwnd, dc); RECT rect; GetClientRect(hwnd, &rect); @@ -1089,6 +1103,62 @@ static inline void ui2_win_paint_background(void *hwnd_ptr, unsigned int backgro EndPaint(hwnd, &paint); } +static inline void ui2_win_paint_transparent_button(void *hwnd_ptr, + unsigned int foreground, double radius, unsigned int border_color, + double border_left, double border_top, double border_right, + double border_bottom) { + HWND hwnd = (HWND)hwnd_ptr; + if (hwnd == NULL) return; + PAINTSTRUCT paint; + HDC dc = BeginPaint(hwnd, &paint); + if (dc != NULL) { + ui2_win_paint_parent_background(hwnd, dc); + RECT rect; + GetClientRect(hwnd, &rect); + HRGN clip = NULL; + if (radius > 0.5) { + int diameter = (int)(radius * 2.0 + 0.5); + clip = CreateRoundRectRgn(rect.left, rect.top, rect.right + 1, + rect.bottom + 1, diameter, diameter); + if (clip != NULL) SelectClipRgn(dc, clip); + } + ui2_win_draw_borders(dc, &rect, border_color, border_left, border_top, + border_right, border_bottom); + if (clip != NULL) { + SelectClipRgn(dc, NULL); + DeleteObject(clip); + } + + int length = GetWindowTextLengthW(hwnd); + wchar_t *text = (wchar_t *)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, + (size_t)(length + 1) * sizeof(wchar_t)); + if (text != NULL) { + GetWindowTextW(hwnd, text, length + 1); + HFONT font = (HFONT)SendMessageW(hwnd, WM_GETFONT, 0, 0); + HGDIOBJ old_font = font == NULL ? NULL : SelectObject(dc, font); + int old_mode = SetBkMode(dc, TRANSPARENT); + COLORREF old_color = SetTextColor(dc, IsWindowEnabled(hwnd) + ? ui2_win_color(foreground) : GetSysColor(COLOR_GRAYTEXT)); + RECT text_rect = rect; + if ((SendMessageW(hwnd, BM_GETSTATE, 0, 0) & BST_PUSHED) != 0) { + OffsetRect(&text_rect, 1, 1); + } + DrawTextW(dc, text, length, &text_rect, DT_CENTER | DT_VCENTER + | DT_SINGLELINE | DT_END_ELLIPSIS | DT_NOPREFIX); + SetTextColor(dc, old_color); + SetBkMode(dc, old_mode); + if (old_font != NULL) SelectObject(dc, old_font); + HeapFree(GetProcessHeap(), 0, text); + } + if (GetFocus() == hwnd) { + RECT focus = rect; + InflateRect(&focus, -3, -3); + DrawFocusRect(dc, &focus); + } + } + EndPaint(hwnd, &paint); +} + static inline void ui2_win_invalidate(void *hwnd) { if (hwnd != NULL) InvalidateRect((HWND)hwnd, NULL, TRUE); } diff --git a/windows/ui_windows.v b/windows/ui_windows.v index 5c09cea..57df163 100644 --- a/windows/ui_windows.v +++ b/windows/ui_windows.v @@ -120,6 +120,8 @@ fn C.ui2_win_paint_background(hwnd voidptr, background u32, radius f64, transpar fn C.ui2_win_paint_background_into(hwnd voidptr, dc voidptr, background u32, radius f64, transparent int, border_color u32, border_left f64, border_top f64, border_right f64, border_bottom f64) +fn C.ui2_win_paint_transparent_button(hwnd voidptr, foreground u32, radius f64, border_color u32, border_left f64, border_top f64, border_right f64, border_bottom f64) + fn C.ui2_win_paint_control_border(hwnd voidptr, color u32, radius f64, left f64, top f64, right f64, bottom f64) fn C.ui2_win_invalidate(hwnd voidptr) @@ -311,6 +313,10 @@ fn windows_bool(value bool) int { return if value { 1 } else { 0 } } +fn windows_uses_transparent_button_paint(kind Kind, box BoxStyle) bool { + return box.transparent && kind in [.button, .toggle_button] +} + fn windows_align(align Align) int { return match align { .left { 0 } @@ -1483,6 +1489,30 @@ fn ui2_windows_control_border(hwnd voidptr) { box.border_top, box.border_right, box.border_bottom) } +@[export: 'ui2_windows_paint_transparent_button'] +fn ui2_windows_paint_transparent_button(hwnd voidptr) int { + st := windows_state() + key := st.handle_keys[windows_handle_id(hwnd)] or { return 0 } + kind := st.node_kinds[key] or { return 0 } + box := st.node_boxes[key] or { return 0 } + if !windows_uses_transparent_button_paint(kind, box) { + return 0 + } + style := st.node_text_styles[key] or { TextStyle{} } + C.ui2_win_paint_transparent_button(hwnd, style.color, box.radius, box.border_color, + box.border_left, box.border_top, box.border_right, box.border_bottom) + return 1 +} + +@[export: 'ui2_windows_is_transparent_button'] +fn ui2_windows_is_transparent_button(hwnd voidptr) int { + st := windows_state() + key := st.handle_keys[windows_handle_id(hwnd)] or { return 0 } + kind := st.node_kinds[key] or { return 0 } + box := st.node_boxes[key] or { return 0 } + return windows_bool(windows_uses_transparent_button_paint(kind, box)) +} + @[export: 'ui2_windows_edit_submit'] fn ui2_windows_edit_submit(hwnd voidptr) int { st := windows_state() diff --git a/windows/ui_windows_test.v b/windows/ui_windows_test.v index 9e73de7..5dd7c41 100644 --- a/windows/ui_windows_test.v +++ b/windows/ui_windows_test.v @@ -69,6 +69,16 @@ $if !ui2_custom_rendering ? { assert windows_content_height(children) == 115 } + fn test_windows_transparent_push_buttons_use_custom_painting() { + transparent := BoxStyle{ + transparent: true + } + assert windows_uses_transparent_button_paint(.button, transparent) + assert windows_uses_transparent_button_paint(.toggle_button, transparent) + assert !windows_uses_transparent_button_paint(.button, BoxStyle{}) + assert !windows_uses_transparent_button_paint(.checkbox, transparent) + } + fn windows_test_font_family(font voidptr) string { mut buffer := []u16{len: 32} C.ui2_win_font_family(font, unsafe { &buffer[0] }, buffer.len)