Files
v/examples/gg/multiwindow.v
2026-09-17 10:47:56 +03:00

1674 lines
56 KiB
V

// vtest build: gg_multiwindow?
// Run with `v -d gg_multiwindow run examples/gg/multiwindow.v`.
// For Linux X11/Xvfb native rendering, add `-d x_multiwindow_x11`.
// For Linux Wayland native rendering, add `-d sokol_wayland`.
// Without enabled native display support, `backend: .auto` can fall back to `.mock`.
// Without `-d gg_multiwindow`, the source still compiles and reports the opt-in error.
// CI also compiles platform render variants with `-d x_multiwindow_x11`,
// `-d sokol_wayland`, `-d sokol_metal` on macOS, and `-d sokol_d3d11` on Windows.
module main
import gg
import os
import sokol.gfx
import time
const unattended_environment = 'VGG_MULTIWINDOW_EXAMPLE_UNATTENDED'
const unattended_render_timeout = 10 * time.second
const visible_last_event_limit = 4
const visible_title_limit = 220
const visual_margin = 18
const visual_badge_size = 16
const visual_counter_height = 8
const dashboard_control_height = 24
const dashboard_control_gap = 6
const dashboard_control_text_scale = 1
const dashboard_control_horizontal_padding = 7
const client_chrome_titlebar_height = 36
const client_chrome_frame_thickness = 5
const client_chrome_resize_margin = 18
const client_chrome_close_button_size = 18
const client_chrome_close_button_margin = 9
const client_chrome_control_gap = 7
const client_chrome_title_text_scale = 2
enum DashboardControl {
inactive
new_window
close
focus
resize
show_hide
}
struct DashboardControlRect {
control DashboardControl
label string
x int
y int
width int
height int
}
struct WindowDashboard {
id gg.WindowId
mut:
label string
live bool
width int
height int
created bool
lifecycle int
inputs int
key int
text int
mouse int
scroll int
focus int
drop int
touch int
clipboard int
window int
other int
native_decorations bool
last_events []string
last_families []string
}
struct EventState {
mock bool
unattended bool
input_limit int = 12
mut:
caps gg.Capabilities
resized int
printed_inputs int
unattended_close_sent bool
hovered_window string
hovered_control DashboardControl
pressed_window string
pressed_control DashboardControl
last_action string = 'READY'
last_action_target string = 'DASHBOARD'
last_action_result string = 'WAITING'
last_action_detail string
next_window_number int = 3
action_attempts int
hidden_windows map[string]bool
last_created_window gg.WindowId
has_last_created bool
render_signaled map[string]bool
windows []WindowDashboard
}
fn main() {
run_example() or {
eprintln('multi-window example failed: ${err.msg()}')
exit(1)
}
}
fn new_app_prefer_renderer() !&gg.App {
return gg.new_app(require_renderer: true) or {
println('render dashboard fallback: require_renderer unavailable')
return gg.new_app()!
}
}
fn requested_unattended_backend() !string {
backend := os.getenv(unattended_environment)
if backend == '' {
return ''
}
if backend !in ['mock', 'x11', 'wayland', 'appkit', 'win32'] {
return error('${unattended_environment} must select mock, x11, wayland, appkit, or win32')
}
return backend
}
fn run_example() ! {
mut app := new_app_prefer_renderer()!
mut failures := []string{}
mut cleanup_needed := false
run_example_session(mut app) or {
failures << err.msg()
cleanup_needed = true
}
live_windows := app.window_ids() or {
failures << 'cleanup inspection failed: ${err.msg()}'
cleanup_needed = true
[]gg.WindowId{}
}
if live_windows.len > 0 {
cleanup_needed = true
}
if cleanup_needed {
app.stop() or { failures << 'cleanup failed: ${err.msg()}' }
}
remaining_windows := app.window_ids() or {
failures << 'cleanup validation failed: ${err.msg()}'
[]gg.WindowId{}
}
if remaining_windows.len > 0 {
failures << 'cleanup incomplete: ${remaining_windows.len} live window(s) remain'
}
if failures.len > 0 {
return error(failures.join('; '))
}
println('{"example":"multiwindow","status":"PASS","cleanup":"complete"}')
}
fn run_example_session(mut app gg.App) ! {
unattended_backend := requested_unattended_backend()!
mut caps := app.capabilities()
if unattended_backend != '' && '${caps.backend}' != unattended_backend {
return error('unattended multi-window example expected backend `${unattended_backend}`, got `${caps.backend}`')
}
println('gg multi-window backend: ${caps.backend}')
if unattended_backend != '' {
println('unattended lifecycle enabled for backend: ${unattended_backend}')
}
print_capability_families(caps)
if caps.mock {
println('render dashboard fallback: explicit swapchain unavailable')
println('mock backend selected; stopping after initial lifecycle events')
} else if caps.explicit_swapchain {
println('render dashboard enabled: explicit swapchain available')
println('visual dashboards enabled in each window; titles mirror exact state and counters')
} else {
println('render dashboard fallback: explicit swapchain unavailable')
println('renderer unavailable; titles show per-window state, backend, capabilities, last events, and counters')
}
if !caps.mock {
println('input logging enabled for key/mouse/focus/scroll/drop/touch events; first 12 events will be printed')
}
mut state := &EventState{
mock: caps.mock
unattended: unattended_backend != ''
caps: caps
}
frames := chan string{cap: 2}
main_window := if caps.explicit_swapchain {
app.create_window(
title: 'GG Multi-Window'
width: 640
height: 360
redraw_mode: .continuous
frame_fn: fn [frames, mut state] (mut window gg.WindowContext) ! {
state.draw_window(mut window, frames)!
}
)!
} else {
app.create_window(
title: 'GG Multi-Window'
width: 640
height: 360
)!
}
tools_window := if caps.explicit_swapchain {
app.create_window(
title: 'Tools'
width: 320
height: 240
redraw_mode: .continuous
frame_fn: fn [frames, mut state] (mut window gg.WindowContext) ! {
state.draw_window(mut window, frames)!
}
)!
} else {
app.create_window(
title: 'Tools'
width: 320
height: 240
)!
}
app.set_window_title(tools_window, 'Tools - updated')!
resize_or_ignore_unsupported(mut app, main_window, 720, 420)!
resize_or_ignore_unsupported(mut app, tools_window, 360, 260)!
state.track_window(main_window, 'Main', 720, 420)
state.track_window(tools_window, 'Tools', 360, 260)
state.sync_runtime(mut app)!
caps = state.caps
print_interactive_chrome_help(state.caps, state.has_client_chrome())
state.update_all_titles(mut app)!
if state.unattended && !caps.explicit_swapchain {
verify_dashboard_logical_layout()!
state.exercise_unattended_controls(mut app, frames, main_window, tools_window)!
state.sync_runtime(mut app)!
state.update_all_titles(mut app)!
}
println('live windows:')
for info in app.window_infos()! {
println(' ${info.title}: ${info.width}x${info.height} native_decorations=${info.native_decorations}')
}
if state.unattended && caps.explicit_swapchain {
verify_dashboard_logical_layout()!
driver_result := chan string{cap: 1}
action_result := chan string{cap: 1}
action_job := fn [frames, mut state, main_window, tools_window, action_result] (mut queued_app gg.App) ! {
state.exercise_unattended_controls(mut queued_app, frames, main_window, tools_window) or {
action_result <- err.msg()
return
}
action_result <- ''
}
driver := spawn drive_unattended_render(mut app, frames, [
main_window.str(),
tools_window.str(),
], tools_window.str(), action_job, action_result, driver_result)
mut run_error := ''
run_example_event_loop(mut app, mut state, frames) or { run_error = err.msg() }
driver.wait()
driver_error := <-driver_result
if run_error != '' {
return error('unattended render owner loop: ${run_error}')
}
if driver_error != '' {
return error('unattended render driver: ${driver_error}')
}
return
}
run_example_event_loop(mut app, mut state, frames)!
}
fn run_example_event_loop(mut app gg.App, mut state EventState, frames chan string) ! {
app.run(
event_fn: fn [mut state] (event gg.WindowEvent, mut app gg.App) ! {
handle_window_event(event, mut app, mut state)!
}
input_fn: fn [frames, mut state] (event gg.WindowInputEvent, mut app gg.App) ! {
handle_input_event(event, mut app, mut state, frames)!
}
)!
}
fn drive_unattended_render(mut app gg.App, frames chan string, expected []string, remapped_window string, action_job gg.AppJobFn, action_result chan string, result chan string) {
mut seen := map[string]bool{}
for seen.len < expected.len {
select {
window := <-frames {
if window in expected {
seen[window] = true
}
}
unattended_render_timeout {
post_unattended_render_stop(mut app) or {
result <- ('render barrier timeout; stop admission failed: ' + err.msg())
return
}
result <- ('render barrier timeout with ' + seen.len.str() + '/' +
expected.len.str() + ' windows rendered')
return
}
}
}
app.post(action_job) or {
result <- ('dashboard action admission failed: ' + err.msg())
return
}
select {
action_error := <-action_result {
if action_error != '' {
post_unattended_render_stop(mut app) or {}
result <- ('dashboard actions failed: ' + action_error)
return
}
}
unattended_render_timeout {
post_unattended_render_stop(mut app) or {}
result <- 'dashboard action timeout'
return
}
}
for {
select {
window := <-frames {
if window == remapped_window {
break
}
}
unattended_render_timeout {
post_unattended_render_stop(mut app) or {}
result <- 'shown window did not render after remap'
return
}
}
}
post_unattended_render_stop(mut app) or {
result <- ('stop admission failed: ' + err.msg())
return
}
result <- ''
}
fn post_unattended_render_stop(mut app gg.App) ! {
app.post(fn (mut app gg.App) ! {
app.stop()!
})!
}
fn print_capability_families(caps gg.Capabilities) {
println('capability families:')
println(' windows=${caps.multi_window} owner_queue=${caps.owner_queue} native=${caps.native}')
println(' render explicit_swapchain=${caps.explicit_swapchain} gl=${caps.gl} metal=${caps.metal} d3d11=${caps.d3d11}')
println(' input=${caps.input_events} mouse=${caps.mouse_events} keyboard=${caps.keyboard_events} text=${caps.text_events} focus=${caps.focus_events} drop=${caps.drop_events} touch=${caps.touch_events}')
println(' chrome interactive_move_resize=${caps.interactive_move_resize} native_decorations=${caps.native_decorations} cursor_shapes=${caps.cursor_shapes}')
}
fn print_interactive_chrome_help(caps gg.Capabilities, has_client_chrome bool) {
println('application controls: NEW, CLOSE, FOCUS, RESIZE, and SHOW-HIDE; hover and click inside any dashboard')
if has_client_chrome {
println('Wayland client-side titlebar/frame enabled as fallback: drag the titlebar to move; drag frame edges/corners to resize; click the close button')
println('move/resize is started from gg.App input_fn using the current native user-action serial')
return
}
if caps.interactive_move_resize && caps.native_decorations {
println('interactive move/resize available through native decorations; client chrome demo disabled')
return
}
println('interactive move/resize unavailable for this backend; application dashboard controls remain available')
}
fn handle_window_event(event gg.WindowEvent, mut app gg.App, mut state EventState) ! {
state.note_lifecycle(event)
state.update_window_title(mut app, event.window)!
match event.kind {
.window_created {
println('window created: ${event.window}')
state.schedule_unattended_close(mut app)!
}
.window_resized {
state.resized++
println('window resized: ${event.window} -> ${event.width}x${event.height}')
if state.mock && !state.unattended && state.resized >= 2 {
app.stop()!
}
}
.window_close_requested {
println('window close requested: ${event.window}')
if app.window_exists(event.window) {
app.destroy_window(event.window)!
}
}
.window_destroyed {
println('window destroyed: ${event.window}')
if app.window_ids()!.len == 0 {
app.stop()!
}
}
}
}
fn (mut state EventState) schedule_unattended_close(mut app gg.App) ! {
if !state.unattended || state.caps.explicit_swapchain || state.unattended_close_sent {
return
}
for dashboard in state.windows {
if !dashboard.created {
return
}
}
state.unattended_close_sent = true
app.post(fn (mut app gg.App) ! {
for window in app.window_ids()! {
app.destroy_window(window)!
}
})!
}
fn handle_input_event(event gg.WindowInputEvent, mut app gg.App, mut state EventState, frames chan string) ! {
message := input_event_summary(event)
if message == '' {
return
}
state.note_input(event, message)
state.sync_runtime(mut app) or {}
native_decorations := state.window_native_decorations(event.window)
hovered_control := state.update_dashboard_control_hover(event, native_decorations)
update_dashboard_cursor(event, mut app, state.caps, native_decorations, hovered_control) or {}
if !state.unattended && hovered_control != .inactive && event.event.typ == .mouse_down
&& event.event.mouse_button == .left
&& state.admit_dashboard_control_press(event.window, hovered_control) {
state.perform_dashboard_control(hovered_control, event.window, mut app, frames)
// Dashboard action failures are reflected in persistent status. Nothing fallible
// is propagated after the action, so callback replay cannot repeat the effect.
state.sync_runtime(mut app) or {}
state.update_all_titles(mut app) or {}
state.print_input(message)
return
}
action_message := maybe_begin_client_chrome_action(event, mut app, state.caps,
native_decorations) or { 'interactive chrome action failed: ${err.msg()}' }
if action_message != '' {
println(action_message)
state.note_chrome_action(event.window, action_message)
}
state.update_window_title(mut app, event.window)!
state.print_input(message)
}
fn (mut state EventState) admit_dashboard_control_press(window gg.WindowId, control DashboardControl) bool {
key := window.str()
if state.pressed_window == key && state.pressed_control == control {
return false
}
state.pressed_window = key
state.pressed_control = control
return true
}
fn (mut state EventState) print_input(message string) {
if state.printed_inputs >= state.input_limit {
return
}
state.printed_inputs++
println('input ${state.printed_inputs}/${state.input_limit}: ${message}')
}
fn (mut state EventState) exercise_unattended_controls(mut app gg.App, frames chan string, main_window gg.WindowId, tools_window gg.WindowId) ! {
start_attempts := state.action_attempts
state.perform_dashboard_control(.focus, main_window, mut app, frames)
state.perform_dashboard_control(.resize, main_window, mut app, frames)
state.perform_dashboard_control(.show_hide, main_window, mut app, frames)
if state.last_action_result != 'OK' {
return error('unattended HIDE control failed: ${state.last_action_detail}')
}
state.render_signaled[tools_window.str()] = false
state.perform_dashboard_control(.show_hide, main_window, mut app, frames)
if state.last_action_result != 'OK' {
return error('unattended SHOW control failed: ${state.last_action_detail}')
}
state.perform_dashboard_control(.new_window, tools_window, mut app, frames)
if !state.has_last_created || !app.window_exists(state.last_created_window) {
return error('unattended NEW control did not create a live window')
}
state.perform_dashboard_control(.close, state.last_created_window, mut app, frames)
if state.last_action_result != 'OK' {
return error('unattended CLOSE control was not admitted')
}
if state.action_attempts != start_attempts + 6 {
return error('unattended dashboard control count mismatch: expected=6 actual=${state.action_attempts - start_attempts}')
}
println('MULTIWINDOW_EXAMPLE_CONTROLS actions=6 focus=attempted resize=attempted hide=attempted show=attempted new=attempted close=attempted')
}
fn (mut state EventState) perform_dashboard_control(control DashboardControl, source gg.WindowId, mut app gg.App, frames chan string) {
match control {
.new_window {
label := 'Window ${state.next_window_number}'
mut created := gg.WindowId{}
if state.caps.explicit_swapchain {
created = app.create_window(
title: label
width: 480
height: 300
redraw_mode: .continuous
frame_fn: fn [frames, mut state] (mut window gg.WindowContext) ! {
state.draw_window(mut window, frames)!
}
) or {
state.record_dashboard_action('NEW', label, false, err.msg())
return
}
} else {
created = app.create_window(
title: label
width: 480
height: 300
) or {
state.record_dashboard_action('NEW', label, false, err.msg())
return
}
}
state.next_window_number++
state.track_window(created, label, 480, 300)
state.last_created_window = created
state.has_last_created = true
state.record_dashboard_action('NEW', label, true, created.str())
}
.close {
target := state.window_label(source)
app.destroy_window(source) or {
state.record_dashboard_action('CLOSE', target, false, err.msg())
return
}
state.hidden_windows.delete(source.str())
state.record_dashboard_action('CLOSE', target, true, 'destroy admitted')
}
.focus {
target := state.window_label(source)
app.request_window_focus(source) or {
state.record_dashboard_action('FOCUS', target, false, err.msg())
return
}
state.record_dashboard_action('FOCUS', target, true, 'focus requested')
}
.resize {
target := state.window_label(source)
mut width := 640
mut height := 360
if index := state.window_index(source) {
dashboard := state.windows[index]
width = if dashboard.width >= 760 { 560 } else { dashboard.width + 80 }
height = if dashboard.height >= 500 { 320 } else { dashboard.height + 50 }
}
app.resize_window(source, width, height) or {
state.record_dashboard_action('RESIZE', target, false, err.msg())
return
}
state.record_dashboard_action('RESIZE', target, true, '${width}x${height}')
}
.show_hide {
target_id := state.show_hide_target(source, &app) or {
state.record_dashboard_action('SHOW-HIDE', state.window_label(source), false,
'no live target')
return
}
target := state.window_label(target_id)
key := target_id.str()
if state.hidden_windows[key] {
app.show_window(target_id) or {
state.record_dashboard_action('SHOW', target, false, err.msg())
return
}
state.hidden_windows[key] = false
state.record_dashboard_action('SHOW', target, true, 'show requested')
} else {
app.hide_window(target_id) or {
state.record_dashboard_action('HIDE', target, false, err.msg())
return
}
state.hidden_windows[key] = true
state.record_dashboard_action('HIDE', target, true, 'hide requested')
}
}
.inactive {}
}
}
fn (state &EventState) show_hide_target(source gg.WindowId, app &gg.App) ?gg.WindowId {
for dashboard in state.windows {
if dashboard.id.str() != source.str() && app.window_exists(dashboard.id) {
return dashboard.id
}
}
if app.window_exists(source) {
return source
}
return none
}
fn (state &EventState) window_label(id gg.WindowId) string {
if index := state.window_index(id) {
return state.windows[index].label
}
return id.str()
}
fn (mut state EventState) record_dashboard_action(action string, target string, success bool, detail string) {
state.action_attempts++
state.last_action = action
state.last_action_target = target
state.last_action_result = if success { 'OK' } else { 'ERROR' }
state.last_action_detail = detail
println('dashboard control action=${action} target=${target} result=${state.last_action_result} detail=${detail}')
}
fn (mut state EventState) track_window(id gg.WindowId, label string, width int, height int) {
if index := state.window_index(id) {
mut dashboard := state.windows[index]
dashboard.label = label
dashboard.live = true
dashboard.width = width
dashboard.height = height
state.windows[index] = dashboard
return
}
state.windows << WindowDashboard{
id: id
label: label
live: true
width: width
height: height
}
}
fn (state &EventState) window_index(id gg.WindowId) ?int {
target := id.str()
for i, dashboard in state.windows {
if dashboard.id.str() == target {
return i
}
}
return none
}
fn (mut state EventState) sync_runtime(mut app gg.App) ! {
state.caps = app.capabilities()
for info in app.window_infos()! {
index := state.ensure_window(info.id)
mut dashboard := state.windows[index]
dashboard.width = info.width
dashboard.height = info.height
dashboard.native_decorations = info.native_decorations
state.windows[index] = dashboard
}
}
fn (state &EventState) window_native_decorations(id gg.WindowId) bool {
if index := state.window_index(id) {
return state.windows[index].native_decorations
}
return state.caps.native_decorations
}
fn (state &EventState) has_client_chrome() bool {
for dashboard in state.windows {
if dashboard.live && client_chrome_enabled(state.caps, dashboard.native_decorations) {
return true
}
}
return false
}
fn (mut state EventState) ensure_window(id gg.WindowId) int {
if index := state.window_index(id) {
return index
}
state.windows << WindowDashboard{
id: id
label: 'Window ${state.windows.len + 1}'
live: true
}
return state.windows.len - 1
}
fn (mut state EventState) note_lifecycle(event gg.WindowEvent) {
index := state.ensure_window(event.window)
mut dashboard := state.windows[index]
dashboard.lifecycle++
dashboard.window++
match event.kind {
.window_created {
dashboard.live = true
dashboard.created = true
dashboard.width = event.width
dashboard.height = event.height
dashboard.add_last_event('created ${event.width}x${event.height}', 'window')
}
.window_resized {
dashboard.live = true
dashboard.width = event.width
dashboard.height = event.height
dashboard.add_last_event('resized ${event.width}x${event.height}', 'window')
}
.window_close_requested {
dashboard.add_last_event('close requested', 'window')
}
.window_destroyed {
dashboard.live = false
dashboard.add_last_event('destroyed', 'window')
state.hidden_windows.delete(event.window.str())
}
}
state.windows[index] = dashboard
}
fn (mut state EventState) note_input(event gg.WindowInputEvent, message string) {
index := state.ensure_window(event.window)
mut dashboard := state.windows[index]
dashboard.inputs++
family := input_event_family(event.event)
match family {
'key' { dashboard.key++ }
'text' { dashboard.text++ }
'mouse' { dashboard.mouse++ }
'scroll' { dashboard.scroll++ }
'focus' { dashboard.focus++ }
'drop' { dashboard.drop++ }
'touch' { dashboard.touch++ }
'clipboard' { dashboard.clipboard++ }
'window' { dashboard.window++ }
else { dashboard.other++ }
}
dashboard.add_last_event(short_event_message(event.window, message), family)
state.windows[index] = dashboard
}
fn (mut state EventState) note_chrome_action(id gg.WindowId, message string) {
index := state.ensure_window(id)
mut dashboard := state.windows[index]
dashboard.window++
dashboard.add_last_event(short_event_message(id, message), 'window')
state.windows[index] = dashboard
}
fn input_event_family(input gg.Event) string {
return match input.typ {
.key_down, .key_up { 'key' }
.char { 'text' }
.mouse_down, .mouse_up, .mouse_move, .mouse_enter, .mouse_leave { 'mouse' }
.mouse_scroll { 'scroll' }
.focused, .unfocused { 'focus' }
.files_dropped { 'drop' }
.touches_began, .touches_moved, .touches_ended, .touches_cancelled { 'touch' }
.clipboard_pasted { 'clipboard' }
.resized, .iconified, .restored, .suspended, .resumed, .quit_requested { 'window' }
else { 'other' }
}
}
fn (mut dashboard WindowDashboard) add_last_event(message string, family string) {
dashboard.last_events << message
dashboard.last_families << family
for dashboard.last_events.len > visible_last_event_limit {
dashboard.last_events.delete(0)
}
for dashboard.last_families.len > visible_last_event_limit {
dashboard.last_families.delete(0)
}
}
fn short_event_message(window gg.WindowId, message string) string {
prefix := '${window}: '
if message.starts_with(prefix) {
return message[prefix.len..]
}
return message
}
fn (mut state EventState) update_all_titles(mut app gg.App) ! {
for dashboard in state.windows {
state.update_window_title(mut app, dashboard.id)!
}
}
fn (mut state EventState) update_window_title(mut app gg.App, id gg.WindowId) ! {
if !app.window_exists(id) {
return
}
index := state.window_index(id) or { return }
app.set_window_title(id, state.window_title(state.windows[index]))!
}
fn (state &EventState) window_title(dashboard WindowDashboard) string {
live := if dashboard.live { 'live ${dashboard.width}x${dashboard.height}' } else { 'closed' }
caps := compact_capabilities(state.caps, dashboard.native_decorations)
chrome := state.chrome_title_hint(dashboard)
last := if dashboard.last_events.len == 0 {
'last: waiting'
} else {
'last: ' + dashboard.last_events.join(' | ')
}
counters := 'events l=${dashboard.lifecycle} in=${dashboard.inputs} key=${dashboard.key} text=${dashboard.text} mouse=${dashboard.mouse}+${dashboard.scroll} focus=${dashboard.focus} drop=${dashboard.drop} touch=${dashboard.touch} clip=${dashboard.clipboard} win=${dashboard.window} other=${dashboard.other}'
action := 'action ${state.last_action}/${state.last_action_target}/${state.last_action_result}'
return truncate_title('${dashboard.label} | ${state.caps.backend} ${caps} | ${chrome} | ${live} | ${action} | ${counters} | ${last}')
}
fn (state &EventState) chrome_title_hint(dashboard WindowDashboard) string {
if client_chrome_enabled(state.caps, dashboard.native_decorations) {
return 'chrome: Wayland client-side titlebar/frame fallback'
}
if state.caps.interactive_move_resize && dashboard.native_decorations {
return 'chrome: native decorations'
}
return 'chrome: move/resize unavailable'
}
fn compact_capabilities(caps gg.Capabilities, native_decorations bool) string {
mut tags := []string{}
if caps.native {
tags << 'native'
} else {
tags << 'mock'
}
if caps.explicit_swapchain {
tags << 'render'
}
if caps.mouse_events {
tags << 'mouse'
}
if caps.keyboard_events {
tags << 'key'
}
if caps.text_events {
tags << 'text'
}
if caps.focus_events {
tags << 'focus'
}
if caps.drop_events {
tags << 'drop'
}
if caps.touch_events {
tags << 'touch'
}
if caps.interactive_move_resize {
tags << 'move-resize'
}
if native_decorations {
tags << 'native-decor'
} else if caps.wayland && caps.interactive_move_resize {
tags << 'client-chrome'
}
return '[' + tags.join(',') + ']'
}
fn truncate_title(title string) string {
if title.len <= visible_title_limit {
return title
}
return title[..visible_title_limit] + '...'
}
fn (mut state EventState) draw_window(mut window gg.WindowContext, frames chan string) ! {
info := window.frame_info()
id := info.window
index := state.window_index(id) or {
return error('missing dashboard state for render window ${id}')
}
dashboard := state.windows[index]
if !dashboard.live {
return
}
width := max_int(1, int(info.metrics.logical_size.width))
height := max_int(1, int(info.metrics.logical_size.height))
caps := state.caps
hovered_control := if state.hovered_window == id.str() {
state.hovered_control
} else {
DashboardControl.inactive
}
last_action := state.last_action
last_action_target := state.last_action_target
last_action_result := state.last_action_result
background := dashboard_background(caps)
action := gfx.create_clear_pass_action(background.r, background.g, background.b, background.a)
window.with_swapchain_sgl(action, fn [dashboard, caps, width, height, hovered_control, last_action, last_action_target, last_action_result] (mut drawing gg.WindowSglContext) ! {
drawing.defaults()
drawing.matrix_mode_projection()
drawing.load_identity()
drawing.ortho(0, f32(width), f32(height), 0, -1, 1)
draw_window_dashboard(mut drawing, dashboard, caps, width, height, hovered_control,
last_action, last_action_target, last_action_result)
})!
key := id.str()
if state.unattended && !state.render_signaled[key] {
state.render_signaled[key] = true
frames <- key
}
}
fn draw_rect_filled(mut window gg.WindowSglContext, x f32, y f32, width f32, height f32, color gg.Color) {
if width <= 0 || height <= 0 {
return
}
window.c4b(color.r, color.g, color.b, color.a)
window.begin_quads()
window.v2f(x, y)
window.v2f(x + width, y)
window.v2f(x + width, y + height)
window.v2f(x, y + height)
window.end()
}
fn draw_rect_empty(mut window gg.WindowSglContext, x f32, y f32, width f32, height f32, color gg.Color) {
if width <= 0 || height <= 0 {
return
}
window.c4b(color.r, color.g, color.b, color.a)
window.begin_lines()
window.v2f(x, y)
window.v2f(x + width, y)
window.v2f(x + width, y)
window.v2f(x + width, y + height)
window.v2f(x + width, y + height)
window.v2f(x, y + height)
window.v2f(x, y + height)
window.v2f(x, y)
window.end()
}
fn draw_window_dashboard(mut window gg.WindowSglContext, dashboard WindowDashboard, caps gg.Capabilities, framebuffer_width int, framebuffer_height int, hovered_control DashboardControl, last_action string, last_action_target string, last_action_result string) {
width := max_int(1, framebuffer_width)
height := max_int(1, framebuffer_height)
inner_width := max_int(64, width - 2 * visual_margin)
client_chrome := client_chrome_enabled(caps, dashboard.native_decorations)
content_top := if client_chrome {
client_chrome_titlebar_height + client_chrome_frame_thickness
} else {
0
}
draw_rect_filled(mut window, 0, 0, f32(width), f32(height), dashboard_background(caps))
draw_client_chrome_zones(mut window, dashboard, caps, width, height)
if client_chrome {
draw_rect_empty(mut window, f32(client_chrome_frame_thickness + 6), f32(content_top + 6), f32(width - 2 * client_chrome_frame_thickness - 12), f32(height - content_top -
client_chrome_frame_thickness - 12), gg.rgb(88, 101, 118))
} else {
draw_rect_filled(mut window, 0, 0, f32(width), 10, backend_color(caps))
draw_rect_empty(mut window, 8, 8, f32(width - 16), f32(height - 16), gg.rgb(96, 116, 142))
}
control_bottom := draw_dashboard_control_bar(mut window, width, content_top, hovered_control)
status_color := if last_action_result == 'ERROR' {
gg.rgb(242, 112, 112)
} else {
gg.rgb(182, 214, 232)
}
status_y := control_bottom + 8
draw_tiny_text(mut window, 'LAST ${last_action} ${last_action_target} ${last_action_result}',
visual_margin, status_y, inner_width, 1, status_color)
badge_y := status_y + 18
draw_capability_badges(mut window, caps, visual_margin, badge_y)
draw_counter_bars(mut window, dashboard, visual_margin, badge_y + 30, inner_width)
draw_last_event_strip(mut window, dashboard, visual_margin, height - 54, inner_width)
}
fn draw_dashboard_control_bar(mut window gg.WindowSglContext, width int, content_top int, hovered DashboardControl) int {
mut bottom := content_top
for control in dashboard_control_rects(width, content_top) {
active := control.control == hovered
fill := if active { gg.rgb(66, 139, 214) } else { gg.rgb(45, 57, 72) }
border := if active { gg.rgb(224, 240, 255) } else { gg.rgb(112, 132, 154) }
text_color := if active { gg.rgb(255, 255, 255) } else { gg.rgb(214, 224, 236) }
draw_rect_filled(mut window, f32(control.x), f32(control.y), f32(control.width),
f32(control.height), fill)
draw_rect_empty(mut window, f32(control.x), f32(control.y), f32(control.width),
f32(control.height), border)
text_width := control.label.len * 6 * dashboard_control_text_scale
text_x := control.x + max_int(0, (control.width - text_width) / 2)
text_y := control.y + (control.height - 7 * dashboard_control_text_scale) / 2
draw_tiny_text(mut window, control.label, text_x, text_y, control.width - 4,
dashboard_control_text_scale, text_color)
bottom = max_int(bottom, control.y + control.height)
}
return bottom
}
fn dashboard_control_label(control DashboardControl) string {
return match control {
.new_window { 'NEW' }
.close { 'CLOSE' }
.focus { 'FOCUS' }
.resize { 'RESIZE' }
.show_hide { 'SHOW-HIDE' }
.inactive { '' }
}
}
fn dashboard_control_rects(width int, content_top int) []DashboardControlRect {
controls := [DashboardControl.new_window, .close, .focus, .resize, .show_hide]
mut rects := []DashboardControlRect{cap: controls.len}
mut x := visual_margin
mut y := content_top + 14
for control in controls {
label := dashboard_control_label(control)
button_width := max_int(32, label.len * 6 * dashboard_control_text_scale +
2 * dashboard_control_horizontal_padding)
if x > visual_margin && x + button_width > width - visual_margin {
x = visual_margin
y += dashboard_control_height + dashboard_control_gap
}
rects << DashboardControlRect{
control: control
label: label
x: x
y: y
width: button_width
height: dashboard_control_height
}
x += button_width + dashboard_control_gap
}
return rects
}
fn dashboard_control_at(x f32, y f32, width int, content_top int) DashboardControl {
for control in dashboard_control_rects(width, content_top) {
if x >= f32(control.x) && x <= f32(control.x + control.width) && y >= f32(control.y)
&& y <= f32(control.y + control.height) {
return control.control
}
}
return .inactive
}
fn verify_dashboard_logical_layout() ! {
baseline := dashboard_control_rects(360, client_chrome_titlebar_height +
client_chrome_frame_thickness)
for dpi_scale in [f32(1), 2] {
framebuffer_width := int(360 * dpi_scale)
logical_width := int(f32(framebuffer_width) / dpi_scale)
rects := dashboard_control_rects(logical_width, client_chrome_titlebar_height +
client_chrome_frame_thickness)
if rects != baseline {
return error('dashboard logical layout changed at dpi scale ${dpi_scale}')
}
for rect in rects {
control := dashboard_control_at(f32(rect.x + rect.width / 2), f32(rect.y +
rect.height / 2), logical_width, client_chrome_titlebar_height +
client_chrome_frame_thickness)
if control != rect.control {
return error('dashboard hit-test mismatch at dpi scale ${dpi_scale}')
}
}
}
}
fn (mut state EventState) update_dashboard_control_hover(event gg.WindowInputEvent, native_decorations bool) DashboardControl {
input := event.event
if input.typ == .mouse_leave {
if state.hovered_window == event.window.str() {
state.hovered_window = ''
state.hovered_control = .inactive
}
if state.pressed_window == event.window.str() {
state.pressed_window = ''
state.pressed_control = .inactive
}
return .inactive
}
if input.typ == .mouse_up && state.pressed_window == event.window.str() {
state.pressed_window = ''
state.pressed_control = .inactive
}
if input.typ !in [.mouse_enter, .mouse_move, .mouse_down, .mouse_up] {
if state.hovered_window == event.window.str() {
return state.hovered_control
}
return .inactive
}
content_top := if client_chrome_enabled(state.caps, native_decorations) {
client_chrome_titlebar_height + client_chrome_frame_thickness
} else {
0
}
control := dashboard_control_at(input.mouse_x, input.mouse_y, input.window_width, content_top)
state.hovered_window = event.window.str()
state.hovered_control = control
return control
}
fn draw_client_chrome_zones(mut window gg.WindowSglContext, dashboard WindowDashboard, caps gg.Capabilities, width int, height int) {
if !client_chrome_enabled(caps, dashboard.native_decorations) {
return
}
titlebar := client_chrome_titlebar_height
frame := client_chrome_frame_thickness
inner_width := max_int(0, width - 2 * frame)
inner_height := max_int(0, height - titlebar - frame)
draw_rect_filled(mut window, 0, 0, f32(width), f32(height), gg.rgb(44, 50, 60))
draw_rect_filled(mut window, f32(frame), f32(titlebar), f32(inner_width), f32(inner_height),
dashboard_background(caps))
draw_rect_filled(mut window, f32(frame), f32(frame), f32(inner_width), f32(titlebar - frame), gg.rgb(48,
56, 68))
draw_rect_filled(mut window, f32(frame), f32(frame), f32(inner_width), 1, gg.rgb(89, 101, 118))
draw_rect_filled(mut window, f32(frame), f32(titlebar - 1), f32(inner_width), 1, gg.rgb(24, 30,
38))
draw_rect_empty(mut window, 0, 0, f32(width), f32(height), gg.rgb(20, 25, 32))
draw_rect_empty(mut window, 1, 1, f32(width - 2), f32(height - 2), gg.rgb(82, 94, 110))
draw_client_chrome_title(mut window, dashboard.label, width)
draw_client_chrome_titlebar_separators(mut window, width)
draw_client_chrome_minimize_button(mut window, width)
draw_client_chrome_maximize_button(mut window, width)
draw_client_chrome_close_button(mut window, width)
}
fn draw_client_chrome_minimize_button(mut window gg.WindowSglContext, width int) {
x := window_control_button_x(width, 2)
y := close_button_y()
size := f32(client_chrome_close_button_size)
draw_client_chrome_control_button(mut window, x, y, gg.rgb(68, 78, 92), gg.rgb(126, 140, 158))
draw_rect_filled(mut window, x + 5, y + size - 6, size - 10, 2, gg.rgb(218, 224, 232))
}
fn draw_client_chrome_maximize_button(mut window gg.WindowSglContext, width int) {
x := window_control_button_x(width, 1)
y := close_button_y()
draw_client_chrome_control_button(mut window, x, y, gg.rgb(68, 78, 92), gg.rgb(126, 140, 158))
draw_rect_empty(mut window, x + 5, y + 5, 8, 8, gg.rgb(218, 224, 232))
draw_rect_filled(mut window, x + 6, y + 5, 6, 1, gg.rgb(218, 224, 232))
}
fn draw_client_chrome_close_button(mut window gg.WindowSglContext, width int) {
x := close_button_x(width)
y := close_button_y()
size := f32(client_chrome_close_button_size)
draw_client_chrome_control_button(mut window, x, y, gg.rgb(178, 71, 72), gg.rgb(234, 150, 145))
for offset in 0 .. 5 {
step := f32(offset * 2)
draw_rect_filled(mut window, x + 5 + step, y + 5 + step, 2, 2, gg.rgb(250, 236, 232))
draw_rect_filled(mut window, x + 5 + step, y + size - 7 - step, 2, 2, gg.rgb(250, 236, 232))
}
}
fn draw_client_chrome_control_button(mut window gg.WindowSglContext, x f32, y f32, fill gg.Color, border gg.Color) {
size := f32(client_chrome_close_button_size)
draw_rect_filled(mut window, x, y, size, size, fill)
draw_rect_empty(mut window, x, y, size, size, border)
}
fn draw_client_chrome_titlebar_separators(mut window gg.WindowSglContext, width int) {
frame := client_chrome_frame_thickness
inner_width := max_int(0, width - 2 * frame)
titlebar := client_chrome_titlebar_height
separator_x := int(window_control_button_x(width, 2)) - client_chrome_control_gap
draw_rect_filled(mut window, f32(frame), f32(titlebar), f32(inner_width), 1, gg.rgb(14, 18, 24))
draw_rect_filled(mut window, f32(frame), f32(titlebar + 1), f32(inner_width), 1, gg.rgb(77, 88,
104))
if separator_x > frame + 80 {
draw_rect_filled(mut window, f32(separator_x), f32(frame + 6), 1,
f32(titlebar - frame - 12), gg.rgb(28, 34, 43))
draw_rect_filled(mut window, f32(separator_x + 1), f32(frame + 6), 1, f32(titlebar - frame -
12), gg.rgb(83, 94, 110))
}
}
fn draw_client_chrome_title(mut window gg.WindowSglContext, title string, width int) {
x := client_chrome_frame_thickness + 14
y := (client_chrome_titlebar_height - 7 * client_chrome_title_text_scale) / 2
max_width := int(window_control_button_x(width, 2)) - x - 18
draw_tiny_text(mut window, title, x, y, max_width, client_chrome_title_text_scale, gg.rgb(230,
235, 240))
}
fn draw_tiny_text(mut window gg.WindowSglContext, text string, x int, y int, max_width int, scale int, color gg.Color) {
if max_width <= 0 || scale <= 0 {
return
}
upper := text.to_upper()
mut cursor_x := x
for i in 0 .. upper.len {
if cursor_x + 5 * scale > x + max_width {
return
}
ch := upper[i]
if ch == ` ` {
cursor_x += 4 * scale
continue
}
glyph := tiny_title_glyph(ch)
for row, bits in glyph {
for col in 0 .. bits.len {
if bits[col] == `1` {
draw_rect_filled(mut window, f32(cursor_x + col * scale), f32(y + row * scale),
f32(scale), f32(scale), color)
}
}
}
cursor_x += 6 * scale
}
}
fn tiny_title_glyph(ch u8) []string {
return match ch {
`A` { ['01110', '10001', '10001', '11111', '10001', '10001', '10001'] }
`B` { ['11110', '10001', '10001', '11110', '10001', '10001', '11110'] }
`C` { ['01111', '10000', '10000', '10000', '10000', '10000', '01111'] }
`D` { ['11110', '10001', '10001', '10001', '10001', '10001', '11110'] }
`E` { ['11111', '10000', '10000', '11110', '10000', '10000', '11111'] }
`F` { ['11111', '10000', '10000', '11110', '10000', '10000', '10000'] }
`G` { ['01111', '10000', '10000', '10011', '10001', '10001', '01110'] }
`H` { ['10001', '10001', '10001', '11111', '10001', '10001', '10001'] }
`I` { ['11111', '00100', '00100', '00100', '00100', '00100', '11111'] }
`J` { ['00111', '00010', '00010', '00010', '10010', '10010', '01100'] }
`K` { ['10001', '10010', '10100', '11000', '10100', '10010', '10001'] }
`L` { ['10000', '10000', '10000', '10000', '10000', '10000', '11111'] }
`M` { ['10001', '11011', '10101', '10101', '10001', '10001', '10001'] }
`N` { ['10001', '11001', '10101', '10011', '10001', '10001', '10001'] }
`O` { ['01110', '10001', '10001', '10001', '10001', '10001', '01110'] }
`P` { ['11110', '10001', '10001', '11110', '10000', '10000', '10000'] }
`Q` { ['01110', '10001', '10001', '10001', '10101', '10010', '01101'] }
`R` { ['11110', '10001', '10001', '11110', '10100', '10010', '10001'] }
`S` { ['01111', '10000', '10000', '01110', '00001', '00001', '11110'] }
`T` { ['11111', '00100', '00100', '00100', '00100', '00100', '00100'] }
`U` { ['10001', '10001', '10001', '10001', '10001', '10001', '01110'] }
`V` { ['10001', '10001', '10001', '10001', '01010', '01010', '00100'] }
`W` { ['10001', '10001', '10001', '10101', '10101', '10101', '01010'] }
`X` { ['10001', '01010', '00100', '00100', '00100', '01010', '10001'] }
`Y` { ['10001', '01010', '00100', '00100', '00100', '00100', '00100'] }
`Z` { ['11111', '00001', '00010', '00100', '01000', '10000', '11111'] }
`0` { ['01110', '10001', '10011', '10101', '11001', '10001', '01110'] }
`1` { ['00100', '01100', '00100', '00100', '00100', '00100', '01110'] }
`2` { ['01110', '10001', '00001', '00010', '00100', '01000', '11111'] }
`3` { ['11110', '00001', '00001', '01110', '00001', '00001', '11110'] }
`4` { ['10010', '10010', '10010', '11111', '00010', '00010', '00010'] }
`5` { ['11111', '10000', '10000', '11110', '00001', '00001', '11110'] }
`6` { ['01110', '10000', '10000', '11110', '10001', '10001', '01110'] }
`7` { ['11111', '00001', '00010', '00100', '01000', '01000', '01000'] }
`8` { ['01110', '10001', '10001', '01110', '10001', '10001', '01110'] }
`9` { ['01110', '10001', '10001', '01111', '00001', '00001', '01110'] }
`-` { ['00000', '00000', '00000', '01110', '00000', '00000', '00000'] }
else { ['11111', '00001', '00010', '00100', '00100', '00000', '00100'] }
}
}
fn draw_capability_badges(mut window gg.WindowSglContext, caps gg.Capabilities, x int, y int) {
mut badge_x := x
draw_capability_badge(mut window, badge_x, y, caps.native, gg.rgb(46, 180, 125))
badge_x += visual_badge_size + 8
draw_capability_badge(mut window, badge_x, y, caps.explicit_swapchain, gg.rgb(86, 160, 255))
badge_x += visual_badge_size + 8
draw_capability_badge(mut window, badge_x, y, caps.mouse_events, gg.rgb(244, 172, 68))
badge_x += visual_badge_size + 8
draw_capability_badge(mut window, badge_x, y, caps.keyboard_events, gg.rgb(238, 98, 98))
badge_x += visual_badge_size + 8
draw_capability_badge(mut window, badge_x, y, caps.text_events, gg.rgb(184, 116, 255))
badge_x += visual_badge_size + 8
draw_capability_badge(mut window, badge_x, y, caps.focus_events, gg.rgb(94, 217, 236))
badge_x += visual_badge_size + 8
draw_capability_badge(mut window, badge_x, y, caps.drop_events, gg.rgb(238, 214, 92))
badge_x += visual_badge_size + 8
draw_capability_badge(mut window, badge_x, y, caps.touch_events, gg.rgb(255, 137, 196))
}
fn draw_capability_badge(mut window gg.WindowSglContext, x int, y int, enabled bool, color gg.Color) {
fill := if enabled { color } else { gg.rgb(42, 48, 58) }
border := if enabled { gg.rgb(220, 230, 240) } else { gg.rgb(80, 88, 100) }
draw_rect_filled(mut window, f32(x), f32(y), visual_badge_size, visual_badge_size, fill)
draw_rect_empty(mut window, f32(x), f32(y), visual_badge_size, visual_badge_size, border)
}
fn draw_counter_bars(mut window gg.WindowSglContext, dashboard WindowDashboard, x int, y int, width int) {
max_value := dashboard.max_counter()
mut row_y := y
draw_counter_bar(mut window, x, row_y, width, dashboard.lifecycle, max_value,
event_family_color('window'))
row_y += visual_counter_height + 4
draw_counter_bar(mut window, x, row_y, width, dashboard.key, max_value,
event_family_color('key'))
row_y += visual_counter_height + 4
draw_counter_bar(mut window, x, row_y, width, dashboard.mouse + dashboard.scroll, max_value,
event_family_color('mouse'))
row_y += visual_counter_height + 4
draw_counter_bar(mut window, x, row_y, width, dashboard.focus, max_value,
event_family_color('focus'))
row_y += visual_counter_height + 4
draw_counter_bar(mut window, x, row_y, width, dashboard.drop + dashboard.touch +
dashboard.clipboard, max_value, event_family_color('drop'))
row_y += visual_counter_height + 4
draw_counter_bar(mut window, x, row_y, width, dashboard.text + dashboard.other, max_value,
event_family_color('text'))
}
fn draw_counter_bar(mut window gg.WindowSglContext, x int, y int, width int, value int, max_value int, color gg.Color) {
draw_rect_filled(mut window, f32(x), f32(y), f32(width), visual_counter_height, gg.rgb(38, 45,
56))
if value <= 0 {
draw_rect_empty(mut window, f32(x), f32(y), f32(width), visual_counter_height, gg.rgb(64,
73, 88))
return
}
bar_width := scaled_width(value, max_value, width)
draw_rect_filled(mut window, f32(x), f32(y), f32(bar_width), visual_counter_height, color)
draw_rect_empty(mut window, f32(x), f32(y), f32(width), visual_counter_height, gg.rgb(132, 148,
168))
}
fn draw_last_event_strip(mut window gg.WindowSglContext, dashboard WindowDashboard, x int, y int, width int) {
slot_width := max_int(18, width / visible_last_event_limit)
for slot in 0 .. visible_last_event_limit {
family := if slot < dashboard.last_families.len {
dashboard.last_families[slot]
} else {
'empty'
}
color := event_family_color(family)
slot_x := x + slot * slot_width
draw_rect_filled(mut window, f32(slot_x), f32(y), f32(slot_width - 6), 22, color)
draw_rect_empty(mut window, f32(slot_x), f32(y), f32(slot_width - 6), 22, gg.rgb(156, 170,
188))
}
}
fn (dashboard WindowDashboard) max_counter() int {
mut maximum := 1
maximum = max_int(maximum, dashboard.lifecycle)
maximum = max_int(maximum, dashboard.key)
maximum = max_int(maximum, dashboard.text)
maximum = max_int(maximum, dashboard.mouse + dashboard.scroll)
maximum = max_int(maximum, dashboard.focus)
maximum = max_int(maximum, dashboard.drop + dashboard.touch + dashboard.clipboard)
maximum = max_int(maximum, dashboard.window)
maximum = max_int(maximum, dashboard.other)
return maximum
}
fn scaled_width(value int, max_value int, width int) int {
if value <= 0 || width <= 0 {
return 0
}
raw := value * width / max_int(1, max_value)
return max_int(8, min_int(width, raw))
}
fn dashboard_background(caps gg.Capabilities) gg.Color {
if caps.mock {
return gg.rgb(28, 34, 42)
}
if caps.win32 {
return gg.rgb(24, 36, 54)
}
if caps.backend == .appkit {
return gg.rgb(38, 31, 45)
}
if caps.x11 {
return gg.rgb(28, 42, 34)
}
if caps.wayland {
return gg.rgb(38, 39, 28)
}
return gg.rgb(30, 32, 38)
}
fn backend_color(caps gg.Capabilities) gg.Color {
if caps.mock {
return gg.rgb(104, 120, 140)
}
if caps.win32 {
return gg.rgb(66, 150, 255)
}
if caps.backend == .appkit {
return gg.rgb(236, 112, 164)
}
if caps.x11 {
return gg.rgb(82, 196, 126)
}
if caps.wayland {
return gg.rgb(230, 196, 76)
}
return gg.rgb(150, 160, 176)
}
fn event_family_color(family string) gg.Color {
return match family {
'window' { gg.rgb(88, 166, 255) }
'key' { gg.rgb(238, 98, 98) }
'text' { gg.rgb(184, 116, 255) }
'mouse' { gg.rgb(244, 172, 68) }
'scroll' { gg.rgb(245, 196, 82) }
'focus' { gg.rgb(94, 217, 236) }
'drop' { gg.rgb(238, 214, 92) }
'touch' { gg.rgb(255, 137, 196) }
'clipboard' { gg.rgb(116, 222, 164) }
'other' { gg.rgb(170, 180, 196) }
else { gg.rgb(52, 60, 72) }
}
}
fn max_int(a int, b int) int {
if a > b {
return a
}
return b
}
fn min_int(a int, b int) int {
if a < b {
return a
}
return b
}
fn input_event_summary(event gg.WindowInputEvent) string {
input := event.event
match input.typ {
.key_down {
return '${event.window}: key down ${input.key_code} repeat=${input.key_repeat} modifiers=${input.modifiers}'
}
.key_up {
return '${event.window}: key up ${input.key_code} modifiers=${input.modifiers}'
}
.char {
return '${event.window}: char code ${input.char_code}'
}
.mouse_down {
return '${event.window}: mouse down ${input.mouse_button} at ${input.mouse_x},${input.mouse_y}'
}
.mouse_up {
return '${event.window}: mouse up ${input.mouse_button} at ${input.mouse_x},${input.mouse_y}'
}
.mouse_move {
return '${event.window}: mouse move ${input.mouse_x},${input.mouse_y} delta=${input.mouse_dx},${input.mouse_dy}'
}
.mouse_scroll {
return '${event.window}: scroll ${input.scroll_x},${input.scroll_y} at ${input.mouse_x},${input.mouse_y}'
}
.mouse_enter {
return '${event.window}: mouse enter'
}
.mouse_leave {
return '${event.window}: mouse leave'
}
.touches_began {
return '${event.window}: touches began count=${input.num_touches}'
}
.touches_moved {
return '${event.window}: touches moved count=${input.num_touches}'
}
.touches_ended {
return '${event.window}: touches ended count=${input.num_touches}'
}
.touches_cancelled {
return '${event.window}: touches cancelled count=${input.num_touches}'
}
.resized {
return '${event.window}: resized ${input.window_width}x${input.window_height} framebuffer=${input.framebuffer_width}x${input.framebuffer_height}'
}
.iconified {
return '${event.window}: iconified'
}
.restored {
return '${event.window}: restored'
}
.focused {
return '${event.window}: focused'
}
.unfocused {
return '${event.window}: unfocused'
}
.suspended {
return '${event.window}: suspended'
}
.resumed {
return '${event.window}: resumed'
}
.quit_requested {
return '${event.window}: quit requested'
}
.clipboard_pasted {
return '${event.window}: clipboard pasted'
}
.files_dropped {
return '${event.window}: ${event.dropped_files.len} file(s) dropped'
}
else {
return ''
}
}
}
fn maybe_begin_client_chrome_action(event gg.WindowInputEvent, mut app gg.App, caps gg.Capabilities, native_decorations bool) !string {
if !client_chrome_enabled(caps, native_decorations) {
return ''
}
input := event.event
if input.typ != .mouse_down || input.mouse_button != .left {
return ''
}
if close_button_hit_at(input.mouse_x, input.mouse_y, input.window_width, input.window_height) {
app.destroy_window(event.window)!
return '${event.window}: client chrome close button destroyed window'
}
inactive_control := inactive_client_chrome_control_hit_at(input.mouse_x, input.mouse_y,
input.window_width, input.window_height)
if inactive_control {
return ''
}
if edge := resize_edge_at(input.mouse_x, input.mouse_y, input.window_width, input.window_height) {
app.begin_window_resize(event.window, edge)!
return '${event.window}: interactive resize ${edge} started'
}
if move_hit_at(input.mouse_x, input.mouse_y, input.window_width, input.window_height) {
app.begin_window_move(event.window)!
return '${event.window}: interactive move started'
}
return ''
}
fn update_dashboard_cursor(event gg.WindowInputEvent, mut app gg.App, caps gg.Capabilities, native_decorations bool, hovered_control DashboardControl) ! {
if !caps.cursor_shapes {
return
}
input := event.event
match input.typ {
.mouse_leave {
app.set_window_cursor(event.window, .default)!
}
.mouse_enter, .mouse_move {
shape := if hovered_control != .inactive {
gg.WindowCursorShape.pointer
} else if client_chrome_enabled(caps, native_decorations) {
client_chrome_cursor_shape_at(input.mouse_x, input.mouse_y, input.window_width,
input.window_height)
} else {
gg.WindowCursorShape.default
}
app.set_window_cursor(event.window, shape)!
}
else {}
}
}
fn client_chrome_cursor_shape_at(x f32, y f32, width int, height int) gg.WindowCursorShape {
if close_button_hit_at(x, y, width, height) {
return .pointer
}
if inactive_client_chrome_control_hit_at(x, y, width, height) {
return .default
}
if edge := resize_edge_at(x, y, width, height) {
return cursor_shape_for_resize_edge(edge)
}
if move_hit_at(x, y, width, height) {
return .move
}
return .default
}
fn cursor_shape_for_resize_edge(edge gg.WindowResizeEdge) gg.WindowCursorShape {
return match edge {
.top, .bottom { .ns_resize }
.left, .right { .ew_resize }
.top_left, .bottom_right { .nwse_resize }
.top_right, .bottom_left { .nesw_resize }
}
}
fn client_chrome_enabled(caps gg.Capabilities, native_decorations bool) bool {
return caps.wayland && caps.interactive_move_resize && !native_decorations
}
fn move_hit_at(x f32, y f32, width int, height int) bool {
if width <= 2 * client_chrome_resize_margin || height <= client_chrome_titlebar_height {
return false
}
if client_chrome_titlebar_control_hit_at(x, y, width, height) {
return false
}
return x > f32(client_chrome_resize_margin) && x < f32(width - client_chrome_resize_margin)
&& y >= 0 && y <= f32(client_chrome_titlebar_height)
}
fn inactive_client_chrome_control_hit_at(x f32, y f32, width int, height int) bool {
return window_control_button_hit_at(x, y, width, height, 1)
|| window_control_button_hit_at(x, y, width, height, 2)
}
fn client_chrome_titlebar_control_hit_at(x f32, y f32, width int, height int) bool {
return close_button_hit_at(x, y, width, height)
|| inactive_client_chrome_control_hit_at(x, y, width, height)
}
fn close_button_hit_at(x f32, y f32, width int, height int) bool {
return window_control_button_hit_at(x, y, width, height, 0)
}
fn window_control_button_hit_at(x f32, y f32, width int, height int, index_from_right int) bool {
if width <= 0 || height <= client_chrome_titlebar_height {
return false
}
button_x := window_control_button_x(width, index_from_right)
button_y := close_button_y()
button_size := f32(client_chrome_close_button_size)
return x >= button_x && x <= button_x + button_size && y >= button_y
&& y <= button_y + button_size
}
fn close_button_x(width int) f32 {
return window_control_button_x(width, 0)
}
fn window_control_button_x(width int, index_from_right int) f32 {
step := client_chrome_close_button_size + client_chrome_control_gap
return f32(width - client_chrome_close_button_margin - client_chrome_close_button_size -
index_from_right * step)
}
fn close_button_y() f32 {
return f32((client_chrome_titlebar_height - client_chrome_close_button_size) / 2)
}
fn resize_edge_at(x f32, y f32, width int, height int) ?gg.WindowResizeEdge {
if width <= 0 || height <= 0 {
return none
}
margin := f32(client_chrome_resize_margin)
w := f32(width)
h := f32(height)
near_left := x >= 0 && x <= margin
near_right := x >= w - margin && x <= w
near_top := y >= 0 && y <= margin
near_bottom := y >= h - margin && y <= h
if near_top && near_left {
return gg.WindowResizeEdge.top_left
}
if near_top && near_right {
return gg.WindowResizeEdge.top_right
}
if near_top {
return gg.WindowResizeEdge.top
}
if near_bottom && near_left {
return gg.WindowResizeEdge.bottom_left
}
if near_bottom && near_right {
return gg.WindowResizeEdge.bottom_right
}
if near_bottom {
return gg.WindowResizeEdge.bottom
}
if near_left {
return gg.WindowResizeEdge.left
}
if near_right {
return gg.WindowResizeEdge.right
}
return none
}
fn resize_or_ignore_unsupported(mut app gg.App, window gg.WindowId, width int, height int) ! {
app.resize_window(window, width, height) or {
if err.msg() == 'multiwindow: backend capability is unsupported' {
return
}
return err
}
}