v2 / vlib / gg / gg.c.v
1092 lines · 1015 sloc · 33.67 KB · cc5c71e1cb91979944a23001c75f04d6bf2669d2
Raw
1// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
2// Use of this source code is governed by an MIT license that can be found in the LICENSE file.
3
4module gg
5
6import os
7import os.font
8import time
9import sokol.sapp
10import sokol.sgl
11import sokol.gfx
12
13@[typedef]
14struct C.XRRScreenResources {
15 noutput int
16 outputs &int
17}
18
19@[typedef]
20struct C.XRROutputInfo {
21 crtc u64
22}
23
24@[typedef]
25struct C.XRRCrtcInfo {
26 width u32
27 height u32
28}
29
30fn C.XOpenDisplay(i32) voidptr
31fn C.XCloseDisplay(voidptr) i32
32fn C.DefaultScreen(voidptr) i32
33fn C.DefaultRootWindow(voidptr) u64
34fn C.XRRGetScreenResources(voidptr, u64) &C.XRRScreenResources
35fn C.XRRGetOutputPrimary(voidptr, u64) u64
36fn C.XRRFreeScreenResources(&C.XRRScreenResources)
37fn C.XRRGetOutputInfo(voidptr, &C.XRRScreenResources, u64) &C.XRROutputInfo
38fn C.XRRFreeOutputInfo(&C.XRROutputInfo)
39fn C.XRRGetCrtcInfo(voidptr, &C.XRRScreenResources, u64) &C.XRRCrtcInfo
40fn C.XRRFreeCrtcInfo(&C.XRRCrtcInfo)
41
42$if linux {
43 #flag -lXrandr
44 #include <X11/extensions/Xrandr.h>
45}
46
47$if windows {
48 #flag -lgdi32
49 #include "windows.h"
50}
51
52#flag wasm32_emscripten --embed-file @VEXEROOT/examples/assets/fonts/RobotoMono-Regular.ttf@/assets/fonts/RobotoMono-Regular.ttf
53
54$if macos {
55 fn C.gg_macos_resize_window(window voidptr, width int, height int)
56 fn C.gg_macos_set_window_resizable(window voidptr, resizable bool)
57}
58
59$if windows {
60 struct C.RECT {
61 left i32
62 top i32
63 right i32
64 bottom i32
65 }
66
67 fn C.GetWindowRect(hwnd voidptr, rect &C.RECT) int
68 fn C.GetWindowLongW(hwnd voidptr, index int) i32
69 fn C.SetWindowLongW(hwnd voidptr, index int, new_long i32) i32
70 fn C.AdjustWindowRectEx(rect &C.RECT, style u32, menu int, ex_style u32) int
71 fn C.SetWindowPos(hwnd voidptr, hwnd_insert_after voidptr, x int, y int, cx int, cy int, flags u32) int
72}
73
74// call Windows API to get screen size
75fn C.GetSystemMetrics(i32) i32
76fn C.XResizeWindow(display voidptr, window u64, width u32, height u32) int
77fn C.XFlush(display voidptr) int
78
79pub type TouchPoint = C.sapp_touchpoint
80
81pub struct Event {
82pub mut:
83 frame_count u64
84 typ sapp.EventType
85 key_code KeyCode
86 char_code u32
87 key_repeat bool
88 modifiers u32
89 mouse_button MouseButton
90 mouse_x f32
91 mouse_y f32
92 mouse_dx f32
93 mouse_dy f32
94 scroll_x f32
95 scroll_y f32
96 num_touches int
97 touches [8]TouchPoint
98 window_width int
99 window_height int
100 framebuffer_width int
101 framebuffer_height int
102}
103
104pub struct Config {
105pub:
106 width int = 800 // desired start width of the window
107 height int = 600 // desired start height of the window
108 retina bool // TODO: implement or deprecate
109 resizable bool = true // prevent user-initiated resizing when supported by the platform backend
110 user_data voidptr // a custom pointer to the application data/instance. When it is not set explicitly, it will default to a pointer to the current gg.Context instance.
111 font_size int // TODO: implement or deprecate
112 create_window bool // TODO: implement or deprecate
113 // window_user_ptr voidptr
114 window_title string = 'A GG Window. Set window_title: to change it.' // the desired title of the window
115 icon sapp.IconDesc
116 html5_canvas_name string = 'canvas'
117 borderless_window bool // create the window without native decorations when supported by the platform backend
118 always_on_top bool // TODO: implement or deprecate
119 bg_color Color // The background color of the window. By default, the first thing gg does in ctx.begin(), is clear the whole buffer with that color.
120 init_fn FNCb = unsafe { nil } // Called once, after Sokol has finished its setup. Some gg and Sokol functions have to be called *in this* callback, or after this callback, but not before
121 frame_fn FNCb = unsafe { nil } // Called once per frame, usually 60 times a second (depends on swap_interval). See also https://dri.freedesktop.org/wiki/ConfigurationOptions/#synchronizationwithverticalrefreshswapintervals
122 native_frame_fn FNCb = unsafe { nil }
123 cleanup_fn FNCb = unsafe { nil } // Called once, after Sokol determines that the application is finished/closed. Put your app specific cleanup/free actions here.
124 fail_fn FNFail = unsafe { nil } // Called once per Sokol error/log message. TODO: currently it does nothing with latest Sokol, reimplement using Sokol's new sapp_logger APIs.
125
126 update_fn FNUpdate = unsafe { nil } // Called once at the start of each frame, so usually ~60 times a second. The first argument is the delta `dt` time passed, since the *previous* update call (in seconds).
127
128 event_fn FNEvent = unsafe { nil } // Called once per each user initiated event, received by Sokol/GG.
129 on_event FNEvent2 = unsafe { nil } // Called once per each user initiated event, received by Sokol/GG. Same as event_fn, just the parameter order is different. TODO: deprecate this, in favor of event_fn
130 quit_fn FNEvent = unsafe { nil } // Called when the user closes the app window.
131
132 keydown_fn FNKeyDown = unsafe { nil } // Called once per key press, no matter how long the key is held down. Note that here you can access the scan code/physical key, but not the logical character.
133 keyup_fn FNKeyUp = unsafe { nil } // Called once per key press, when the key is released.
134 char_fn FNChar = unsafe { nil } // Called once per character (after the key is pressed down, and then released). Note that you can access the character/utf8 rune here, not just the scan code.
135
136 move_fn FNMove = unsafe { nil } // Called while the mouse/touch point is moving.
137 click_fn FNClick = unsafe { nil } // Called once when the mouse/touch button is clicked.
138 unclick_fn FNUnClick = unsafe { nil } // Called once when the mouse/touch button is released.
139 leave_fn FNEvent = unsafe { nil } // Called once when the mouse/touch point leaves the window.
140 enter_fn FNEvent = unsafe { nil } // Called once when the mouse/touch point enters again the window.
141 resized_fn FNEvent = unsafe { nil } // Called once when the window has changed its size.
142 scroll_fn FNEvent = unsafe { nil } // Called while the user is scrolling. The direction of scrolling is indicated by either 1 or -1.
143 // wait_events bool // set this to true for UIs, to save power
144 fullscreen bool // set this to true, if you want your window to start in fullscreen mode (suitable for games/demos/screensavers)
145 scale f32 = 1.0
146 sample_count int // bigger values usually have performance impact, but can produce smoother/antialiased lines, if you draw lines or polygons (2 is usually good enough)
147 texture_filter TextureFilter = .linear // default texture filter for newly created images; use `.nearest` for pixel art scaling
148 swap_interval int = 1 // 1 = 60fps, 2 = 30fps etc. Honored on Windows, macOS, Linux, iOS, and HTML5; Android support is not implemented yet.
149 // ved needs this
150 // init_text bool
151 font_path string
152 custom_bold_font_path string
153 ui_mode bool // refreshes only on events to save CPU usage
154 // font bytes for embedding
155 font_bytes_normal []u8
156 font_bytes_bold []u8
157 font_bytes_mono []u8
158 font_bytes_italic []u8
159 native_rendering bool // Cocoa on macOS/iOS, GDI+ on Windows
160 // drag&drop
161 enable_dragndrop bool // enable file dropping (drag'n'drop), default is false
162 max_dropped_files int = 1 // max number of dropped files to process (default: 1)
163 max_dropped_file_path_length int = 2048 // max length in bytes of a dropped UTF-8 file path (default: 2048)
164
165 min_width int // desired minimum width of the window
166 min_height int // desired minimum height of the window
167}
168
169@[heap]
170pub struct PipelineContainer {
171pub mut:
172 alpha sgl.Pipeline
173 add sgl.Pipeline
174}
175
176fn (mut container PipelineContainer) init_pipeline() {
177 // FIXME(FireRedz): this looks kinda funny, find a better way to initialize pipeline.
178
179 // Alpha
180 mut alpha_pipdesc := gfx.PipelineDesc{}
181 unsafe { vmemset(&alpha_pipdesc, 0, int(sizeof(alpha_pipdesc))) }
182 alpha_pipdesc.label = c'alpha-pipeline'
183 alpha_pipdesc.colors[0] = gfx.ColorTargetState{
184 blend: gfx.BlendState{
185 enabled: true
186 src_factor_rgb: .src_alpha
187 dst_factor_rgb: .one_minus_src_alpha
188 }
189 }
190 container.alpha = sgl.make_pipeline(&alpha_pipdesc)
191
192 // Add
193 mut add_pipdesc := gfx.PipelineDesc{}
194 unsafe { vmemset(&add_pipdesc, 0, int(sizeof(add_pipdesc))) }
195 add_pipdesc.label = c'additive-pipeline'
196 add_pipdesc.colors[0] = gfx.ColorTargetState{
197 blend: gfx.BlendState{
198 enabled: true
199 src_factor_rgb: .src_alpha
200 dst_factor_rgb: .one
201 }
202 }
203 container.add = sgl.make_pipeline(&add_pipdesc)
204}
205
206@[heap]
207pub struct Context {
208mut:
209 render_text bool = true
210 // a cache with all images created by the user. used for sokol image init and to save space
211 // (so that the user can store image ids, not entire Image objects)
212 image_cache []Image
213 needs_refresh bool = true
214 ticks int // for ui mode only
215 last_bg_overlay_frame u64
216 translucent_bg_seed_pending bool
217pub:
218 native_rendering bool
219pub mut:
220 scale f32 = 1.0 // will get set to 2.0 for retina, will remain 1.0 for normal
221 width int
222 height int
223 clear_pass gfx.PassAction
224 bg_color Color
225 window sapp.Desc
226 pipeline &PipelineContainer = unsafe { nil }
227 config Config
228 user_data voidptr
229 ft &FT = unsafe { nil }
230 font_inited bool
231 ui_mode bool // do not redraw everything 60 times/second, but only when the user requests
232 frame u64 // the current frame counted from the start of the application; always increasing
233 //
234 timer time.StopWatch // starts right after new_context, and can be controlled/stopped/restarted in whatever way the user wants.
235 update_timer time.StopWatch // measures how much time has passed since the start of the frame.
236 frame_timer time.StopWatch // enforces swap_interval as a fallback when the platform ignores vsync.
237 // Note: when there is an update_fn, this timer is reset by GG itself, at the start of each frame.
238
239 mbtn_mask u8
240 mouse_buttons MouseButtons // typed version of mbtn_mask; easier to use for user programs
241 mouse_pos_x int
242 mouse_pos_y int
243 mouse_dx int
244 mouse_dy int
245 scroll_x int
246 scroll_y int
247
248 key_modifiers Modifier // the current key modifiers
249 key_repeat bool // whether the pressed key was an autorepeated one
250 pressed_keys [key_code_max]bool // an array representing all currently pressed keys
251 pressed_keys_edge [key_code_max]bool // true when the previous state of pressed_keys,
252 // *before* the current event was different
253 fps FPSConfig
254 has_started bool
255}
256
257fn gg_init_sokol_window(user_data voidptr) {
258 mut ctx := unsafe { &Context(user_data) }
259 desc := sapp.create_desc()
260 /*
261 desc := gfx.Desc{
262 mtl_device: sapp.metal_get_device()
263 mtl_renderpass_descriptor_cb: sapp.metal_get_renderpass_descriptor
264 mtl_drawable_cb: sapp.metal_get_drawable
265 d3d11_device: sapp.d3d11_get_device()
266 d3d11_device_context: sapp.d3d11_get_device_context()
267 d3d11_render_target_view_cb: sapp.d3d11_get_render_target_view
268 d3d11_depth_stencil_view_cb: sapp.d3d11_get_depth_stencil_view
269 }
270 */
271 gfx.setup(&desc)
272 sgl_desc := sgl.Desc{}
273 sgl.setup(&sgl_desc)
274 ctx.set_scale()
275 if !ctx.config.resizable {
276 gg_apply_window_resizable()
277 }
278 // is_high_dpi := sapp.high_dpi()
279 // fb_w := sapp.width()
280 // fb_h := sapp.height()
281 // println('ctx.scale=${ctx.scale} is_high_dpi=${is_high_dpi} fb_w=${fb_w} fb_h=${fb_h}')
282 // if ctx.config.init_text {
283 // `os.is_file()` won't work on Android if the font file is embedded into the APK
284 exists := $if !android { os.is_file(ctx.config.font_path) } $else { true }
285 if ctx.config.font_path != '' && exists {
286 // t := time.ticks()
287 ctx.ft = new_ft(
288 font_path: ctx.config.font_path
289 custom_bold_font_path: ctx.config.custom_bold_font_path
290 scale: ctx.scale
291 ) or { panic(err) }
292 // println('FT took ${time.ticks()-t} ms')
293 ctx.font_inited = true
294 } else {
295 if ctx.config.font_bytes_normal.len > 0 {
296 ctx.ft = new_ft(
297 bytes_normal: ctx.config.font_bytes_normal
298 bytes_bold: ctx.config.font_bytes_bold
299 bytes_mono: ctx.config.font_bytes_mono
300 bytes_italic: ctx.config.font_bytes_italic
301 scale: sapp.dpi_scale()
302 ) or { panic(err) }
303 ctx.font_inited = true
304 } else {
305 sfont := font.default()
306 if ctx.config.font_path != '' {
307 eprintln('font file "${ctx.config.font_path}" does not exist, the system font (${sfont}) was used instead.')
308 }
309
310 ctx.ft = new_ft(
311 font_path: sfont
312 custom_bold_font_path: ctx.config.custom_bold_font_path
313 scale: sapp.dpi_scale()
314 ) or { panic(err) }
315 ctx.font_inited = true
316 }
317 }
318
319 // Pipeline
320 ctx.pipeline = &PipelineContainer{}
321 ctx.pipeline.init_pipeline()
322
323 ctx.timer = time.new_stopwatch()
324 ctx.update_timer = time.new_stopwatch()
325 if ctx.config.init_fn != unsafe { nil } {
326 $if android {
327 // NOTE on Android sokol can emit resize events *before* the init function is
328 // called (Android has to initialize a lot more through the Activity system to
329 // reach a valid coontext) and thus the user's code will miss the resize event.
330 // To prevent this we emit a custom window resize event, if the screen size has
331 // changed meanwhile.
332 win_size := ctx.window_size()
333 if ctx.width != win_size.width || ctx.height != win_size.height {
334 ctx.set_cached_window_size(win_size.width, win_size.height)
335 if ctx.config.resized_fn != unsafe { nil } {
336 e := Event{
337 typ: .resized
338 window_width: ctx.width
339 window_height: ctx.height
340 }
341 ctx.config.resized_fn(&e, ctx.user_data)
342 }
343 }
344 }
345 ctx.config.init_fn(ctx.user_data)
346 }
347 ctx.has_started = true
348 // Create images now that we can do that after sg is inited
349 if ctx.native_rendering {
350 return
351 }
352
353 for i in 0 .. ctx.image_cache.len {
354 if ctx.image_cache[i].simg.id == 0 {
355 ctx.image_cache[i].init_sokol_image()
356 }
357 }
358}
359
360fn gg_frame_fn(mut ctx Context) {
361 ctx.frame++
362 if ctx.config.frame_fn == unsafe { nil } {
363 return
364 }
365 ctx.pace_frame()
366 if ctx.native_rendering {
367 // return
368 }
369 defer {
370 ctx.mouse_dx = 0
371 ctx.mouse_dy = 0
372 ctx.scroll_x = 0
373 ctx.scroll_y = 0
374 }
375
376 ctx.record_frame()
377 ctx.memory_trace_frame()
378
379 if ctx.ui_mode && !ctx.needs_refresh {
380 // println('ui mode, exiting')
381 // Draw 3 more frames after the "stop refresh" command
382 ctx.ticks++
383 if ctx.ticks > 3 {
384 return
385 }
386 }
387 if ctx.config.update_fn != unsafe { nil } {
388 dt := ctx.update_timer.elapsed().seconds()
389 ctx.update_timer.restart()
390 ctx.config.update_fn(f32(dt), ctx.user_data)
391 }
392 ctx.config.frame_fn(ctx.user_data)
393 ctx.needs_refresh = false
394}
395
396fn swap_interval_frame_budget(swap_interval int) time.Duration {
397 if swap_interval <= 0 {
398 return time.Duration(0)
399 }
400 return time.Duration(swap_interval) * time.second / 60
401}
402
403fn (mut ctx Context) pace_frame() {
404 frame_budget := swap_interval_frame_budget(ctx.config.swap_interval)
405 if frame_budget <= 0 || ctx.frame <= 1 {
406 ctx.frame_timer.restart()
407 return
408 }
409 elapsed := ctx.frame_timer.elapsed()
410 $if !emscripten ? {
411 if elapsed < frame_budget {
412 time.sleep(frame_budget - elapsed)
413 }
414 }
415 ctx.frame_timer.restart()
416}
417
418fn gg_event_fn(ce voidptr, user_data voidptr) {
419 // e := unsafe { &sapp.Event(ce) }
420 mut e := unsafe { &Event(ce) }
421 mut ctx := unsafe { &Context(user_data) }
422 if ctx.ui_mode {
423 ctx.refresh_ui()
424 }
425 if e.typ == .mouse_down {
426 bitplace := int(e.mouse_button)
427 ctx.mbtn_mask |= u8(1 << bitplace)
428 ctx.mouse_buttons = unsafe { MouseButtons(ctx.mbtn_mask) }
429 }
430 if e.typ == .mouse_up {
431 bitplace := int(e.mouse_button)
432 ctx.mbtn_mask &= ~(u8(1 << bitplace))
433 ctx.mouse_buttons = unsafe { MouseButtons(ctx.mbtn_mask) }
434 }
435 if e.typ == .mouse_move && e.mouse_button == .invalid {
436 if ctx.mbtn_mask & 0x01 > 0 {
437 e.mouse_button = .left
438 }
439 if ctx.mbtn_mask & 0x02 > 0 {
440 e.mouse_button = .right
441 }
442 if ctx.mbtn_mask & 0x04 > 0 {
443 e.mouse_button = .middle
444 }
445 }
446 e.mouse_x /= ctx.scale
447 e.mouse_y /= ctx.scale
448 e.mouse_dx /= ctx.scale
449 e.mouse_dy /= ctx.scale
450 e.scroll_x /= ctx.scale
451 e.scroll_y /= ctx.scale
452 ctx.mouse_pos_x = int(e.mouse_x)
453 ctx.mouse_pos_y = int(e.mouse_y)
454 ctx.mouse_dx = int(e.mouse_dx)
455 ctx.mouse_dy = int(e.mouse_dy)
456 ctx.scroll_x = int(e.scroll_x)
457 ctx.scroll_y = int(e.scroll_y)
458 ctx.key_modifiers = unsafe { Modifier(e.modifiers) }
459 ctx.key_repeat = e.key_repeat
460 if e.typ in [.key_down, .key_up] {
461 key_idx := int(e.key_code) % key_code_max
462 prev := ctx.pressed_keys[key_idx]
463 next := e.typ == .key_down
464 ctx.pressed_keys[key_idx] = next
465 ctx.pressed_keys_edge[key_idx] = prev != next
466 }
467 if ctx.config.event_fn != unsafe { nil } {
468 ctx.config.event_fn(e, ctx.user_data)
469 } else if ctx.config.on_event != unsafe { nil } {
470 ctx.config.on_event(ctx.user_data, e)
471 }
472 match e.typ {
473 .mouse_move {
474 if ctx.config.move_fn != unsafe { nil } {
475 ctx.config.move_fn(e.mouse_x, e.mouse_y, ctx.user_data)
476 }
477 }
478 .mouse_down {
479 if ctx.config.click_fn != unsafe { nil } {
480 ctx.config.click_fn(e.mouse_x, e.mouse_y, e.mouse_button, ctx.user_data)
481 }
482 }
483 .mouse_up {
484 if ctx.config.unclick_fn != unsafe { nil } {
485 ctx.config.unclick_fn(e.mouse_x, e.mouse_y, e.mouse_button, ctx.user_data)
486 }
487 }
488 .mouse_leave {
489 if ctx.config.leave_fn != unsafe { nil } {
490 ctx.config.leave_fn(e, ctx.user_data)
491 }
492 }
493 .mouse_enter {
494 if ctx.config.enter_fn != unsafe { nil } {
495 ctx.config.enter_fn(e, ctx.user_data)
496 }
497 }
498 .mouse_scroll {
499 if ctx.config.scroll_fn != unsafe { nil } {
500 ctx.config.scroll_fn(e, ctx.user_data)
501 }
502 }
503 .key_down {
504 if ctx.config.keydown_fn != unsafe { nil } {
505 ctx.config.keydown_fn(e.key_code, unsafe { Modifier(e.modifiers) }, ctx.user_data)
506 }
507 }
508 .key_up {
509 if ctx.config.keyup_fn != unsafe { nil } {
510 ctx.config.keyup_fn(e.key_code, unsafe { Modifier(e.modifiers) }, ctx.user_data)
511 }
512 }
513 .char {
514 if ctx.config.char_fn != unsafe { nil } {
515 ctx.config.char_fn(e.char_code, ctx.user_data)
516 }
517 }
518 .resized {
519 ctx.scale = dpi_scale()
520 ctx.ft.scale = ctx.scale
521 ctx.set_cached_window_size(e.window_width, e.window_height)
522 if ctx.config.resized_fn != unsafe { nil } {
523 ctx.config.resized_fn(e, ctx.user_data)
524 }
525 }
526 .quit_requested {
527 if ctx.config.quit_fn != unsafe { nil } {
528 ctx.config.quit_fn(e, ctx.user_data)
529 }
530 }
531 else {
532 // dump(e)
533 }
534 }
535
536 $if windows || (linux && !sokol_wayland ?) {
537 if e.typ == .key_down && e.key_code in [.backspace, .delete, .enter, .tab] {
538 // with Win32 and X11, sokol does not send .char events for some keys;
539 // we will emulate them for consistency here:
540 // NOTE: on Wayland (sokol_wayland), the backend already sends real CHAR events for these
541 // keys via xkb_state_key_get_utf8, so this block must not run there or events double-fire.
542 e.char_code = match e.key_code {
543 .backspace { u32(8) }
544 .tab { 9 }
545 .enter { 13 }
546 .delete { 127 }
547 else { u32(e.key_code) }
548 }
549
550 e.key_code = .invalid
551 e.typ = .char
552 if ctx.config.event_fn != unsafe { nil } {
553 ctx.config.event_fn(e, ctx.user_data)
554 } else if ctx.config.on_event != unsafe { nil } {
555 ctx.config.on_event(ctx.user_data, e)
556 }
557 if ctx.config.char_fn != unsafe { nil } {
558 ctx.config.char_fn(e.char_code, ctx.user_data)
559 }
560 }
561 }
562}
563
564fn gg_cleanup_fn(user_data voidptr) {
565 mut ctx := unsafe { &Context(user_data) }
566 if ctx.config.cleanup_fn != unsafe { nil } {
567 ctx.config.cleanup_fn(ctx.user_data)
568 }
569 gfx.shutdown()
570}
571
572fn gg_fail_fn(msg &char, user_data voidptr) {
573 mut ctx := unsafe { &Context(user_data) }
574 vmsg := unsafe { tos3(msg) }
575 if ctx.config.fail_fn != unsafe { nil } {
576 ctx.config.fail_fn(vmsg, ctx.user_data)
577 } else {
578 eprintln('gg error: ${vmsg}')
579 }
580}
581
582//---- public methods
583
584// start creates a new context and runs it right away.
585// It is a convenient way to start short/throwaway gg based prototypes,
586// that do not need to keep and update their own state, like simple
587// animations/visualisations that depend only on the time, or the ctx.frame counter.
588// Use gg.new_context() for more complex ones.
589pub fn start(cfg Config) {
590 mut ctx := new_context(cfg)
591 ctx.run()
592}
593
594// new_context returns an initialized `Context` allocated on the heap.
595pub fn new_context(cfg Config) &Context {
596 mut ctx := &Context{
597 user_data: cfg.user_data
598 width: cfg.width
599 height: cfg.height
600 config: cfg
601 ft: unsafe { nil }
602 ui_mode: cfg.ui_mode
603 native_rendering: cfg.native_rendering
604 window: sapp.Desc{
605 init_userdata_cb: gg_init_sokol_window
606 frame_userdata_cb: gg_frame_fn
607 event_userdata_cb: gg_event_fn
608 // fail_userdata_cb: gg_fail_fn
609 cleanup_userdata_cb: gg_cleanup_fn
610 window_title: &char(cfg.window_title.str)
611 icon: cfg.icon
612 html5: sapp.Html5Desc{
613 canvas_selector: &char(cfg.html5_canvas_name.str)
614 }
615 width: cfg.width
616 height: cfg.height
617 sample_count: cfg.sample_count
618 high_dpi: true
619 fullscreen: cfg.fullscreen
620 __v_native_render: cfg.native_rendering
621 min_width: cfg.min_width
622 min_height: cfg.min_height
623 borderless_window: cfg.borderless_window
624 // drag&drop
625 enable_dragndrop: cfg.enable_dragndrop
626 max_dropped_files: cfg.max_dropped_files
627 max_dropped_file_path_length: cfg.max_dropped_file_path_length
628 swap_interval: cfg.swap_interval
629 }
630 }
631 ctx.set_bg_color(cfg.bg_color)
632 // C.printf('new_context() %p\n', cfg.user_data)
633 return ctx
634}
635
636fn (mut ctx Context) set_cached_window_size(width int, height int) {
637 ctx.width = width
638 ctx.height = height
639 ctx.window.width = width
640 ctx.window.height = height
641}
642
643fn gg_apply_window_resizable() {
644 $if macos {
645 window := sapp.macos_get_window()
646 if window != unsafe { nil } {
647 C.gg_macos_set_window_resizable(window, false)
648 }
649 } $else $if windows {
650 hwnd := sapp.win32_get_hwnd()
651 if hwnd == unsafe { nil } {
652 return
653 }
654 mut style := u32(C.GetWindowLongW(hwnd, C.GWL_STYLE))
655 style &= ~u32(C.WS_SIZEBOX | C.WS_MAXIMIZEBOX)
656 C.SetWindowLongW(hwnd, C.GWL_STYLE, i32(style))
657 C.SetWindowPos(hwnd, unsafe { nil }, 0, 0, 0, 0,
658 u32(C.SWP_NOMOVE | C.SWP_NOSIZE | C.SWP_NOZORDER | C.SWP_NOACTIVATE | C.SWP_FRAMECHANGED))
659 } $else $if linux {
660 sapp.set_resizable(false)
661 }
662}
663
664fn gg_resize_window(width int, height int) {
665 if width <= 0 || height <= 0 {
666 return
667 }
668 $if macos {
669 window := sapp.macos_get_window()
670 if window != unsafe { nil } {
671 C.gg_macos_resize_window(window, width, height)
672 }
673 } $else $if windows {
674 hwnd := sapp.win32_get_hwnd()
675 if hwnd == unsafe { nil } {
676 return
677 }
678 scale := dpi_scale()
679 client_width := int(f32(width) * scale + 0.5)
680 client_height := int(f32(height) * scale + 0.5)
681 mut rect := C.RECT{
682 right: i32(client_width)
683 bottom: i32(client_height)
684 }
685 style := u32(C.GetWindowLongW(hwnd, C.GWL_STYLE))
686 ex_style := u32(C.GetWindowLongW(hwnd, C.GWL_EXSTYLE))
687 if C.AdjustWindowRectEx(&rect, style, 0, ex_style) == 0 {
688 return
689 }
690 mut current_rect := C.RECT{}
691 if C.GetWindowRect(hwnd, ¤t_rect) == 0 {
692 return
693 }
694 C.SetWindowPos(hwnd, unsafe { nil }, int(current_rect.left), int(current_rect.top),
695 int(rect.right - rect.left), int(rect.bottom - rect.top),
696 u32(C.SWP_NOZORDER | C.SWP_NOACTIVATE))
697 } $else $if linux && !sokol_wayland ? {
698 display := sapp.x11_get_display()
699 window := u64(usize(sapp.x11_get_window()))
700 if display == unsafe { nil } || window == 0 {
701 return
702 }
703 scale := dpi_scale()
704 x11_width := int(f32(width) * scale + 0.5)
705 x11_height := int(f32(height) * scale + 0.5)
706 C.XResizeWindow(display, window, u32(x11_width), u32(x11_height))
707 C.XFlush(display)
708 }
709}
710
711// run starts the main loop of the context.
712pub fn (mut ctx Context) run() {
713 // set context late, in case it changed (e.g., due to embedding)
714 ctx.window = sapp.Desc{
715 ...ctx.window
716 user_data: ctx
717 }
718 if ctx.user_data == unsafe { nil } {
719 ctx.user_data = ctx
720 }
721 sapp.run(&ctx.window)
722}
723
724// quit closes the context window and exits the event loop for it
725pub fn (ctx &Context) quit() {
726 sapp.request_quit() // does not require ctx right now, but sokol multi-window might in the future
727}
728
729// set_bg_color sets the color of the window background to `c`.
730pub fn (mut ctx Context) set_bg_color(c Color) {
731 ctx.bg_color = c
732 ctx.clear_pass = gfx.create_clear_pass_action(f32(c.r) / 255.0, f32(c.g) / 255.0,
733 f32(c.b) / 255.0, f32(c.a) / 255.0)
734 ctx.translucent_bg_seed_pending = c.a < 255
735}
736
737// Resize the context's Window
738pub fn (mut ctx Context) resize(width int, height int) {
739 ctx.set_cached_window_size(width, height)
740 gg_resize_window(width, height)
741}
742
743// refresh_ui requests a complete re-draw of the window contents.
744pub fn (mut ctx Context) refresh_ui() {
745 ctx.needs_refresh = true
746 ctx.ticks = 0
747}
748
749// begin prepares the context for drawing.
750pub fn (ctx &Context) begin() {
751 if ctx.render_text && ctx.font_inited {
752 ctx.ft.flush()
753 }
754 sgl.defaults()
755 sgl.matrix_mode_projection()
756 sgl.ortho(0.0, f32(sapp.width()), f32(sapp.height()), 0.0, -1.0, 1.0)
757 if ctx.bg_color.a < 255 && ctx.last_bg_overlay_frame != ctx.frame {
758 mut ctx_mut := unsafe { &Context(ctx) }
759 ctx_mut.last_bg_overlay_frame = ctx.frame
760 // Sokol clear actions replace pixels and do not blend.
761 // Draw a translucent fullscreen quad once per frame to get trail/fade behavior.
762 ctx.draw_rect_filled(0, 0, f32(ctx.width), f32(ctx.height), ctx.bg_color)
763 }
764}
765
766pub enum EndEnum {
767 clear
768 passthru
769}
770
771@[params]
772pub struct EndOptions {
773pub:
774 how EndEnum
775}
776
777const dontcare_pass = gfx.PassAction{
778 colors: [
779 gfx.ColorAttachmentAction{
780 load_action: .dontcare
781 clear_value: gfx.Color{1.0, 1.0, 1.0, 1.0}
782 },
783 gfx.ColorAttachmentAction{
784 load_action: .dontcare
785 clear_value: gfx.Color{1.0, 1.0, 1.0, 1.0}
786 },
787 gfx.ColorAttachmentAction{
788 load_action: .dontcare
789 clear_value: gfx.Color{1.0, 1.0, 1.0, 1.0}
790 },
791 gfx.ColorAttachmentAction{
792 load_action: .dontcare
793 clear_value: gfx.Color{1.0, 1.0, 1.0, 1.0}
794 },
795 ]!
796}
797
798const load_pass = gfx.PassAction{
799 colors: [
800 gfx.ColorAttachmentAction{
801 load_action: .load
802 clear_value: gfx.Color{1.0, 1.0, 1.0, 1.0}
803 },
804 gfx.ColorAttachmentAction{
805 load_action: .load
806 clear_value: gfx.Color{1.0, 1.0, 1.0, 1.0}
807 },
808 gfx.ColorAttachmentAction{
809 load_action: .load
810 clear_value: gfx.Color{1.0, 1.0, 1.0, 1.0}
811 },
812 gfx.ColorAttachmentAction{
813 load_action: .load
814 clear_value: gfx.Color{1.0, 1.0, 1.0, 1.0}
815 },
816 ]!
817}
818
819pub fn create_default_pass(action gfx.PassAction) gfx.Pass {
820 return sapp.create_default_pass(action)
821}
822
823// end finishes all the drawing for the context ctx.
824// All accumulated draw calls before ctx.end(), will be done in a separate Sokol pass.
825//
826// Note: each Sokol pass, has a limit on the number of draw calls, that can be done in it.
827// Once that limit is reached, the whole pass will not draw anything, which can be frustrating.
828//
829// To overcome this limitation, you may use *several passes*, when you want to make thousands
830// of draw calls (for example, if you need to draw thousands of circles/rectangles/sprites etc),
831// where each pass will render just a limited amount of primitives.
832//
833// In the context of the gg module (without dropping to using sgl and gfx directly), it means,
834// that you will need a new pair of ctx.begin() and ctx.end() calls, surrounding all the draw
835// calls, that should be done in each pass.
836//
837// The default ctx.end() is equivalent to ctx.end(how:.clear). It will erase the existing
838// rendered content with the background color, before drawing anything else.
839// You can call ctx.end(how:.passthru) for a pass, that *will not* erase the previously
840// rendered content in the context.
841pub fn (ctx &Context) end(options EndOptions) {
842 $if show_fps ? {
843 ctx.show_fps()
844 } $else {
845 if ctx.fps.show {
846 ctx.show_fps()
847 }
848 }
849 pass := match options.how {
850 .clear {
851 if ctx.bg_color.a < 255 {
852 if ctx.translucent_bg_seed_pending {
853 mut ctx_mut := unsafe { &Context(ctx) }
854 ctx_mut.translucent_bg_seed_pending = false
855 create_default_pass(ctx.clear_pass)
856 } else {
857 create_default_pass(load_pass)
858 }
859 } else {
860 create_default_pass(ctx.clear_pass)
861 }
862 }
863 .passthru {
864 create_default_pass(dontcare_pass)
865 }
866 }
867
868 gfx.begin_pass(pass)
869 sgl.draw()
870 gfx.end_pass()
871 gfx.commit()
872 /*
873 if gg.config.wait_events {
874 // println('gg: waiting')
875 wait_events()
876 }
877 */
878}
879
880pub struct FPSConfig {
881pub mut:
882 x int // horizontal position on screen
883 y int // vertical position on screen
884 width int // minimum width
885 height int // minimum height
886 show bool // do not show by default, use `-d show_fps` or set it manually in your app to override with: `app.gg.fps.show = true`
887 text_config TextCfg = TextCfg{
888 color: yellow
889 size: 20
890 align: .center
891 vertical_align: .middle
892 }
893 background_color Color = Color{
894 r: 0
895 g: 0
896 b: 0
897 a: 128
898 }
899}
900
901pub fn (ctx &Context) show_fps() {
902 if !ctx.font_inited {
903 return
904 }
905 frame_duration := sapp.frame_duration()
906 sgl.defaults()
907 sgl.matrix_mode_projection()
908 sgl.ortho(0.0, f32(sapp.width()), f32(sapp.height()), 0.0, -1.0, 1.0)
909 ctx.set_text_cfg(ctx.fps.text_config)
910 fps_text := int(0.5 + 1.0 / frame_duration).str()
911 if ctx.fps.width == 0 {
912 mut fps := unsafe { &ctx.fps }
913 fps.width, fps.height =
914 ctx.text_size('00') // usual size; prevents blinking on variable width fonts
915 }
916 char_width := ctx.fps.text_config.size / 2
917 mut full_width := ctx.fps.width
918 if char_width * fps_text.len > ctx.fps.width {
919 full_width += (fps_text.len - 2) * char_width
920 }
921 ctx.draw_rect_filled(ctx.fps.x, ctx.fps.y, full_width + 2, ctx.fps.height + 4,
922 ctx.fps.background_color)
923 ctx.draw_text(ctx.fps.x + full_width / 2 + 1, ctx.fps.y + ctx.fps.height / 2 + 2, fps_text,
924 ctx.fps.text_config)
925}
926
927fn (mut ctx Context) set_scale() {
928 mut s := sapp.dpi_scale()
929 $if android {
930 w := ctx.config.width
931 h := ctx.config.height
932 dw := sapp.width()
933 dh := sapp.height()
934 if dw <= dh {
935 if w <= 0 {
936 s = 1.0
937 } else {
938 s = f32(dw) / w
939 }
940 } else {
941 if h <= 0 {
942 s = 1.0
943 } else {
944 s = f32(dh) / h
945 }
946 }
947 }
948 // Note: on older X11, `Xft.dpi` from ~/.Xresources, that sokol uses,
949 // may not be set which leads to sapp.dpi_scale reporting incorrectly 0.0
950 if s < 0.1 {
951 s = 1.0
952 }
953 ctx.scale = s
954}
955
956// window_size returns the current dimensions of the window.
957pub fn (ctx Context) window_size() Size {
958 s := ctx.scale
959 return Size{int(sapp.width() / s), int(sapp.height() / s)}
960}
961
962//---- public module functions
963
964// dpi_scale returns the DPI scale coefficient for the screen.
965// Do not use for Android development, use `Context.scale` instead.
966pub fn dpi_scale() f32 {
967 mut s := sapp.dpi_scale()
968 $if android {
969 s *= android_dpi_scale()
970 }
971 // Note: on older X11, `Xft.dpi` from ~/.Xresources, that sokol uses,
972 // may not be set which leads to sapp.dpi_scale reporting incorrectly 0.0
973 if s < 0.1 {
974 s = 1.0
975 }
976 return s
977}
978
979// high_dpi returns true if `gg` is running on a high DPI monitor or screen.
980pub fn high_dpi() bool {
981 return C.sapp_high_dpi()
982}
983
984// screen_size returns the size of the active screen.
985pub fn screen_size() Size {
986 $if macos {
987 return C.gg_get_screen_size()
988 }
989 $if windows {
990 return Size{
991 width: int(C.GetSystemMetrics(C.SM_CXSCREEN))
992 height: int(C.GetSystemMetrics(C.SM_CYSCREEN))
993 }
994 }
995 $if linux {
996 display := C.XOpenDisplay(0)
997 if display == unsafe { nil } {
998 return Size{}
999 }
1000 defer { C.XCloseDisplay(display) }
1001 root := C.DefaultRootWindow(display)
1002 resources := C.XRRGetScreenResources(display, root)
1003 if resources == unsafe { nil } {
1004 return Size{}
1005 }
1006 defer { C.XRRFreeScreenResources(resources) }
1007 primary_output := C.XRRGetOutputPrimary(display, root)
1008 if primary_output == 0 {
1009 return Size{}
1010 }
1011 for i := 0; i < resources.noutput; i++ {
1012 if unsafe { u64(resources.outputs[i]) } == primary_output {
1013 output_info := C.XRRGetOutputInfo(display, resources,
1014 unsafe { resources.outputs[i] })
1015 if output_info == unsafe { nil } {
1016 return Size{}
1017 }
1018 crtc_info := C.XRRGetCrtcInfo(display, resources, output_info.crtc)
1019 C.XRRFreeOutputInfo(output_info)
1020 if crtc_info == unsafe { nil } {
1021 return Size{}
1022 }
1023 res := Size{
1024 width: unsafe { int(crtc_info.width) }
1025 height: unsafe { int(crtc_info.height) }
1026 }
1027 C.XRRFreeCrtcInfo(crtc_info)
1028 return res
1029 }
1030 }
1031 }
1032 return Size{}
1033}
1034
1035// window_size returns the `Size` of the active window.
1036// Do not use for Android development, use `Context.window_size()` instead.
1037pub fn window_size() Size {
1038 s := dpi_scale()
1039 return Size{int(sapp.width() / s), int(sapp.height() / s)}
1040}
1041
1042// set_window_title sets main window's title
1043pub fn set_window_title(title string) {
1044 C.sapp_set_window_title(&char(title.str))
1045}
1046
1047// window_size_real_pixels returns the `Size` of the active window without scale
1048pub fn window_size_real_pixels() Size {
1049 return Size{sapp.width(), sapp.height()}
1050}
1051
1052// is it fullscreen
1053pub fn is_fullscreen() bool {
1054 return sapp.is_fullscreen()
1055}
1056
1057// toggle fullscreen
1058pub fn toggle_fullscreen() {
1059 sapp.toggle_fullscreen()
1060}
1061
1062/*
1063pub fn wait_events() {
1064 unsafe {
1065 $if macos {
1066 #NSEvent *event = [NSApp nextEventMatchingMask:NSEventMaskAny
1067 #untilDate:[NSDate distantFuture]
1068 #inMode:NSDefaultRunLoopMode
1069 #dequeue:YES];
1070 #[NSApp sendEvent:event];
1071 }
1072 $if windows {
1073 C.WaitMessage()
1074 }
1075 }
1076}
1077*/
1078
1079// memory_trace_frame creates a small allocation at the start of each frame,
1080// that is easy to search for in memdump.bin files, created with:
1081// -prealloc -d prealloc_memset -d prealloc_memset_value=65 -d prealloc_dump -d gg_memory_trace_frame
1082@[if gg_memory_trace_frame ?; manualfree]
1083fn (mut ctx Context) memory_trace_frame() {
1084 frame_tag_size := 61 // uneven, and easy to spot in heaptrack histograms as well
1085 unsafe {
1086 frame_tag := &u8(vcalloc(frame_tag_size))
1087 C.snprintf(frame_tag, frame_tag_size, c'@@ gg_memory_trace_frame: %06d ', ctx.frame)
1088 frame_tag[frame_tag_size - 2] = `@`
1089 frame_tag[frame_tag_size - 1] = `@`
1090 free(frame_tag)
1091 }
1092}
1093