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