Initial project import

This commit is contained in:
Agila Forge Importer committed 2026-07-13 15:20:32 +00:00
commit 23f2d29e32
11957 files changed
+1754577

No files matched your search

+63
View File
@@ -0,0 +1,63 @@
## Description
`gg` is V's simple graphics module.
It is currently implemented using `sokol`, and makes easy creating
apps that just need a way to draw simple 2D shapes, and to react to
user's keyboard/mouse input.
## Example
```v cgen
module main
import gg
fn main() {
mut context := gg.new_context(
bg_color: gg.rgb(174, 198, 255)
width: 600
height: 400
window_title: 'Polygons'
frame_fn: frame
)
context.run()
}
fn frame(mut ctx gg.Context) {
ctx.begin()
ctx.draw_convex_poly([f32(100.0), 100.0, 200.0, 100.0, 300.0, 200.0, 200.0, 300.0, 100.0, 300.0],
gg.blue)
ctx.draw_poly_empty([f32(50.0), 50.0, 70.0, 60.0, 90.0, 80.0, 70.0, 110.0], gg.black)
ctx.draw_triangle_filled(450, 142, 530, 280, 370, 280, gg.red)
ctx.end()
}
```
## Troubleshooting
A common problem, if you draw a lot of primitive elements in the same
frame, is that there is a chance that your program can exceed the maximum
allowed amount of vertices and commands, imposed by `sokol`.
The symptom is that your frame will be suddenly black, after it becomes more complex.
Sokol's default for vertices is 131072.
Sokol's default for commands is 32768.
To solve that, you can try adding these lines at the top of your program:
`#flag -D_SGL_DEFAULT_MAX_VERTICES=4194304`
`#flag -D_SGL_DEFAULT_MAX_COMMANDS=65536`
You can see an example of that in:
https://github.com/vlang/v/blob/master/examples/gg/many_thousands_of_circles_overriding_max_vertices.v
Another approach is to use several draw passes, and limit the amount
of draw calls that you make in each, demonstrated in:
https://github.com/vlang/v/blob/master/examples/gg/many_thousands_of_circles.v
Another approach to that problem, is to draw everything yourself in a streaming
texture, then upload that streaming texture as a single draw command to the GPU.
You can see an example of that done in:
https://github.com/vlang/v/blob/master/examples/gg/random.v
and in:
https://github.com/vlang/v/blob/master/examples/gg/random_stars.v
A third approach, is to only upload your changing inputs to the GPU, and do all
the calculations and drawing there in shaders.
+57
View File
@@ -0,0 +1,57 @@
module gg
import math
import sokol.sgl
// draw_cubic_bezier_recursive draws a cubic Bézier curve, also known as a spline, from four points,
// where the first and the last points, *will* be part of the curve, and the middle 2 points are control ones.
// Unlike `draw_cubic_bezier_in_steps`, this method does not use a fixed number of steps for the whole curve,
// but tries to produce more tesselation points dynamically for the curvier parts.
@[direct_array_access]
pub fn (ctx &Context) draw_cubic_bezier_recursive(points []f32, c Color) {
if points.len < 8 {
return
}
ctx.draw_cubic_bezier_recursive_scalar(points[0], points[1], points[2], points[3], points[4],
points[5], points[6], points[7], c)
}
// draw_cubic_bezier_recursive_scalar is the same as `draw_cubic_bezier_recursive`, except that the `points` are given
// as indiviual x,y f32 scalar parameters, and not in a single dynamic array parameter.
pub fn (ctx &Context) draw_cubic_bezier_recursive_scalar(x1 f32, y1 f32, x2 f32, y2 f32, x3 f32, y3 f32,
x4 f32, y4 f32, c Color) {
if c.a == 0 {
return
}
if c.a != 255 {
sgl.load_pipeline(ctx.pipeline.alpha)
}
sgl.c4b(c.r, c.g, c.b, c.a)
sgl.begin_line_strip()
sgl.v2f(x1 * ctx.scale, y1 * ctx.scale)
ctx.cubic_bezier_rec(x1, y1, x2, y2, x3, y3, x4, y4, 0)
sgl.v2f(x4 * ctx.scale, y4 * ctx.scale)
sgl.end()
}
// based on nsvg__flattenCubicBez, from https://github.com/memononen/nanosvg/ :
fn (ctx &Context) cubic_bezier_rec(x1 f32, y1 f32, x2 f32, y2 f32, x3 f32, y3 f32, x4 f32, y4 f32, level int) {
if level > 10 {
return
}
dx, dy := x4 - x1, y4 - y1
d2 := math.abs((x2 - x4) * dy - (y2 - y4) * dx)
d3 := math.abs((x3 - x4) * dy - (y3 - y4) * dx)
if (d2 + d3) * (d2 + d3) < 0.25 * (dx * dx + dy * dy) {
sgl.v2f(x4 * ctx.scale, y4 * ctx.scale)
return
}
x12, y12 := 0.5 * (x1 + x2), 0.5 * (y1 + y2)
x23, y23 := 0.5 * (x2 + x3), 0.5 * (y2 + y3)
x34, y34 := 0.5 * (x3 + x4), 0.5 * (y3 + y4)
x234, y234 := 0.5 * (x23 + x34), 0.5 * (y23 + y34)
x123, y123 := 0.5 * (x12 + x23), 0.5 * (y12 + y23)
x1234, y1234 := 0.5 * (x123 + x234), 0.5 * (y123 + y234)
ctx.cubic_bezier_rec(x1, y1, x12, y12, x123, y123, x1234, y1234, level + 1)
ctx.cubic_bezier_rec(x1234, y1234, x234, y234, x34, y34, x4, y4, level + 1)
}
+330
View File
@@ -0,0 +1,330 @@
module gg
pub const black = Color{
r: 0
g: 0
b: 0
}
pub const gray = Color{
r: 128
g: 128
b: 128
}
pub const white = Color{
r: 255
g: 255
b: 255
}
pub const red = Color{
r: 255
g: 0
b: 0
}
pub const green = Color{
r: 0
g: 255
b: 0
}
pub const blue = Color{
r: 0
g: 0
b: 255
}
pub const yellow = Color{
r: 255
g: 255
b: 0
}
pub const magenta = Color{
r: 255
g: 0
b: 255
}
pub const cyan = Color{
r: 0
g: 255
b: 255
}
pub const orange = Color{
r: 255
g: 165
b: 0
}
pub const purple = Color{
r: 128
g: 0
b: 128
}
pub const indigo = Color{
r: 75
g: 0
b: 130
}
pub const pink = Color{
r: 255
g: 192
b: 203
}
pub const violet = Color{
r: 238
g: 130
b: 238
}
pub const dark_blue = Color{
r: 0
g: 0
b: 139
}
pub const dark_gray = Color{
r: 169
g: 169
b: 169
}
pub const dark_green = Color{
r: 0
g: 100
b: 0
}
pub const dark_red = Color{
r: 139
g: 0
b: 0
}
pub const light_blue = Color{
r: 173
g: 216
b: 230
}
pub const light_gray = Color{
r: 211
g: 211
b: 211
}
pub const light_green = Color{
r: 144
g: 238
b: 144
}
pub const light_red = Color{
r: 255
g: 204
b: 203
}
// Color represents a 32 bit color value in sRGB format
@[markused]
pub struct Color {
pub mut:
r u8
g u8
b u8
a u8 = 255
}
// hex takes in a 32 bit integer and splits it into 4 byte values
pub fn hex(color u32) Color {
return Color{
r: u8((color >> 16) & 0xFF)
g: u8((color >> 8) & 0xFF)
b: u8((color >> 0) & 0xFF)
}
}
// frgb builds a Color instance from the given floating point values (between 0.0 and 1.0) r, g, b
@[inline]
pub fn frgb[T](r T, g T, b T) Color {
return frgba(r, g, b, 1.0)
}
// frgba builds a Color instance from the given floating point values (between 0.0 and 1.0) r, g, b, a
@[inline]
pub fn frgba[T](r T, g T, b T, a T) Color {
return Color{
r: u8(r * 255.0)
g: u8(g * 255.0)
b: u8(b * 255.0)
a: u8(a * 255.0)
}
}
// rgb builds a Color instance from the given r, g, b u8 values
@[inline]
pub fn rgb(r u8, g u8, b u8) Color {
return Color{
r: r
g: g
b: b
}
}
// rgba builds a Color instance from the given r, g, b, a u8 values
@[inline]
pub fn rgba(r u8, g u8, b u8, a u8) Color {
return Color{
r: r
g: g
b: b
a: a
}
}
// + adds `b` to `a`, with a maximum value of 255 for each channel
pub fn (a Color) + (b Color) Color {
mut na := i32(a.a) + b.a
mut nr := i32(a.r) + b.r
mut ng := i32(a.g) + b.g
mut nb := i32(a.b) + b.b
if na > 255 {
na = 255
}
if nr > 255 {
nr = 255
}
if ng > 255 {
ng = 255
}
if nb > 255 {
nb = 255
}
return Color{
r: u8(nr)
g: u8(ng)
b: u8(nb)
a: u8(na)
}
}
// - subtracts `b` from `a`, with a minimum value of 0 for each channel
// the alpha channel will be set as the minimum between ``a.a`` and ``b.a``
pub fn (a Color) - (b Color) Color {
mut na := if a.a > b.a { a.a } else { b.a }
mut nr := i32(a.r) - b.r
mut ng := i32(a.g) - b.g
mut nb := i32(a.b) - b.b
if na < 0 {
na = 0
}
if nr < 0 {
nr = 0
}
if ng < 0 {
ng = 0
}
if nb < 0 {
nb = 0
}
return Color{
r: u8(nr)
g: u8(ng)
b: u8(nb)
a: u8(na)
}
}
// * multiplies Color `c` and `c2` keeping channel values in [0, 255] range
pub fn (c Color) * (c2 Color) Color {
return Color{
r: c.r * c2.r
g: c.g * c2.g
b: c.b * c2.b
a: c.a * c2.a
}
}
// / divides `c` by `c2` and converts each channel's value to u8(int)
pub fn (c Color) / (c2 Color) Color {
return Color{
r: c.r / c2.r
g: c.g / c2.g
b: c.b / c2.b
a: c.a / c2.a
}
}
// over implements an `a` over `b` operation.
// see https://keithp.com/~keithp/porterduff/p253-porter.pdf
pub fn (a Color) over(b Color) Color {
aa := f32(a.a) / 255
ab := f32(b.a) / 255
ar := aa + ab * (1 - aa)
rr := (f32(a.r) * aa + f32(b.r) * ab * (1 - aa)) / ar
gr := (f32(a.g) * aa + f32(b.g) * ab * (1 - aa)) / ar
br := (f32(a.b) * aa + f32(b.b) * ab * (1 - aa)) / ar
return Color{
r: u8(rr)
g: u8(gr)
b: u8(br)
a: u8(ar * 255)
}
}
// eq checks if color `c` and `c2` are equal in every channel
pub fn (c Color) eq(c2 Color) bool {
return c.r == c2.r && c.g == c2.g && c.b == c2.b && c.a == c2.a
}
// str returns a string representation of the Color `c`
pub fn (c Color) str() string {
return 'Color{${c.r}, ${c.g}, ${c.b}, ${c.a}}'
}
// rgba8 converts a color value to an 32bit int in the RGBA8 order.
// see https://developer.apple.com/documentation/coreimage/ciformat
@[inline]
pub fn (c Color) rgba8() i32 {
return i32(u32(c.r) << 24 | u32(c.g) << 16 | u32(c.b) << 8 | u32(c.a))
}
// bgra8 converts a color value to an 32bit int in the BGRA8 order.
// see https://developer.apple.com/documentation/coreimage/ciformat
@[inline]
pub fn (c Color) bgra8() i32 {
return i32(u32(c.b) << 24 | u32(c.g) << 16 | u32(c.r) << 8 | u32(c.a))
}
// abgr8 converts a color value to an 32bit int in the ABGR8 order.
// see https://developer.apple.com/documentation/coreimage/ciformat
@[inline]
pub fn (c Color) abgr8() i32 {
return i32(u32(c.a) << 24 | u32(c.b) << 16 | u32(c.g) << 8 | u32(c.r))
}
const string_colors = {
'blue': blue
'red': red
'green': green
'yellow': yellow
'orange': orange
'purple': purple
'black': black
'gray': gray
'indigo': indigo
'pink': pink
'violet': violet
'white': white
'dark_blue': dark_blue
'dark_gray': dark_gray
'dark_green': dark_green
'dark_red': dark_red
'light_blue': light_blue
'light_gray': light_gray
'light_green': light_green
'light_red': light_red
}
// color_from_string returns a Color, corresponding to the given string
// or black Color if string is not found in lookup table, or a hex color if starting with #
pub fn color_from_string(s string) Color {
if s.starts_with('#') {
mut hex_str := '0x' + s[1..]
return hex(hex_str.u32())
} else {
return string_colors[s]
}
}
// to_css_string returns a CSS compatible string e.g. `rgba(10,11,12,13)` of the color `c`.
pub fn (c Color) to_css_string() string {
return 'rgba(${c.r},${c.g},${c.b},${c.a})'
}
+102
View File
@@ -0,0 +1,102 @@
// vtest build: !musl? // gx now transitively imports sokol, and that needs GL/gl.h, which is not installed on the musl CIs
import gg
fn test_hex() {
// valid colors
// a := gg.hex(0x6c5ce7ff)
// b := gg.rgba(108, 92, 231, 255)
assert gg.hex(0xff6600) == gg.rgb(255, 102, 0) // orange
// doesn't give right value with short hex value
short := gg.hex(0xfff)
assert short != gg.white
// assert short == gg.Color{0, 0, 15, 255}
}
fn test_add() {
a := gg.rgba(100, 100, 100, 100)
b := gg.rgba(100, 100, 100, 100)
r := gg.rgba(200, 200, 200, 200)
assert (a + b) == r
assert gg.red + gg.green == gg.yellow
assert gg.red + gg.blue == gg.magenta
assert gg.green + gg.blue == gg.cyan
}
fn test_sub() {
a := gg.rgba(100, 100, 100, 50)
b := gg.rgba(100, 100, 100, 100)
r := gg.rgba(0, 0, 0, 100)
assert (a - b) == r
assert gg.white - gg.green == gg.magenta
assert gg.white - gg.blue == gg.yellow
assert gg.white - gg.red == gg.cyan
}
fn test_mult() {
a := gg.rgba(10, 10, 10, 10)
b := gg.rgba(10, 10, 10, 10)
r := gg.rgba(100, 100, 100, 100)
assert (a * b) == r
}
fn test_div() {
a := gg.rgba(100, 100, 100, 100)
b := gg.rgba(10, 10, 10, 10)
r := gg.rgba(10, 10, 10, 10)
assert (a / b) == r
}
fn test_rgba8() {
assert gg.white.rgba8() == -1
assert gg.black.rgba8() == 255
assert gg.red.rgba8() == -16776961
assert gg.green.rgba8() == 16711935
assert gg.blue.rgba8() == 65535
}
fn test_bgra8() {
assert gg.white.bgra8() == -1
assert gg.black.bgra8() == 255
assert gg.red.bgra8() == 65535
assert gg.green.bgra8() == 16711935
assert gg.blue.bgra8() == -16776961
}
fn test_abgr8() {
assert gg.white.abgr8() == -1
assert gg.black.abgr8() == -16777216
assert gg.red.abgr8() == -16776961
assert gg.green.abgr8() == -16711936
assert gg.blue.abgr8() == -65536
}
fn test_over() {
// shorten names:
r := gg.red
g := gg.green
b := gg.blue
y := gg.yellow
semi_r := gg.Color{255, 0, 0, 128}
semi_g := gg.Color{0, 255, 0, 128}
semi_b := gg.Color{0, 0, 255, 128}
// fully opaque colors, should be preserved when laid *over* any others:
assert b.over(g) == b
assert r.over(g) == r
assert y.over(r) == y
assert g.over(r) == g
// half transparent pure colors, *over* pure colors, should preserve them correspondingly:
assert semi_r.over(r) == gg.Color{255, 0, 0, 255} // preserve pure red
assert semi_r.over(g) == gg.Color{128, 126, 0, 255}
assert semi_r.over(b) == gg.Color{128, 0, 126, 255}
assert semi_g.over(r) == gg.Color{126, 128, 0, 255}
assert semi_g.over(g) == gg.Color{0, 255, 0, 255} // preserve pure green
assert semi_g.over(b) == gg.Color{0, 128, 126, 255}
assert semi_b.over(r) == gg.Color{126, 0, 128, 255}
assert semi_b.over(g) == gg.Color{0, 126, 128, 255}
assert semi_b.over(b) == gg.Color{0, 0, 255, 255} // preserve pure blue
}
File diff suppressed because it is too large. Load diff
+26
View File
@@ -0,0 +1,26 @@
// vtest build: !musl? && !sanitized_job?
import os
fn test_all_samples_can_be_compiled() {
vexe := @VEXE
vroot := os.dir(vexe)
samples := os.walk_ext('${vroot}/vlib/gg/testdata', '.vv')
mut fails := []string{}
for program_source in samples {
compile_cmd := '${os.quoted_path(vexe)} ${os.quoted_path(program_source)}'
res := os.execute(compile_cmd)
if res.exit_code != 0 {
eprintln('>>> FAIL ${compile_cmd}')
eprintln(res.output)
fails << compile_cmd
}
println('OK ${compile_cmd}')
}
if fails.len > 0 {
eprintln('> Failed summary:')
for f in fails {
eprintln(' failed cmd: ${f}')
}
assert false
}
}
+25
View File
@@ -0,0 +1,25 @@
module gg
import math
const rect_empty_bounds_tolerance = f32(0.001)
fn rect_empty_bounds_match(a f32, b f32) bool {
return math.abs(a - b) <= rect_empty_bounds_tolerance
}
fn test_rect_empty_screen_bounds_keep_the_top_left_corner_aligned() {
tleft_x, tleft_y, bright_x, bright_y := rect_empty_screen_bounds(1.0, 12, 34, 56, 78)
assert rect_empty_bounds_match(tleft_x, 12.1)
assert rect_empty_bounds_match(tleft_y, 34.1)
assert rect_empty_bounds_match(bright_x, 67.9)
assert rect_empty_bounds_match(bright_y, 111.9)
}
fn test_rect_empty_screen_bounds_apply_scale_before_the_pixel_offsets() {
tleft_x, tleft_y, bright_x, bright_y := rect_empty_screen_bounds(2.0, 3, 4, 5, 6)
assert rect_empty_bounds_match(tleft_x, 6.1)
assert rect_empty_bounds_match(tleft_y, 8.1)
assert rect_empty_bounds_match(bright_x, 15.9)
assert rect_empty_bounds_match(bright_y, 19.9)
}
+161
View File
@@ -0,0 +1,161 @@
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license that can be found in the LICENSE file.
module gg
pub enum MouseButton {
left = 0
right = 1
middle = 2
invalid = 256
}
// Note: unlike the MouseButton enum from above,
// the [flag]-ed enum here can have combined states,
// representing several pressed buttons at once.
@[flag]
pub enum MouseButtons {
left
right
middle
}
@[flag]
pub enum Modifier {
shift // (1<<0)
ctrl // (1<<1)
alt // (1<<2)
super // (1<<3)
}
pub enum PenLineType {
solid
dashed
dotted
}
pub enum KeyCode {
invalid = 0
space = 32
apostrophe = 39 //'
comma = 44 //,
minus = 45 //-
period = 46 //.
slash = 47 ///
_0 = 48
_1 = 49
_2 = 50
_3 = 51
_4 = 52
_5 = 53
_6 = 54
_7 = 55
_8 = 56
_9 = 57
semicolon = 59 //;
equal = 61 //=
a = 65
b = 66
c = 67
d = 68
e = 69
f = 70
g = 71
h = 72
i = 73
j = 74
k = 75
l = 76
m = 77
n = 78
o = 79
p = 80
q = 81
r = 82
s = 83
t = 84
u = 85
v = 86
w = 87
x = 88
y = 89
z = 90
left_bracket = 91 //[
backslash = 92 //\
right_bracket = 93 //]
grave_accent = 96 //`
world_1 = 161 // non-us #1
world_2 = 162 // non-us #2
escape = 256
enter = 257
tab = 258
backspace = 259
insert = 260
delete = 261
right = 262
left = 263
down = 264
up = 265
page_up = 266
page_down = 267
home = 268
end = 269
caps_lock = 280
scroll_lock = 281
num_lock = 282
print_screen = 283
pause = 284
f1 = 290
f2 = 291
f3 = 292
f4 = 293
f5 = 294
f6 = 295
f7 = 296
f8 = 297
f9 = 298
f10 = 299
f11 = 300
f12 = 301
f13 = 302
f14 = 303
f15 = 304
f16 = 305
f17 = 306
f18 = 307
f19 = 308
f20 = 309
f21 = 310
f22 = 311
f23 = 312
f24 = 313
f25 = 314
kp_0 = 320
kp_1 = 321
kp_2 = 322
kp_3 = 323
kp_4 = 324
kp_5 = 325
kp_6 = 326
kp_7 = 327
kp_8 = 328
kp_9 = 329
kp_decimal = 330
kp_divide = 331
kp_multiply = 332
kp_subtract = 333
kp_add = 334
kp_enter = 335
kp_equal = 336
left_shift = 340
left_control = 341
left_alt = 342
left_super = 343
right_shift = 344
right_control = 345
right_alt = 346
right_super = 347
menu = 348
}
const key_code_max = 512
+52
View File
@@ -0,0 +1,52 @@
// vtest build: !docker-ubuntu-musl // needs GL/gl.h
module gg
import encoding.base64
import time
fn test_swap_interval_frame_budget() {
assert swap_interval_frame_budget(-1) == time.Duration(0)
assert swap_interval_frame_budget(0) == time.Duration(0)
assert swap_interval_frame_budget(1) == time.second / 60
assert swap_interval_frame_budget(2) == time.second / 30
assert swap_interval_frame_budget(3) == time.second / 20
}
fn test_parse_screenshot_frames_ignores_blank_entries() {
assert parse_screenshot_frames('') == []u64{}
assert parse_screenshot_frames('1, 2,,4 ') == [u64(1), 2, 4]
}
fn test_new_gg_recorder_settings_stdout_defaults_to_first_frame() {
settings := new_gg_recorder_settings_from_env({
'VGG_SCREENSHOT_OUTPUT': 'stdout'
}, '/tmp/minimal_demo')
assert settings.screenshot_output == .stdout
assert settings.screenshot_frames == [u64(1)]
assert settings.stop_at_frame == 1
}
fn test_new_gg_recorder_settings_stdout_uses_last_frame_as_default_stop() {
settings := new_gg_recorder_settings_from_env({
'VGG_SCREENSHOT_OUTPUT': 'stdout'
'VGG_SCREENSHOT_FRAMES': '2, 4'
}, '/tmp/minimal_demo')
assert settings.screenshot_frames == [u64(2), 4]
assert settings.stop_at_frame == 4
}
fn test_new_gg_recorder_settings_preserves_explicit_stop_frame() {
settings := new_gg_recorder_settings_from_env({
'VGG_SCREENSHOT_OUTPUT': 'stdout'
'VGG_SCREENSHOT_FRAMES': '3'
'VGG_STOP_AT_FRAME': '9'
}, '/tmp/minimal_demo')
assert settings.stop_at_frame == 9
}
fn test_screenshot_stdout_payload_contains_expected_marker() {
png := [u8(0x89), `P`, `N`, `G`]
encoded := base64.encode(png)
payload := screenshot_stdout_payload(7, png)
assert payload == '${gg_record_stdout_prefix} frame=7 format=png encoding=base64 data=${encoded}'
}
File diff suppressed because it is too large. Load diff
+768
View File
@@ -0,0 +1,768 @@
module gg
import js.dom
pub enum DOMEventType {
invalid
key_down
key_up
char
mouse_down
mouse_up
mouse_scroll
mouse_move
mouse_enter
mouse_leave
touches_began
touches_moved
touches_ended
touches_cancelled
resized
iconified
restored
focused
unfocused
suspended
resumed
update_cursor
quit_requested
clipboard_pasted
files_dropped
num
}
pub struct Event {
pub mut:
frame_count u64
typ DOMEventType
key_code KeyCode
char_code u32
key_repeat bool
modifiers u32
mouse_button MouseButton
mouse_x f32
mouse_y f32
mouse_dx f32
mouse_dy f32
scroll_x f32
scroll_y f32
// todo(playX): add touches API support in js.dom
// num_touches int
// touches [8]TouchPoint
window_width int
window_height int
framebuffer_width int
framebuffer_height int
}
pub enum DOMMouseButton {
invalid = -1
left = 0
right = 1
middle = 2
}
pub enum DOMModifier {
shift = 1 //(1<<0)
ctrl = 2 //(1<<1)
alt = 4 //(1<<2)
super = 8 //(1<<3)
lmb = 0x100
rmb = 0x200
mmb = 0x400
}
pub enum DOMKeyCode {
invalid = 0
space = 32
apostrophe = 39 //'
comma = 44 //,
minus = 45 //-
period = 46 //.
slash = 47 ///
_0 = 48
_1 = 49
_2 = 50
_3 = 51
_4 = 52
_5 = 53
_6 = 54
_7 = 55
_8 = 56
_9 = 57
semicolon = 59 //;
equal = 61 //=
a = 65
b = 66
c = 67
d = 68
e = 69
f = 70
g = 71
h = 72
i = 73
j = 74
k = 75
l = 76
m = 77
n = 78
o = 79
p = 80
q = 81
r = 82
s = 83
t = 84
u = 85
v = 86
w = 87
x = 88
y = 89
z = 90
left_bracket = 91 //[
backslash = 92 //\
right_bracket = 93 //]
grave_accent = 96 //`
world_1 = 161 // non-us #1
world_2 = 162 // non-us #2
escape = 256
enter = 257
tab = 258
backspace = 259
insert = 260
delete = 261
right = 262
left = 263
down = 264
up = 265
page_up = 266
page_down = 267
home = 268
end = 269
caps_lock = 280
scroll_lock = 281
num_lock = 282
print_screen = 283
pause = 284
f1 = 290
f2 = 291
f3 = 292
f4 = 293
f5 = 294
f6 = 295
f7 = 296
f8 = 297
f9 = 298
f10 = 299
f11 = 300
f12 = 301
f13 = 302
f14 = 303
f15 = 304
f16 = 305
f17 = 306
f18 = 307
f19 = 308
f20 = 309
f21 = 310
f22 = 311
f23 = 312
f24 = 313
f25 = 314
kp_0 = 320
kp_1 = 321
kp_2 = 322
kp_3 = 323
kp_4 = 324
kp_5 = 325
kp_6 = 326
kp_7 = 327
kp_8 = 328
kp_9 = 329
kp_decimal = 330
kp_divide = 331
kp_multiply = 332
kp_subtract = 333
kp_add = 334
kp_enter = 335
kp_equal = 336
left_shift = 340
left_control = 341
left_alt = 342
left_super = 343
right_shift = 344
right_control = 345
right_alt = 346
right_super = 347
menu = 348
}
pub struct Config {
pub:
width int
height int
retina bool
resizable bool
user_data voidptr
font_size int
create_window bool
// window_user_ptr voidptr
window_title string
borderless_window bool
always_on_top bool
bg_color Color
init_fn FNCb = unsafe { nil }
frame_fn FNCb = unsafe { nil }
native_frame_fn FNCb = unsafe { nil }
cleanup_fn FNCb = unsafe { nil }
fail_fn FNFail = unsafe { nil }
event_fn FNEvent = unsafe { nil }
quit_fn FNEvent = unsafe { nil }
keydown_fn FNKeyDown = unsafe { nil }
keyup_fn FNKeyUp = unsafe { nil }
char_fn FNChar = unsafe { nil }
move_fn FNMove = unsafe { nil }
click_fn FNClick = unsafe { nil }
unclick_fn FNUnClick = unsafe { nil }
leave_fn FNEvent = unsafe { nil }
enter_fn FNEvent = unsafe { nil }
resized_fn FNEvent = unsafe { nil }
scroll_fn FNEvent = unsafe { nil }
// wait_events bool // set this to true for UIs, to save power
fullscreen bool
scale f32 = 1.0
sample_count int
texture_filter TextureFilter = .linear
swap_interval int = 1 // 1 = 60fps, 2 = 30fps etc. Ignored by the JS backend; frame pacing follows requestAnimationFrame.
// ved needs this
// init_text bool
font_path string
custom_bold_font_path string
ui_mode bool // refreshes only on events to save CPU usage
// font bytes for embedding
font_bytes_normal []u8
font_bytes_bold []u8
font_bytes_mono []u8
font_bytes_italic []u8
native_rendering bool // Cocoa on macOS/iOS, GDI+ on Windows
// drag&drop
enable_dragndrop bool // enable file dropping (drag'n'drop), default is false
max_dropped_files int = 1 // max number of dropped files to process (default: 1)
max_dropped_file_path_length int = 2048 // max length in bytes of a dropped UTF-8 file path (default: 2048)
html5_canvas_name string = 'canvas' // the id/name of the canvas element, that will be used to render GG apps
}
const size = Size{0, 0}
pub fn window_size() Size {
return size
}
pub struct Context {
mut:
render_text bool = true
image_cache []Image
needs_refresh bool = true
ticks int
pub mut:
scale f32 = 1.0
width int
height int
window JS.Window @[noinit]
config Config
user_data voidptr
ui_mode bool
frame u64
mbtn_mask u8
mouse_buttons MouseButtons
mouse_pos_x int
mouse_pos_y int
mouse_dx int
mouse_dy int
scroll_x int
scroll_y int
key_modifiers Modifier // the current key modifiers
key_repeat bool // whether the pressed key was an autorepeated one
pressed_keys [key_code_max]bool // an array representing all currently pressed keys
pressed_keys_edge [key_code_max]bool // true when the previous state of pressed_keys,
context JS.CanvasRenderingContext2D @[noinit]
canvas JS.HTMLCanvasElement @[noinit]
// *before* the current event was different
}
fn get_canvas(elem JS.HTMLElement) JS.HTMLCanvasElement {
match elem {
JS.HTMLCanvasElement {
return elem
}
else {
panic('gg: element is not an HTMLCanvasElement')
}
}
}
fn get_context(canvas JS.HTMLCanvasElement) JS.CanvasRenderingContext2D {
ctx := canvas.getContext('2d'.str, js_undefined()) or { panic('cannot get context') }
match ctx {
JS.CanvasRenderingContext2D {
return ctx
}
else {
panic('failed to get 2D context')
}
}
}
pub fn new_context(cfg Config) &Context {
mut g := &Context{}
g.user_data = cfg.user_data
g.width = cfg.width
g.height = cfg.height
g.ui_mode = cfg.ui_mode
mut sz := size
sz.height = g.height
sz.width = g.width
g.config = cfg
g.window = dom.window()
document := dom.document
canvas_elem := document.getElementById(cfg.html5_canvas_name.str) or {
panic('gg: cannot get canvas element from cfg.html5_canvas_name.str')
}
canvas := get_canvas(canvas_elem)
g.canvas = canvas
g.context = get_context(g.canvas)
mouse_down_event_handler := fn [mut g] (event JS.Event) {
match event {
JS.MouseEvent {
e := g.handle_mouse_event(event, .mouse_down)
if !isnil(g.config.event_fn) {
f := g.config.event_fn
f(e, g.config.user_data)
}
if !isnil(g.config.click_fn) {
f := g.config.click_fn
f(e.mouse_x, e.mouse_y, e.mouse_button, g.config.user_data)
}
}
else {}
}
}
mouse_up_event_handler := fn [mut g] (event JS.Event) {
match event {
JS.MouseEvent {
e := g.handle_mouse_event(event, .mouse_up)
if !isnil(g.config.event_fn) {
f := g.config.event_fn
f(e, g.config.user_data)
}
if !isnil(g.config.unclick_fn) {
f := g.config.unclick_fn
f(e.mouse_x, e.mouse_y, e.mouse_button, g.config.user_data)
}
}
else {}
}
}
mouse_move_event_handler := fn [mut g] (event JS.Event) {
match event {
JS.MouseEvent {
e := g.handle_mouse_event(event, .mouse_move)
if !isnil(g.config.event_fn) {
f := g.config.event_fn
f(e, g.config.user_data)
}
if !isnil(g.config.move_fn) {
f := g.config.move_fn
f(e.mouse_x, e.mouse_y, g.config.user_data)
}
}
else {}
}
}
mouse_leave_event_handler := fn [mut g] (event JS.Event) {
match event {
JS.MouseEvent {
e := g.handle_mouse_event(event, .mouse_leave)
if !isnil(g.config.event_fn) {
f := g.config.event_fn
f(e, g.config.user_data)
}
if !isnil(g.config.leave_fn) {
f := g.config.leave_fn
f(e, g.config.user_data)
}
}
else {}
}
}
mouse_enter_event_handler := fn [mut g] (event JS.Event) {
match event {
JS.MouseEvent {
e := g.handle_mouse_event(event, .mouse_enter)
if !isnil(g.config.event_fn) {
f := g.config.event_fn
f(e, g.config.user_data)
}
if !isnil(g.config.enter_fn) {
f := g.config.enter_fn
f(e, g.config.user_data)
}
}
else {}
}
}
keydown_event_handler := fn [mut g] (event JS.Event) {
println('keyboard')
match event {
JS.KeyboardEvent {
e := g.handle_keyboard_event(event, .key_down)
if !isnil(g.config.event_fn) {
f := g.config.event_fn
f(e, g.config.user_data)
}
if !isnil(g.config.keydown_fn) {
f := g.config.keydown_fn
// todo: modifiers
f(e.key_code, .super, g.config.user_data)
}
}
else {}
}
}
g.canvas.addEventListener('mousedown'.str, mouse_down_event_handler, JS.EventListenerOptions{})
dom.window().addEventListener('mouseup'.str, mouse_up_event_handler, JS.EventListenerOptions{})
g.canvas.addEventListener('mousemove'.str, mouse_move_event_handler, JS.EventListenerOptions{})
g.canvas.addEventListener('mouseleave'.str, mouse_leave_event_handler, JS.EventListenerOptions{})
g.canvas.addEventListener('mouseenter'.str, mouse_enter_event_handler, JS.EventListenerOptions{})
dom.document.addEventListener('keydown'.str, keydown_event_handler, JS.EventListenerOptions{})
return g
}
pub fn (mut ctx Context) run() {
// set context late, in case it changed (e.g., due to embedding)
if isnil(ctx.user_data) {
ctx.user_data = ctx
}
gg_animation_frame_fn(mut ctx)
}
pub fn (mut ctx Context) begin() {
// ctx.context.beginPath()
}
pub fn (mut ctx Context) end() {
// ctx.context.closePath()
}
pub fn (mut ctx Context) draw_line(x1 f32, y1 f32, x2 f32, y2 f32, c Color) {
ctx.context.beginPath()
ctx.context.strokeStyle = c.to_css_string().str
ctx.context.moveTo(JS.Number(x1), JS.Number(y1))
ctx.context.lineTo(JS.Number(x2), JS.Number(y2))
ctx.context.stroke()
ctx.context.closePath()
}
pub fn (mut ctx Context) quit() {
}
pub fn (mut ctx Context) draw_rect(x f32, y f32, w f32, h f32, c Color) {
ctx.context.beginPath()
ctx.context.fillStyle = c.to_css_string().str
ctx.context.fillRect(JS.Number(x), JS.Number(y), JS.Number(w), JS.Number(h))
ctx.context.closePath()
}
fn gg_animation_frame_fn(mut g Context) {
g.frame++
g.context.clearRect(JS.Number(0), JS.Number(0), JS.Number(g.config.width),
JS.Number(g.config.height))
// todo(playXE): handle events
if !isnil(g.config.frame_fn) {
f := g.config.frame_fn
f(g.user_data)
g.needs_refresh = false
}
g.window.requestAnimationFrame(fn [mut g] (time JS.Number) {
gg_animation_frame_fn(mut g)
})
}
fn (mut g Context) handle_mouse_event(event JS.MouseEvent, typ DOMEventType) Event {
mut e := Event{}
e.typ = typ
e.frame_count = g.frame
match int(event.button) {
0 {
e.mouse_button = .left
}
1 {
e.mouse_button = .middle
}
2 {
e.mouse_button = .right
}
else {
e.mouse_button = .invalid
}
}
e.mouse_x = f32(event.offsetX)
e.mouse_y = f32(event.offsetY)
e.mouse_dx = f32(event.movementX)
e.mouse_dy = f32(event.movementY)
bitplace := int(event.button)
g.mbtn_mask |= u8(1 << bitplace)
// g.mouse_buttons = MouseButtons(g.mbtn_mask)
g.mouse_pos_x = int(event.offsetX)
g.mouse_pos_y = int(event.offsetY)
g.mouse_dx = int(event.movementX)
g.mouse_dy = int(event.movementY)
return e
}
fn (mut g Context) handle_keyboard_event(event JS.KeyboardEvent, typ DOMEventType) Event {
mut e := Event{}
e.typ = typ
e.frame_count = g.frame
match string(event.code) {
'Space' {
e.key_code = .space
}
'Minus' {
e.key_code = .minus
}
'Quote' {
e.key_code = .apostrophe
}
'Comma' {
e.key_code = .comma
}
'Period' {
e.key_code = .period
}
'Digit0' {
e.key_code = ._0
}
'Digit1' {
e.key_code = ._1
}
'Digit2' {
e.key_code = ._2
}
'Digit3' {
e.key_code = ._3
}
'Digit4' {
e.key_code = ._4
}
'Digit5' {
e.key_code = ._5
}
'Digit6' {
e.key_code = ._6
}
'Digit7' {
e.key_code = ._7
}
'Digit8' {
e.key_code = ._8
}
'Digit9' {
e.key_code = ._9
}
'Semicolon' {
e.key_code = .semicolon
}
'Equal' {
e.key_code = .equal
}
'KeyA' {
e.key_code = .a
}
'KeyB' {
e.key_code = .b
}
'KeyC' {
e.key_code = .c
}
'KeyD' {
e.key_code = .d
}
'KeyE' {
e.key_code = .e
}
'KeyF' {
e.key_code = .f
}
'KeyG' {
e.key_code = .g
}
'KeyH' {
e.key_code = .h
}
'KeyI' {
e.key_code = .i
}
'KeyJ' {
e.key_code = .j
}
'KeyK' {
e.key_code = .k
}
'KeyL' {
e.key_code = .l
}
'KeyM' {
e.key_code = .m
}
'KeyN' {
e.key_code = .n
}
'KeyO' {
e.key_code = .o
}
'KeyP' {
e.key_code = .p
}
'KeyQ' {
e.key_code = .q
}
'KeyR' {
e.key_code = .r
}
'KeyS' {
e.key_code = .s
}
'KeyT' {
e.key_code = .t
}
'KeyU' {
e.key_code = .u
}
'KeyV' {
e.key_code = .v
}
'KeyW' {
e.key_code = .w
}
'KeyX' {
e.key_code = .x
}
'KeyY' {
e.key_code = .y
}
'KeyZ' {
e.key_code = .z
}
'BracketLeft' {
e.key_code = .left_bracket
}
'BracketRight' {
e.key_code = .right_bracket
}
'Backslash' {
e.key_code = .backslash
}
'Backquote' {
e.key_code = .grave_accent
}
'Escape' {
e.key_code = .escape
}
'Enter' {
e.key_code = .enter
}
'Tab' {
e.key_code = .tab
}
'Backspace' {
e.key_code = .backspace
}
'Insert' {
e.key_code = .insert
}
'Delete' {
e.key_code = .delete
}
'ArrowRight' {
e.key_code = .right
}
'ArrowLeft' {
e.key_code = .left
}
'ArrowUp' {
e.key_code = .up
}
'ArrowDown' {
e.key_code = .down
}
'PageUp' {
e.key_code = .page_up
}
'PageDown' {
e.key_code = .page_down
}
'Home' {
e.key_code = .home
}
'End' {
e.key_code = .end
}
'CapsLock' {
e.key_code = .caps_lock
}
'ScrollLock' {
e.key_code = .scroll_lock
}
'NumLock' {
e.key_code = .num_lock
}
'PrintScreen' {
e.key_code = .print_screen
}
'Pause' {
e.key_code = .pause
}
'ShiftLeft' {
e.key_code = .left_shift
}
'ShiftRight' {
e.key_code = .right_shift
}
'AltLeft' {
e.key_code = .left_alt
}
'AltRight' {
e.key_code = .right_alt
}
'ControlLeft' {
e.key_code = .left_control
}
'ControlRight' {
e.key_code = .right_control
}
else {
panic('todo: more keycodes (${string(event.code)})')
}
}
return e
}
+51
View File
@@ -0,0 +1,51 @@
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license that can be found in the LICENSE file.
module gg
pub type FNCb = fn (data voidptr)
// FNEvent defines the type of a function that will be called for every event
pub type FNEvent = fn (e &Event, data voidptr)
// FNEvent2 same as FNEvent with inverted arguments TODO: deprecate this, in favor of event_fn
pub type FNEvent2 = fn (data voidptr, e &Event)
// FNFail defines the type of a function that will be called when there is a fail
pub type FNFail = fn (msg string, data voidptr)
// FNKeyDown defines the type of a function that will be called for every key pressed down
pub type FNKeyDown = fn (c KeyCode, m Modifier, data voidptr)
// FNKeyUp defines the type of a function that will be called for every release of a pressed key
pub type FNKeyUp = fn (c KeyCode, m Modifier, data voidptr)
// FNMove defines the type of a function that will be called for every mouse move on the screen
pub type FNMove = fn (x f32, y f32, data voidptr)
// FNClick defines the type of a function that will be called for every mouse click
pub type FNClick = fn (x f32, y f32, button MouseButton, data voidptr)
// FNUnClick defines the type of a function that will be called every time a mouse button is released
pub type FNUnClick = fn (x f32, y f32, button MouseButton, data voidptr)
// FNChar defines the type of a function that will be called once per character
pub type FNChar = fn (c u32, data voidptr)
// FNUpdate defines the type of a function, that will be called at the start of each frame
// with an argument `dt`, that has the passed time in seconds, since the previous update.
pub type FNUpdate = fn (dt f32, data voidptr)
pub struct PenConfig {
pub:
color Color
line_type PenLineType = .solid
thickness f32 = 1
}
@[markused]
pub struct Size {
pub mut:
width int
height int
}
@@ -0,0 +1,21 @@
module gg
import os
import sokol.sapp
#include <android/configuration.h>
fn C.AConfiguration_new() voidptr
fn C.AConfiguration_fromAssetManager(voidptr, voidptr)
fn C.AConfiguration_getDensity(voidptr) u32
fn C.AConfiguration_delete(voidptr)
// android_dpi_scale returns the scale factor of the device.
pub fn android_dpi_scale() f32 {
config := C.AConfiguration_new()
activity := &os.NativeActivity(sapp.android_get_native_activity())
C.AConfiguration_fromAssetManager(config, activity.assetManager)
density := C.AConfiguration_getDensity(config)
C.AConfiguration_delete(config)
return f32(density) / 160
}
+22
View File
@@ -0,0 +1,22 @@
module gg
#include "@VEXEROOT/vlib/gg/gg_darwin.m"
fn C.gg_get_screen_size() Size
fn C.darwin_draw_string(x i32, y i32, s string, cfg voidptr)
fn C.darwin_text_width(s string) i32
fn C.darwin_text_width_runes(r []rune) i32
fn C.darwin_window_refresh()
fn C.darwin_draw_rect(f32, f32, f32, f32, voidptr)
fn C.darwin_create_image(path string) Image
fn C.darwin_draw_image(f32, f32, f32, f32, &Image)
fn C.darwin_draw_circle(f32, f32, f32, voidptr)
fn C.darwin_draw_circle_empty(f32, f32, f32, voidptr)
+252
View File
@@ -0,0 +1,252 @@
#include <Cocoa/Cocoa.h>
NSColor* nscolor(gg__Color c) {
float red = (float)c.r / 255.0f;
float green = (float)c.g / 255.0f;
float blue = (float)c.b / 255.0f;
return [NSColor colorWithDeviceRed:red green:green blue:blue alpha:1.0f];
}
NSString* nsstring(string s) {
return [[NSString alloc] initWithBytesNoCopy:s.str
length:s.len
encoding:NSUTF8StringEncoding
freeWhenDone:false];
}
gg__Size gg_get_screen_size() {
NSScreen *currentScreen = nil;
NSWindow *mainWindow = [NSApp mainWindow];
// 1. Try screen containing the main window
if (mainWindow) {
currentScreen = [mainWindow screen];
}
// 2. If no main window, try the key window (might be different, e.g., a panel)
if (!currentScreen) {
NSWindow *keyWindow = [NSApp keyWindow];
if (keyWindow) {
currentScreen = [keyWindow screen];
}
}
// 3. If no relevant window, find the screen containing the mouse cursor
if (!currentScreen) {
// Get mouse location in global screen coordinates (bottom-left origin)
NSPoint mouseLocation = [NSEvent mouseLocation];
NSArray<NSScreen *> *screens = [NSScreen screens];
for (NSScreen *screen in screens) {
// Check if the mouse location is within the screen's frame
// Note: Both mouseLocation and screen.frame use bottom-left origin coordinates
if (NSMouseInRect(mouseLocation, [screen frame], NO)) {
currentScreen = screen;
break; // Found the screen with the mouse
}
}
}
// 4. As a last resort, fall back to the main screen
if (!currentScreen) {
NSLog(@"Warning: Could not determine current screen based on window or mouse. Falling back to mainScreen.");
currentScreen = [NSScreen mainScreen];
}
// Now get the size of the determined screen
NSDictionary *description = [currentScreen deviceDescription];
// Use NSDeviceSize to get pixel dimensions
NSSize displayPixelSize = [[description objectForKey:NSDeviceSize] sizeValue];
// Create the V gg.Size object
gg__Size res;
res.width = displayPixelSize.width;
res.height = displayPixelSize.height;
return res;
}
void gg_macos_resize_window(void *window_ptr, int width, int height) {
if (window_ptr == nil || width <= 0 || height <= 0) {
return;
}
NSWindow *window = (__bridge NSWindow *)window_ptr;
[window setContentSize:NSMakeSize((CGFloat)width, (CGFloat)height)];
}
void gg_macos_set_window_resizable(void *window_ptr, bool resizable) {
if (window_ptr == nil) {
return;
}
NSWindow *window = (__bridge NSWindow *)window_ptr;
NSWindowStyleMask style = [window styleMask];
if (resizable) {
style |= NSWindowStyleMaskResizable;
} else {
style &= ~NSWindowStyleMaskResizable;
}
[window setStyleMask:style];
}
// When non-zero, every string is drawn in a monospace font regardless of the
// per-call cfg.mono flag. Lets an app switch the whole UI to a fixed-width font
// so text widths can be computed as char_count * advance (no CoreText layout).
// Default 0 => behaviour unchanged for every other gg app.
int g_gg_force_mono = 0;
void gg_set_force_mono(int on) { g_gg_force_mono = on; }
// Cache of the (font + color) attribute dictionaries passed to -drawAtPoint:.
// darwin_draw_string is called once per text run, every frame; building a fresh
// NSFont (font lookup), NSColor, and NSDictionary on each call was pure ObjC
// churn (CPU + autorelease-pool pressure). The set of distinct (size, style,
// color) combinations a UI actually uses is tiny (a few sizes × a theme palette),
// so we memoize the finished attribute dict. Keyed on everything that changes the
// dict — color rgb (nscolor ignores alpha), size, bold, mono, and the global
// force-mono flag. The dictionary retains each cached attr dict (ARC), so they
// persist for the process; bounded by the number of distinct combinations.
static NSMutableDictionary<NSNumber*, NSDictionary*>* g_text_attr_cache = nil;
static NSDictionary* darwin_text_attrs(gg__TextCfg cfg) {
uint64_t key = ((uint64_t)cfg.color.r)
| ((uint64_t)cfg.color.g << 8)
| ((uint64_t)cfg.color.b << 16)
| (((uint64_t)cfg.size & 0xFFFF) << 24)
| ((uint64_t)(cfg.bold ? 1 : 0) << 40)
| ((uint64_t)(cfg.mono ? 1 : 0) << 41)
| ((uint64_t)(g_gg_force_mono ? 1 : 0) << 42);
if (g_text_attr_cache == nil) {
g_text_attr_cache = [[NSMutableDictionary alloc] init];
}
NSNumber* k = @(key);
NSDictionary* cached = [g_text_attr_cache objectForKey:k];
if (cached != nil) {
return cached;
}
NSFont* font = [NSFont userFontOfSize:cfg.size];
// # NSFont* font = [NSFont fontWithName:@"Roboto Mono" size:cfg.size];
if (g_gg_force_mono) {
// Global monospace mode: requested size minus 1, kept in sync with the
// app-side width metric (see fuse_text_width / mono_char_advance).
font = [NSFont fontWithName:@"Menlo" size:cfg.size - 1];
} else if (cfg.mono) {
// # font = [NSFont fontWithName:@"Roboto Mono" size:cfg.size];
font = [NSFont fontWithName:@"Menlo" size:cfg.size - 5];
}
if (cfg.bold) {
font = [[NSFontManager sharedFontManager] convertFont:font toHaveTrait:NSBoldFontMask];
}
NSDictionary* attr = @{
NSForegroundColorAttributeName : nscolor(cfg.color),
// NSParagraphStyleAttributeName: paragraphStyle,
NSFontAttributeName : font,
};
[g_text_attr_cache setObject:attr forKey:k];
return attr;
}
void darwin_draw_string(int x, int y, string s, gg__TextCfg cfg) {
NSDictionary* attr = darwin_text_attrs(cfg);
[nsstring(s) drawAtPoint:NSMakePoint(x, y - 15) withAttributes:attr];
}
int darwin_text_width(string s) {
// println('text_width "${s}" len=${s.len}')
NSString* n = @"";
if (s.len == 1) {
// println('len=1')
n = [NSString stringWithFormat:@"%c", s.str[0]];
} else {
n = nsstring(s);
}
/*
# if (!defaultFont){
# defaultFont = [NSFont userFontOfSize: ui__DEFAULT_FONT_SIZE];
# }
# NSDictionary *attrs = @{
# NSFontAttributeName: defaultFont,
# };
*/
NSSize size = [n sizeWithAttributes:nil];
// # printf("!!!%f\n", ceil(size.width));
return (int)(ceil(size.width));
}
void darwin_draw_rect(float x, float y, float width, float height, gg__Color c) {
NSColor* color = nscolor(c);
NSRect rect = NSMakeRect(x, y, width, height);
[color setFill];
NSRectFill(rect);
}
static void mark_view_tree_needs_display(NSView *view) {
if (view == nil) {
return;
}
[view setNeedsDisplay:YES];
for (NSView *subview in [view subviews]) {
mark_view_tree_needs_display(subview);
}
}
void darwin_window_refresh() {
dispatch_async(dispatch_get_main_queue(), ^{
NSWindow *window = [NSApp mainWindow];
if (window == nil) {
window = [NSApp keyWindow];
}
if (window == nil) {
return;
}
NSView *contentView = [window contentView];
if (contentView == nil) {
return;
}
mark_view_tree_needs_display(contentView);
[window displayIfNeeded];
});
// puts("refresh");
//[[NSApp mainWindow].contentView drawRect:NSMakeRect(0,0,2000,2000)];
//[[NSGraphicsContext currentContext] flushGraphics];
}
gg__Image darwin_create_image(string path_) {
// file = file.trim_space()
NSString* path = nsstring(path_);
NSImage* img = [[NSImage alloc] initWithContentsOfFile:path];
if (img == 0) {
}
NSSize size = [img size];
gg__Image res;
res.width = size.width;
res.height = size.height;
res.path = path_;
res.ok = true;
// printf("inited img width=%d\n", res.width) ;
// need __bridge_retained so that the pointer is not freed by ARC
res.data = (__bridge_retained voidptr)(img);
return res;
}
void darwin_draw_image(float x, float y, float w, float h, gg__Image* img) {
NSImage* i = (__bridge NSImage*)(img->data);
[i drawInRect:NSMakeRect(x, y, w, h)];
}
void darwin_draw_circle(float x, float y, float d, gg__Color color) {
NSColor* c = nscolor(color);
NSRect rect = NSMakeRect(x, y, d * 2, d * 2);
NSBezierPath* circlePath = [NSBezierPath bezierPath];
[circlePath appendBezierPathWithOvalInRect:rect];
[c setFill];
// [circlePath stroke];
[circlePath fill];
// NSRectFill(rect);
}
void darwin_draw_circle_empty(float x, float y, float d, gg__Color color) {
NSColor* outlineColor = nscolor(color);
CGFloat outlineWidth = 1.0; //2.0;
NSRect rect = NSMakeRect(x, y, d * 2, d * 2);
NSBezierPath* circlePath = [NSBezierPath bezierPath];
[circlePath appendBezierPathWithOvalInRect:rect];
[outlineColor setStroke];
[circlePath setLineWidth:outlineWidth];
[circlePath stroke];
}
+27
View File
@@ -0,0 +1,27 @@
module gg
import sokol.sgl
// Stuff for ui from now for screenshot (that would be interesting for gg if screenshot is implemented also for gg)
// required for ui.DrawDevice interface (with &gg.Context as an instance)
pub fn (ctx &Context) scissor_rect(x int, y int, w int, h int) {
sgl.scissor_rect(int(ctx.scale * x), int(ctx.scale * y), int(ctx.scale * w),
int(ctx.scale * h), true)
}
// empty function
pub fn (ctx &Context) has_text_style() bool {
return false
}
// empty function
pub fn (ctx &Context) set_text_style(font_name string, font_path string, size int, color Color, align int,
vertical_align int) {
}
// default draw_text (draw_text_def but without set_text_cfg)
pub fn (ctx &Context) draw_text_default(x int, y int, text string) {
scale := if ctx.ft.scale == 0 { f32(1) } else { ctx.ft.scale }
ctx.ft.fons.draw_text(x * scale, y * scale, text) // TODO: check offsets/alignment
}
+493
View File
@@ -0,0 +1,493 @@
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license that can be found in the LICENSE file.
module gg
import os
import stbi
import sokol.gfx
import sokol.sgl
// Image holds the fields and data needed to
// represent a bitmap/pixel based image in memory.
@[heap; markused]
pub struct Image {
pub mut:
id int
width int
height int
nr_channels int
nr_mipmaps int
ok bool
data voidptr
ext string
simg_ok bool
simg gfx.Image
ssmp gfx.Sampler
path string
texture_filter TextureFilter = .linear
}
@[params]
pub struct ImageConfig {
pub:
texture_filter ?TextureFilter
max_mipmaps int = 1 // a value below 1 means as many as possible
}
// destroy GPU resources associated with the image
fn (image &Image) destroy() {
if image.ok {
if image.simg.id > 0 {
gfx.destroy_image(image.simg)
}
if image.ssmp.id > 0 {
gfx.destroy_sampler(image.ssmp)
}
}
}
// create_image_with_filter creates an `Image` from `file` with the requested texture filter.
@[deprecated: 'use Context.create_image instead']
pub fn (mut ctx Context) create_image_with_filter(file string, texture_filter TextureFilter) !Image {
return ctx.create_image(file, texture_filter: texture_filter)!
}
// create_image creates an `Image` from `file`.
pub fn (mut ctx Context) create_image(file string, cfg ImageConfig) !Image {
if !os.exists(file) {
$if android {
image_data := os.read_apk_asset(file)!
mut image := ctx.create_image_from_byte_array(image_data, cfg)!
image.path = file
return image
} $else {
return error('image file "${file}" not found')
}
}
$if macos {
if ctx.native_rendering {
mut img := C.darwin_create_image(file)
img.texture_filter = cfg.texture_filter or { ctx.config.texture_filter }
img.id = ctx.image_cache.len
unsafe {
ctx.image_cache << img
}
return img
}
}
mut img := load_image(file)!
img.id = ctx.image_cache.len
img.texture_filter = cfg.texture_filter or { ctx.config.texture_filter }
unsafe {
ctx.image_cache << img
}
if gfx.is_valid() {
img.init_sokol_image()
}
return img
}
// adapted from https://github.com/floooh/sokol/issues/102#issuecomment-2926566603
fn sokol_mipmap(mut simg_desc gfx.ImageDesc, max_mipmaps int) {
mut levels := 0
mut size := 0
width := simg_desc.width
height := simg_desc.height
mipmaps := if max_mipmaps < gfx.sg_max_mipmaps && max_mipmaps > 0 {
max_mipmaps
} else {
gfx.sg_max_mipmaps
}
for i in 1 .. mipmaps {
w := width >> i
h := height >> i
if w < 1 || h < 1 { break
}
size += w * h * 4 // 4 = img.nr_channels
levels++
}
// use unsafe to access the data because we have to anyway
mut target := unsafe { malloc(size) }
mut src_width := width
mut src_height := height
for level in 1 .. levels {
src := &u8(simg_desc.data.subimage[0][level - 1].ptr)
target_width := src_width / 2
target_height := src_height / 2
for x in 0 .. target_width {
for y in 0 .. target_height {
// channels RGBA
chans := 4
for ch in 0 .. 4 {
// avererage colors in a 2x2 square over the source image where (x*2|y*2) is the lower left corner
mut color := 0
sx := x * 2
sy := y * 2
color += unsafe { src[(sx + src_width * sy) * chans + ch] }
color += unsafe { src[(sx + src_width * (sy + 1)) * chans + ch] }
color += unsafe { src[((sx + 1) + src_width * (sy + 1)) * chans + ch] }
color += unsafe { src[((sx + 1) + src_width * sy) * chans + ch] }
color /= 4
unsafe {
target[(x + y * target_width) * chans + ch] = u8(color)
}
}
}
}
src_width = target_width
src_height = target_height
simg_desc.data.subimage[0][level].ptr = target
simg_desc.data.subimage[0][level].size = usize(target_width * target_height * 4)
unsafe {
target += target_width * target_height * 4
}
}
simg_desc.num_mipmaps = levels
}
// init_sokol_image initializes this `Image` for use with the
// sokol graphical backend system.
pub fn (mut img Image) init_sokol_image() &Image {
// println('\n init sokol image ${img.path} ok=${img.simg_ok}')
mut img_desc := gfx.ImageDesc{
width: img.width
height: img.height
// wrap_u: .clamp_to_edge // XTODO SAMPLER
// wrap_v: .clamp_to_edge
label: &char(img.path.str)
d3d11_texture: 0
}
// NOTE the following code, sometimes, result in hard-to-detect visual errors/bugs:
// img_size := usize(img.nr_channels * img.width * img.height)
// As an example see https://github.com/vlang/vab/issues/239
// The image will come out blank for some reason and no SOKOL_ASSERT
// nor any CI check will/can currently catch this.
// Since all of gg currently runs with more or less *defaults* from sokol_gfx/sokol_gl
// we should currently just use the sum of each of the RGB and A channels (= 4) here instead.
// Optimized PNG images that have no alpha channel is often optimized to only have
// 3 (or less) channels which stbi will correctly detect and set as `img.nr_channels`
// but the current sokol_gl context setup expects 4. It *should* be the same with
// all other stbi supported formats.
img_size := usize(4 * img.width * img.height)
img_desc.data.subimage[0][0] = gfx.Range{
ptr: img.data
size: img_size
}
sokol_mipmap(mut img_desc, img.nr_mipmaps)
img.nr_mipmaps = img_desc.num_mipmaps
img.simg = gfx.make_image(&img_desc)
gfx_filter := img.texture_filter.gfx_filter()
mut smp_desc := gfx.SamplerDesc{
min_filter: gfx_filter
mag_filter: gfx_filter
mipmap_filter: .linear
wrap_u: .clamp_to_edge
wrap_v: .clamp_to_edge
}
img.ssmp = gfx.make_sampler(&smp_desc)
img.simg_ok = true
img.ok = true
return img
}
// draw_image draws the provided image onto the screen.
pub fn (ctx &Context) draw_image(x f32, y f32, width f32, height f32, img_ &Image) {
ctx.draw_image_with_config(
img: img_
img_rect: Rect{x, y, width, height}
part_rect: Rect{0, 0, img_.width, img_.height}
)
}
// new_streaming_image returns a cached `image_idx` of a special image, that
// can be updated *each frame* by calling: gg.update_pixel_data(image_idx, buf)
// ... where buf is a pointer to the actual pixel data for the image.
// Note: you still need to call app.gg.draw_image after that, to actually draw it.
// Note: Sokol needs to be setup, *before* calling this function. In practice,
// this often means, that you have to call it once in the `init_fn` callback of
// gg.new_context, or gg.start, and then store the result in your app instance.
pub fn (mut ctx Context) new_streaming_image(w int, h int, channels int, sicfg StreamingImageConfig) int {
mut img := Image{}
img.width = w
img.height = h
img.nr_channels = channels // 4 bytes per pixel for .rgba8, see pixel_format
mut img_desc := gfx.ImageDesc{
width: img.width
height: img.height
pixel_format: sicfg.pixel_format
num_slices: 1
num_mipmaps: 1
usage: .stream
label: &char(img.path.str)
}
// Sokol requires that streamed images have NO .ptr/.size initially:
img_desc.data.subimage[0][0] = gfx.Range{
ptr: 0
size: usize(0)
}
img.simg = gfx.make_image(&img_desc)
mut smp_desc := gfx.SamplerDesc{
wrap_u: sicfg.wrap_u // SAMPLER
wrap_v: sicfg.wrap_v
min_filter: sicfg.min_filter
mag_filter: sicfg.mag_filter
}
img.ssmp = gfx.make_sampler(&smp_desc)
img.simg_ok = true
img.ok = true
img_idx := ctx.cache_image(img)
return img_idx
}
// update_pixel_data is a helper for working with image streams (i.e. images,
// that are updated dynamically by the CPU on each frame)
pub fn (mut ctx Context) update_pixel_data(cached_image_idx int, buf &u8) {
mut image := ctx.get_cached_image_by_idx(cached_image_idx)
image.update_pixel_data(buf)
}
// update_pixel_data updates the sokol specific pixel data associated
// with this `Image`.
pub fn (mut img Image) update_pixel_data(buf &u8) {
mut data := gfx.ImageData{}
data.subimage[0][0].ptr = buf
data.subimage[0][0].size = usize(img.width * img.height * img.nr_channels)
gfx.update_image(img.simg, &data)
}
// create_image_with_size creates an `Image` from `file` in the given
// `width` x `height` dimension.
//
// TODO: copypasta
@[deprecated]
pub fn (mut ctx Context) create_image_with_size(file string, width int, height int) Image {
if !gfx.is_valid() {
// Sokol is not initialized yet, add stbi object to a queue/cache
// ctx.image_queue << file
mut img := load_image(file) or { return Image{} }
img.texture_filter = ctx.config.texture_filter
img.width = width
img.height = height
img.ok = false
img.id = ctx.image_cache.len
ctx.image_cache << img
return img
}
return ctx.create_image(file) or { Image{} }
}
fn load_image(file string) !Image {
if !os.exists(file) {
return error('image file "${file}" not found')
}
stb_img := stbi.load(file)!
return Image{
width: stb_img.width
height: stb_img.height
nr_channels: stb_img.nr_channels
ok: stb_img.ok
data: stb_img.data
ext: stb_img.ext
path: file
}
}
// create_image_from_memory creates an `Image` from the
// memory buffer `buf` of size `bufsize`.
//
// See also: create_image_from_byte_array
pub fn (mut ctx Context) create_image_from_memory(buf &u8, bufsize int, cfg ImageConfig) !Image {
stb_img := stbi.load_from_memory(buf, bufsize)!
mut img := Image{
width: stb_img.width
height: stb_img.height
nr_channels: stb_img.nr_channels
ok: stb_img.ok
data: stb_img.data
ext: stb_img.ext
id: ctx.image_cache.len
texture_filter: cfg.texture_filter or { ctx.config.texture_filter }
}
if gfx.is_valid() {
img.init_sokol_image()
}
ctx.image_cache << img
return img
}
// create_image_from_memory_with_filter creates an `Image` from `buf` with the requested texture filter.
@[deprecated: 'use Context.create_image_from_memory instead']
pub fn (mut ctx Context) create_image_from_memory_with_filter(buf &u8, bufsize int, texture_filter TextureFilter) !Image {
return ctx.create_image_from_memory(buf, bufsize, texture_filter: texture_filter)
}
// create_image_from_byte_array creates an `Image` from the
// byte array `b`.
//
// See also: create_image_from_memory
pub fn (mut ctx Context) create_image_from_byte_array(b []u8, cfg ImageConfig) !Image {
return ctx.create_image_from_memory(b.data, b.len, cfg)
}
// create_image_from_byte_array_with_filter creates an `Image` from `b` with the requested texture filter.
@[deprecated: 'use Context.create_image_from_byte_array instead']
pub fn (mut ctx Context) create_image_from_byte_array_with_filter(b []u8, texture_filter TextureFilter) !Image {
return ctx.create_image_from_memory(b.data, b.len, texture_filter: texture_filter)
}
pub struct StreamingImageConfig {
pub:
pixel_format gfx.PixelFormat = .rgba8
wrap_u gfx.Wrap = .clamp_to_edge
wrap_v gfx.Wrap = .clamp_to_edge
min_filter gfx.Filter = .linear
mag_filter gfx.Filter = .linear
num_mipmaps int = 1
num_slices int = 1
}
fn (filter TextureFilter) gfx_filter() gfx.Filter {
return match filter {
.linear { .linear }
.nearest { .nearest }
}
}
// draw_image_with_config takes in a config that details how the
// provided image should be drawn onto the screen
pub fn (ctx &Context) draw_image_with_config(config DrawImageConfig) {
$if macos {
if ctx.native_rendering {
unsafe {
mut img := config.img
if config.img == nil {
// Get image by id
if config.img_id > 0 {
img = &ctx.image_cache[config.img_id]
} else {
$if !noggverbose ? {
eprintln('gg: failed to get image to draw natively')
}
return
}
}
if img.id >= ctx.image_cache.len {
eprintln('gg: draw_image() bad img id ${img.id} (img cache len = ${ctx.image_cache.len})')
return
}
if img.width == 0 {
println('gg: draw_image() width=0')
return
}
if !os.exists(img.path) {
println('not exist path')
return
}
x := config.img_rect.x
y := config.img_rect.y
width := if config.img_rect.width == 0 {
// Calculate the width by dividing it by the height ratio.
// e.g. the original image is 100x100, we're drawing 0x20. Find the ratio (5)
// by dividing the height 100 by 20, and then divide the width by 5.
f32(img.width / (img.height / config.img_rect.height))
} else {
config.img_rect.width
}
height := if config.img_rect.height == 0 {
// Same as above.
f32(img.height / (img.width / config.img_rect.width))
} else {
config.img_rect.height
}
C.darwin_draw_image(x, ctx.height - (y + config.img_rect.height), width, height,
img)
return
}
}
}
id := if !isnil(config.img) { config.img.id } else { config.img_id }
if id >= ctx.image_cache.len {
eprintln('gg: draw_image() bad img id ${id} (img cache len = ${ctx.image_cache.len})')
return
}
img := &ctx.image_cache[id]
if !img.simg_ok {
return
}
mut img_rect := config.img_rect
if img_rect.width == 0 && img_rect.height == 0 {
img_rect = Rect{img_rect.x, img_rect.y, img.width, img.height}
}
mut part_rect := config.part_rect
if part_rect.width == 0 && part_rect.height == 0 {
part_rect = Rect{part_rect.x, part_rect.y, img.width, img.height}
}
u0 := part_rect.x / img.width
v0 := part_rect.y / img.height
u1 := (part_rect.x + part_rect.width) / img.width
v1 := (part_rect.y + part_rect.height) / img.height
x0 := img_rect.x * ctx.scale
y0 := img_rect.y * ctx.scale
x1 := (img_rect.x + img_rect.width) * ctx.scale
mut y1 := (img_rect.y + img_rect.height) * ctx.scale
if img_rect.height == 0 {
scale := f32(img.width) / f32(img_rect.width)
y1 = f32(img_rect.y + int(f32(img.height) / scale)) * ctx.scale
}
flip_x := config.flip_x
flip_y := config.flip_y
mut u0f := if !flip_x { u0 } else { u1 }
mut u1f := if !flip_x { u1 } else { u0 }
mut v0f := if !flip_y { v0 } else { v1 }
mut v1f := if !flip_y { v1 } else { v0 }
// FIXME: is this okay?
match config.effect {
.alpha { sgl.load_pipeline(ctx.pipeline.alpha) }
.add { sgl.load_pipeline(ctx.pipeline.add) }
}
sgl.enable_texture()
sgl.texture(img.simg, img.ssmp)
if config.rotation != 0 {
width := img_rect.width * ctx.scale
height := (if img_rect.height > 0 { img_rect.height } else { img.height }) * ctx.scale
sgl.push_matrix()
sgl.translate(x0 + (width / 2), y0 + (height / 2), 0)
sgl.rotate(sgl.rad(-config.rotation), 0, 0, 1)
sgl.translate(-x0 - (width / 2), -y0 - (height / 2), 0)
}
sgl.begin_quads()
sgl.c4b(config.color.r, config.color.g, config.color.b, config.color.a)
sgl.v3f_t2f(x0, y0, config.z, u0f, v0f)
sgl.v3f_t2f(x1, y0, config.z, u1f, v0f)
sgl.v3f_t2f(x1, y1, config.z, u1f, v1f)
sgl.v3f_t2f(x0, y1, config.z, u0f, v1f)
sgl.end()
if config.rotation != 0 {
sgl.pop_matrix()
}
sgl.disable_texture()
}
+22
View File
@@ -0,0 +1,22 @@
module gg
@[heap]
pub struct Image {
pub mut:
id int
width int
height int
nr_channels int
ok bool
data voidptr
ext string
path string
}
pub fn (ctx &Context) draw_image_with_config(config DrawImageConfig) {
}
// destroy GPU resources associated with the image
fn (img &Image) destroy() {
}
+120
View File
@@ -0,0 +1,120 @@
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license that can be found in the LICENSE file.
module gg
// TextureFilter controls how gg samples textures when images are scaled.
pub enum TextureFilter {
linear
nearest
}
// DrawImageConfig struct defines the various options
// that can be used to draw an image onto the screen
pub struct DrawImageConfig {
pub mut:
flip_x bool // set to true, if you need to flip the image horizontally (around a vertical axis), <- will become ->
flip_y bool // set to true, if you need to flip the image vertically (around a horizontal axiz), -\/- will become -/\-
img &Image = unsafe { nil }
img_id int
img_rect Rect // defines the size and position on image when rendering to the screen
part_rect Rect // defines the size and position of part of the image to use when rendering
z f32
color Color = white
effect ImageEffect = .alpha
rotation f32 // the amount to rotate the image in degrees, counterclockwise. Use a negative value, to rotate it clockwise.
}
pub enum ImageEffect {
// TODO(FireRedz): Add more effects
alpha
add
}
// Rect represents a rectangular shape in `gg`.
pub struct Rect {
pub mut:
x f32
y f32
width f32
height f32
}
// cache_image caches the image `img` in memory for later reuse.
// cache_image returns the cache index of the cached image.
//
// See also: get_cached_image_by_idx
// See also: remove_cached_image_by_idx
pub fn (mut ctx Context) cache_image(img Image) int {
ctx.image_cache << img
image_idx := ctx.image_cache.len - 1
ctx.image_cache[image_idx].id = image_idx
return image_idx
}
const missing_image = Image{}
// get_cached_image_by_idx returns a cached `Image` identified by `image_idx`.
// If image not found, returns `Image{ok: false}`
//
// See also: cache_image
// See also: remove_cached_image_by_idx
pub fn (mut ctx Context) get_cached_image_by_idx(image_idx int) &Image {
if image_idx < 0 || image_idx > ctx.image_cache.len - 1 {
return unsafe { &missing_image }
}
return &ctx.image_cache[image_idx]
}
// remove_cached_image_by_idx removes an `Image` identified by `image_idx` from the
// image cache.
//
// See also: cache_image
// See also: get_cached_image_by_idx
pub fn (mut ctx Context) remove_cached_image_by_idx(image_idx int) {
if image_idx < 0 || image_idx > ctx.image_cache.len - 1 {
return
}
ctx.image_cache[image_idx].destroy()
ctx.image_cache[image_idx] = unsafe { &missing_image }
}
// Draw part of an image using uv coordinates
// img_rect is the size and position (in pixels on screen) of the displayed rectangle (ie the draw_image args)
// part_rect is the size and position (in absolute pixels in the image) of the wanted part
// eg. On a 600*600 context, to display only the first 400*400 pixels of a 2000*2000 image
// on the entire context surface, call :
// draw_image_part(Rect{0, 0, 600, 600}, Rect{0, 0, 400, 400}, img)
pub fn (ctx &Context) draw_image_part(img_rect Rect, part_rect Rect, img_ &Image) {
ctx.draw_image_with_config(
img: img_
img_rect: img_rect
part_rect: part_rect
)
}
// draw_image_flipped draws the provided image flipped horizontally (use `draw_image_with_config` to flip vertically)
pub fn (ctx &Context) draw_image_flipped(x f32, y f32, width f32, height f32, img_ &Image) {
ctx.draw_image_with_config(
flip_x: true
img: img_
img_rect: Rect{x, y, width, height}
)
}
// draw_image_by_id draws an image by its id
pub fn (ctx &Context) draw_image_by_id(x f32, y f32, width f32, height f32, id int) {
ctx.draw_image_with_config(
img_id: id
img_rect: Rect{x, y, width, height}
)
}
// draw_image_3d draws an image with a z depth
pub fn (ctx &Context) draw_image_3d(x f32, y f32, z f32, width f32, height f32, img_ &Image) {
ctx.draw_image_with_config(
img: img_
img_rect: Rect{x, y, width, height}
z: z
)
}
+27
View File
@@ -0,0 +1,27 @@
// vtest build: !docker-ubuntu-musl // needs GL/gl.h
module gg
import os
const background_path = os.join_path(@VEXEROOT, 'examples/flappylearning/assets/img/background.png')
fn test_load_image_loads_supported_file_contents() {
img := load_image(background_path)!
assert img.width > 0
assert img.height > 0
assert img.nr_channels == 4
assert img.path == background_path
}
fn test_load_image_returns_error_for_unsupported_file_contents() {
file := os.join_path(os.vtmp_dir(), 'gg_unsupported_image_${os.getpid()}.jpg')
os.write_file(file, 'not an image') or { panic(err) }
defer {
os.rm(file) or {}
}
load_image(file) or {
assert err.msg().contains('stbi_image failed to load')
return
}
assert false
}
+85
View File
@@ -0,0 +1,85 @@
// vtest build: !docker-ubuntu-musl // needs GL/gl.h
import os
import gg
const vroot = @VEXEROOT
const background_path = os.join_path(vroot, 'examples/flappylearning/assets/img/background.png')
fn test_get_cached_image_idx_bounds_checking() {
mut ctx := gg.new_context(width: 100)
assert ctx.get_cached_image_by_idx(-1).ok == false
assert ctx.get_cached_image_by_idx(1).ok == false
}
fn test_remove_cached_image_remove_and_get() {
mut ctx := gg.new_context(width: 100)
image := gg.Image{
ok: true
}
idx := ctx.cache_image(image)
assert ctx.get_cached_image_by_idx(idx).ok == true
ctx.remove_cached_image_by_idx(idx)
assert ctx.get_cached_image_by_idx(idx).ok == false
}
fn test_remove_cached_images_by_id_keeps_other_ids_valid() {
mut ctx := gg.new_context(width: 100)
background_bytes := os.read_bytes(background_path)!
prev := ctx.create_image_from_byte_array(background_bytes)!
curr := ctx.create_image_from_byte_array(background_bytes)!
assert prev.id == 0
assert curr.id == 1
assert ctx.get_cached_image_by_idx(prev.id).ok == true
assert ctx.get_cached_image_by_idx(curr.id).ok == true
ctx.remove_cached_image_by_idx(prev.id)
ctx.remove_cached_image_by_idx(curr.id)
assert ctx.get_cached_image_by_idx(prev.id).ok == false
assert ctx.get_cached_image_by_idx(curr.id).ok == false
}
fn test_new_context_sets_borderless_window_flag() {
ctx := gg.new_context(
width: 100
borderless_window: true
)
assert ctx.window.borderless_window == true
}
fn test_new_context_sets_texture_filter() {
ctx := gg.new_context(
width: 100
texture_filter: .nearest
)
assert ctx.config.texture_filter == .nearest
}
fn test_create_image_from_byte_array_loads_rgba_pixels() {
mut ctx := gg.new_context(width: 100)
background_bytes := os.read_bytes(background_path)!
img := ctx.create_image_from_byte_array(background_bytes)!
assert img.width > 0
assert img.height > 0
assert img.nr_channels == 4
assert !isnil(img.data)
assert img.texture_filter == .linear
assert ctx.get_cached_image_by_idx(img.id).nr_channels == 4
}
fn test_create_image_from_byte_array_uses_context_texture_filter() {
mut ctx := gg.new_context(
width: 100
texture_filter: .nearest
)
background_bytes := os.read_bytes(background_path)!
img := ctx.create_image_from_byte_array(background_bytes)!
assert img.texture_filter == .nearest
assert ctx.get_cached_image_by_idx(img.id).texture_filter == .nearest
}
fn test_create_image_from_byte_array_with_filter_overrides_context_default() {
mut ctx := gg.new_context(width: 100)
background_bytes := os.read_bytes(background_path)!
img := ctx.create_image_from_byte_array(background_bytes, texture_filter: .nearest)!
assert img.texture_filter == .nearest
assert ctx.get_cached_image_by_idx(img.id).texture_filter == .nearest
}
+8
View File
@@ -0,0 +1,8 @@
module gg
// is_key_down returns whether the given key is currently pressed.
// You can use this, if you do not want to implement your own key event handling.
@[inline]
pub fn (mut ctx Context) is_key_down(k KeyCode) bool {
return ctx.pressed_keys[int(k)]
}
+102
View File
@@ -0,0 +1,102 @@
module m4
/**********************************************************************
* Simple vector/matrix graphic utility
* Copyright (c) 2021 Dario Deledda. All rights reserved.
* Use of this source code is governed by an MIT license
* that can be found in the LICENSE file.
**********************************************************************/
import math
// Translate degrees to radians
@[inline]
pub fn rad(deg f32) f32 {
return (math.pi / 180.0) * deg
}
// Translate radians to degrees
@[inline]
pub fn deg(grad f32) f32 {
return (180.0 / math.pi) * grad
}
// calculate the Orthographic projection matrix
pub fn ortho(left f32, right f32, bottom f32, top f32, z_near f32, z_far f32) Mat4 {
rml := right - left
rpl := right + left
tmb := top - bottom
tpb := top + bottom
fmn := z_far - z_near
fpn := z_far + z_near
// vfmt off
if fmn != 0 {
return Mat4{
e: [
2 / rml, 0, 0, -(rpl / rml),
0 , 2 / tmb, 0, -(tpb / tmb),
0 , 0, 2 / fmn, -(fpn / fmn),
0 , 0, 0, 1,
]!
}
}
return Mat4{
e: [
2 / rml, 0, 0, -(rpl / rml),
0, 2 / tmb, 0, -(tpb / tmb),
0, 0, 0, 0,
0, 0, 0, 1,
]!
}
// vfmt on
}
// Calculate the perspective matrix using (fov:fov, ar:aspect_ratio ,n:near_pane, f:far_plane) as parameters
pub fn perspective(fov f32, ar f32, n f32, f f32) Mat4 {
ctan := f32(1.0 / math.tan(fov * (f32(math.pi) / 360.0))) // for the FOV we use 360 instead 180
// vfmt off
return Mat4{
e: [
ctan / ar, 0, 0, 0,
0, ctan, 0, 0,
0, 0, (n + f) / (n - f), -1.0,
0, 0, (2.0 * n * f) / (n - f), 0,
]!
}
// vfmt on
}
// Calculate the look-at matrix
pub fn look_at(eye Vec4, center Vec4, up Vec4) Mat4 {
f := (center - eye).normalize3()
s := (f % up).normalize3()
u := (s % f)
// vfmt off
return Mat4{e: [
s.e[0], u.e[0], -f.e[0], 0,
s.e[1], u.e[1], -f.e[1], 0,
s.e[2], u.e[2], -f.e[2], 0,
-(s * eye), -(u * eye), f * eye, 1,
]!}
// vfmt on
}
// Get the complete transformation matrix for GLSL demos
pub fn calc_tr_matrices(w f32, h f32, rx f32, ry f32, in_scale f32) Mat4 {
proj := perspective(60, w / h, 0.01, 10.0)
// vfmt off
view := look_at(
Vec4{ e: [f32(0.0), 1.5, 6, 0]! },
Vec4{ e: [f32(0), 0, 0, 0]!},
Vec4{ e: [f32(0), 1.0, 0, 0]!}
)
view_proj := view * proj
rxm := rotate(rad(rx), Vec4{ e: [f32(1), 0, 0, 0]! })
rym := rotate(rad(ry), Vec4{ e: [f32(0), 1, 0, 0]! })
scale_m := scale(Vec4{ e: [in_scale, in_scale, in_scale, 1]! })
// vfmt on
model := rym * rxm
res := (scale_m * model) * view_proj
return res
}
+237
View File
@@ -0,0 +1,237 @@
import gg.m4
// vfmt off
pub fn test_m4() {
unsafe {
// Test Mat4
mut a := m4.Mat4{ e: [
f32(0), 1, 2, 3,
4, 5, 6, 7,
8, 9, 10, 11,
12, 13, 14, 15,
]!
}
mut b := m4.Mat4{}
mut c := m4.Mat4{}
// equal test
assert a.e == [
f32(0), 1, 2, 3,
4, 5, 6, 7,
8, 9, 10, 11,
12, 13, 14, 15,
]!
// copy test
b.copy(a)
assert a.e == b.e
// test: transpose, scale
assert b.transpose().mul_scalar(2.0).mul_scalar(0.5).transpose().e == a.e
assert b.sum_all() == 120.0
// test rows/columns set/get
for i in 0 .. 4 {
b = m4.zero_m4()
b.set_row(i, m4.Vec4{ e: [f32(1.0), 2, 3, 4]! })
assert b.get_f(0, i) == 1.0
assert b.get_f(1, i) == 2.0
assert b.get_f(2, i) == 3.0
assert b.get_f(3, i) == 4.0
// println(b)
c = m4.zero_m4()
c.set_col(i, m4.Vec4{ e: [f32(1.0), 2, 3, 4]! })
assert c.get_f(i, 0) == 1.0
assert c.get_f(i, 1) == 2.0
assert c.get_f(i, 2) == 3.0
assert c.get_f(i, 3) == 4.0
// println(c)
}
}
}
fn test_swap_col_row() {
unsafe {
// swap_col / swap_row
b := m4.Mat4{ e: [
f32(1), 2, 3, 4,
5, 6, 7, 8,
9, 10, 11, 12,
13, 14, 15, 16,
]!
}
b.swap_col(0, 2)
assert b.e == [
f32(3), 2, 1, 4,
7, 6, 5, 8,
11, 10, 9, 12,
15, 14, 13, 16,
]!
b = m4.Mat4{ e: [
f32(1), 2, 3, 4,
5, 6, 7, 8,
9, 10, 11, 12,
13, 14, 15, 16,
]!
}
b.swap_row(0, 2)
assert b.e == [
f32(9), 10, 11, 12,
5, 6, 7, 8,
1, 2, 3, 4,
13, 14, 15, 16,
]!
}
}
fn test_sum_sub() {
unsafe {
// test sum/sub
b := m4.unit_m4()
c := m4.unit_m4()
assert m4.sub(m4.add(b, c), b).e == m4.unit_m4().e
assert (b + c - b).e == m4.unit_m4().e
}
}
fn test_transpose() {
unsafe {
b := m4.Mat4{ e: [
f32(0), 1, 2, 3,
4, 5, 6, 7,
8, 9, 10, 11,
12, 13, 14, 15,
]!
}
assert b.transpose().transpose().e == b.e
}
}
fn test_multiplication() {
unsafe {
b := m4.Mat4{ e: [
f32(1), 0, 0, 0,
0, 2, 0, 0,
0, 0, 3, 0,
0, 0, 0, 4,
]!
}
c := m4.Mat4{ e: [
f32(1), 2, 3, 4,
5, 6, 7, 8,
9, 10, 11, 12,
13, 14, 15, 16,
]!
}
assert (c * c).e == [
f32(90),100,110,120,
202,228,254,280,
314,356,398,440,
426,484,542,600,
]!
assert m4.mul(c, c).e == [
f32(90),100,110,120,
202,228,254,280,
314,356,398,440,
426,484,542,600,
]!
assert m4.mul(b, c).e == [
f32(1), 2, 3, 4,
10, 12, 14, 16,
27, 30, 33, 36,
52, 56, 60, 64,
]!
assert (b * c).e == [
f32(1), 2, 3, 4,
10, 12, 14, 16,
27, 30, 33, 36,
52, 56, 60, 64,
]!
assert m4.det(b) == 24
}
}
fn test_det() {
unsafe {
b := m4.Mat4{ e: [
f32(5), 6, 6, 8,
2, 2, 2, 8,
6, 6, 2, 8,
2, 3, 6, 7,
]!
}
assert m4.det(b) == -8
c := m4.Mat4{ e: [
f32(1), 8, 2, 3,
8, 2, 3, 1,
2, 3, 3, 2,
3, 1, 2, 4,
]!
}
// println("*** INVERSE ****")
// println(m4.mul(b.inverse(),b))
// println(m4.clean_small(m4.mul(c.inverse(),c)))
// println("****************")
assert m4.mul(b.inverse(), b).e == m4.unit_m4().e
assert m4.mul(c.inverse(), c).is_equal(m4.unit_m4())
}
}
fn test_vec4() {
// Test Vec4
// println("*** Vector4 ****")
assert m4.vec3(1,2,3) == m4.Vec4{[f32(1), 2, 3, 1]!}
mut v := m4.Vec4{[f32(1), 2, 3, 4]!}
assert v * v.inv() == 4
assert v.mul_scalar(1.0 / v.mod()).mod() == 1
assert v + m4.Vec4{ e: [f32(5), 6, 7, 8]! } == m4.Vec4{ e: [f32(6), 8, 10, 12]! }
assert v - m4.Vec4{ e: [f32(1), 2, 3, 4]! } == m4.Vec4{ e: [f32(0), 0, 0, 0]! }
assert v.mul_vec4(m4.Vec4{ e: [f32(2), 2, 2, 2]! }) == m4.Vec4{ e: [f32(2), 4, 6, 8]! }
assert f32_abs(v.normalize().mod() - 1) < m4.precision
v = m4.Vec4{[f32(1), 2, 3, 0]!}
assert f32_abs(v.normalize3().mod3() - 1) < m4.precision
assert f32_abs(v.normalize3().mod() - 1) < m4.precision
// cross product
// x y z
// 1 2 3 ==> -3 6 -3 0
// 4 5 6
// println(m4.Vec4{[f32(1),2,3,2]!} % m4.Vec4{[f32(4),5,6,2]!})
assert m4.Vec4{[f32(1), 2, 3, 0]!} % m4.Vec4{[f32(4), 5, 6, 0 ]!} == m4.Vec4{[ f32(-3), 6, -3, 0, ]!}
assert m4.Vec4{[f32(1), 2, 3, 13]!} % m4.Vec4{[f32(4), 5, 6, 11]!} == m4.Vec4{[ f32(-3), 6, -3, 0, ]!}
// matrix * vector
a := m4.Mat4{ e: [
f32(1), 2, 3, 4
5, 6, 7, 8
9, 10, 11, 12
13, 14, 15, 16
]!
}
assert m4.mul_vec(a, m4.Vec4{[f32(1), 2, 3, 4]!}) == m4.Vec4{[ f32(30), 70, 110,150, ]!}
// Rotation
// println("*** Rotation ****")
rotx := m4.rotate(m4.rad(-90), m4.Vec4{ e: [f32(1.0), 0, 0, 0]! }).clean()
roty := m4.rotate(m4.rad(-90), m4.Vec4{ e: [f32(0), 1.0, 0, 0]! }).clean()
rotz := m4.rotate(m4.rad(-90), m4.Vec4{ e: [f32(0), 0, 1, 0]! }).clean()
// println( rotx )
// println( roty )
// println( rotz )
// println( m4.mul_vec(rotx, m4.Vec4{e:[f32(0),0,1,0]!}).clean())
assert m4.mul_vec(roty, m4.Vec4{ e: [f32(1.0), 0.0, 0, 0]! }).clean() == m4.Vec4{ e: [f32(0), 0.0, -1, 0]! }
assert m4.mul_vec(rotz, m4.Vec4{ e: [f32(1.0), 0.0, 0, 0]! }).clean() == m4.Vec4{ e: [f32(0), 1, 0, 0]! }
assert m4.mul_vec(rotx, m4.Vec4{ e: [f32(0), 0, 1, 0]! }).clean() == m4.Vec4{ e: [f32(0), -1, 0, 0]! }
// println("****************")
}
fn test_proj() {
ort := m4.ortho(0,300,0,200,0,0)
assert m4.mul_vec(ort, m4.Vec4{[ f32(150), 100, 0, 1]!}).is_equal(m4.Vec4{[ f32(0), 0, 0, 1]!})
assert m4.mul_vec(ort, m4.Vec4{[ f32(0), 0, 0, 1]!}).is_equal(m4.Vec4{[ f32(-1), -1, 0, 1]!})
assert m4.mul_vec(ort, m4.Vec4{[ f32(300), 200, 0, 1]!}).is_equal(m4.Vec4{[ f32(1), 1, 0, 1]!})
}
// vfmt on
+590
View File
@@ -0,0 +1,590 @@
/**********************************************************************
*
* Simply vector/matrix utility
*
* Copyright (c) 2021 Dario Deledda. All rights reserved.
* Use of this source code is governed by an MIT license
* that can be found in the LICENSE file.
*
* TODO:
**********************************************************************/
module m4
import math
pub union Mat4 {
pub mut:
e [16]f32
f [4][4]f32
}
pub const precision = f32(10e-7)
// default precision for the module
/*********************************************************************
*
* Utility
*
*********************************************************************/
// String representation of the matrix
pub fn (x Mat4) str() string {
// vfmt off
return unsafe {
'|${x.e[0]: -6.3},${x.e[1]: -6.3},${x.e[2]: -6.3},${x.e[3]: -6.3}|\n' +
'|${x.e[4]: -6.3},${x.e[5]: -6.3},${x.e[6]: -6.3},${x.e[7]: -6.3}|\n' +
'|${x.e[8]: -6.3},${x.e[9]: -6.3},${x.e[10]:-6.3},${x.e[11]:-6.3}|\n' +
'|${x.e[12]:-6.3},${x.e[13]:-6.3},${x.e[14]:-6.3},${x.e[15]:-6.3}|'
}
// vfmt on
}
// Remove all the raw zeros
@[direct_array_access]
pub fn (a Mat4) clean() Mat4 {
unsafe {
x := Mat4{}
for c, value in a.e {
if f32_abs(value) < precision {
x.e[c] = 0
} else {
x.e[c] = value
}
}
return x
}
return zero_m4() // workaround -cstrict
}
// Sum all the elements of the matrix
pub fn (x Mat4) sum_all() f32 {
mut res := f32(0)
for v in unsafe { x.e } {
res += v
}
return res
}
// Check if two matrix are equal using module precision
@[direct_array_access]
pub fn (x Mat4) is_equal(y Mat4) bool {
unsafe {
for c, value in x.e {
if f32_abs(value - y.e[c]) > precision {
return false
}
}
return true
}
}
//-------------------------------------
// Set/Get values
//-------------------------------------
// Get an element of the matrix using [0..15] indexes, one dimension
pub fn (x Mat4) get_e(elem_index int) f32 {
unsafe {
return x.e[elem_index]
}
}
// Get an element of the matrix using [0..3][0..3] indexes, two dimension
pub fn (x Mat4) get_f(index_col int, index_row int) f32 {
unsafe {
return x.e[int(u32(index_row) << 2) + index_col]
}
}
// Set an element of the matrix using [0..15] indexes, one dimension
pub fn (mut x Mat4) set_e(index int, value f32) {
unsafe {
x.e[index] = value
}
}
// Set an element of the matrix using [0..3][0..3] indexes, two dimension
pub fn (mut x Mat4) set_f(index_col int, index_row int, value f32) {
unsafe {
x.e[int(u32(index_row) << 2) + index_col] = value
}
}
// Copy a matrix elements from another matrix
pub fn (mut x Mat4) copy(y Mat4) {
unsafe {
x.e = [
// vfmt off
y.e[0 ], y.e[1 ], y.e[2 ], y.e[3 ],
y.e[4 ], y.e[5 ], y.e[6 ], y.e[7 ],
y.e[8 ], y.e[9 ], y.e[10], y.e[11],
y.e[12], y.e[13], y.e[14], y.e[15],
// vfmt on
]!
}
}
// Set the trace of the matrix using a vec4
pub fn (mut x Mat4) set_trace(v3 Vec4) {
unsafe {
// vfmt off
x.e[0 ] = v3.e[0]
x.e[5 ] = v3.e[1]
x.e[10] = v3.e[2]
x.e[15] = v3.e[3]
// vfmt on
}
}
// Get the trace of the matrix
pub fn (x Mat4) get_trace() Vec4 {
unsafe {
// vfmt off
return Vec4{ e: [ x.e[0], x.e[5], x.e[10], x.e[15], ]! }
// vfmt on
}
}
// Set all the matrix elements to value
pub fn (mut x Mat4) set_f32(value f32) {
unsafe {
// vfmt off
x.e = [
value, value, value, value,
value, value, value, value,
value, value, value, value,
value, value, value, value,
]!
// vfmt on
}
}
//-------------------------------------
// Rows/Column access
//-------------------------------------
// Set the row as the input vec4
@[direct_array_access; unsafe]
pub fn (mut x Mat4) set_row(row int, v3 Vec4) {
unsafe {
x.e[row * 4 + 0] = v3.e[0]
x.e[row * 4 + 1] = v3.e[1]
x.e[row * 4 + 2] = v3.e[2]
x.e[row * 4 + 3] = v3.e[3]
}
}
// Get a row from a matrix
@[direct_array_access; unsafe]
pub fn (x Mat4) get_row(row int) Vec4 {
unsafe {
// vfmt off
return Vec4{ e: [ x.e[row * 4], x.e[row * 4 + 1], x.e[row * 4 + 2], x.e[row * 4 + 3], ]! }
// vfmt on
}
}
// Set the column as the input vec4
@[direct_array_access; unsafe]
pub fn (mut x Mat4) set_col(col int, v3 Vec4) {
unsafe {
// vfmt off
x.e[col ] = v3.e[0]
x.e[col + 4 ] = v3.e[1]
x.e[col + 8 ] = v3.e[2]
x.e[col + 12] = v3.e[3]
// vfmt on
}
}
// Get a column from a matrix
@[direct_array_access; unsafe]
pub fn (x Mat4) get_col(col int) Vec4 {
unsafe {
// vfmt off
return Vec4{ e: [ x.e[col], x.e[col + 4 ], x.e[col + 8 ], x.e[col + 12], ]!}
// vfmt on
}
}
// Swap two columns in the matrix
@[direct_array_access; unsafe]
pub fn (mut x Mat4) swap_col(col1 int, col2 int) {
unsafe {
// vfmt off
v0 := x.e[col1 ]
v1 := x.e[col1 + 4 ]
v2 := x.e[col1 + 8 ]
v3 := x.e[col1 + 12]
x.e[col1 ] = x.e[col2 ]
x.e[col1 + 4 ] = x.e[col2 + 4 ]
x.e[col1 + 8 ] = x.e[col2 + 8 ]
x.e[col1 + 12] = x.e[col2 + 12]
x.e[col2 ] = v0
x.e[col2 + 4 ] = v1
x.e[col2 + 8 ] = v2
x.e[col2 + 12] = v3
// vfmt on
}
}
// Swap two rows in the matrix
@[direct_array_access; unsafe]
pub fn (mut x Mat4) swap_row(row1 int, row2 int) {
unsafe {
v0 := x.e[row1 * 4 + 0]
v1 := x.e[row1 * 4 + 1]
v2 := x.e[row1 * 4 + 2]
v3 := x.e[row1 * 4 + 3]
x.e[row1 * 4 + 0] = x.e[row2 * 4 + 0]
x.e[row1 * 4 + 1] = x.e[row2 * 4 + 1]
x.e[row1 * 4 + 2] = x.e[row2 * 4 + 2]
x.e[row1 * 4 + 3] = x.e[row2 * 4 + 3]
x.e[row2 * 4 + 0] = v0
x.e[row2 * 4 + 1] = v1
x.e[row2 * 4 + 2] = v2
x.e[row2 * 4 + 3] = v3
}
}
//-------------------------------------
// Modify data
//-------------------------------------
// Transpose the matrix
pub fn (x Mat4) transpose() Mat4 {
// vfmt off
return unsafe {
Mat4{ e: [
x.e[0 ], x.e[4 ], x.e[8 ], x.e[12],
x.e[1 ], x.e[5 ], x.e[9 ], x.e[13],
x.e[2 ], x.e[6 ], x.e[10], x.e[14],
x.e[3 ], x.e[7 ], x.e[11], x.e[15],
]!}
}
// vfmt on
}
// Multiply the all the elements of the matrix by a scalar
pub fn (x Mat4) mul_scalar(s f32) Mat4 {
// vfmt off
return unsafe {
Mat4{ e: [
x.e[0 ] * s, x.e[1 ] * s, x.e[2 ] * s, x.e[3 ] * s,
x.e[4 ] * s, x.e[5 ] * s, x.e[6 ] * s, x.e[7 ] * s,
x.e[8 ] * s, x.e[9 ] * s, x.e[10] * s, x.e[11] * s,
x.e[12] * s, x.e[13] * s, x.e[14] * s, x.e[15] * s,
]!}
}
// vfmt on
}
/*********************************************************************
*
* Init/set
*
*********************************************************************/
// Return a zero matrix
pub fn zero_m4() Mat4 {
// vfmt off
return Mat4{ e: [
f32(0), 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0,
]!}
// vfmt on
}
// Return a unity matrix
pub fn unit_m4() Mat4 {
// vfmt off
return Mat4{ e: [
f32(1), 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
]!}
// vfmt on
}
// Return a matrix initialized with value
pub fn set_m4(value f32) Mat4 {
// vfmt off
return Mat4{ e: [
value, value, value, value,
value, value, value, value,
value, value, value, value,
value, value, value, value,
]!}
// vfmt on
}
/*********************************************************************
*
* Math
*
*********************************************************************/
// Sum of matrix, operator +
pub fn (a Mat4) + (b Mat4) Mat4 {
// vfmt off
return unsafe {
Mat4{ e: [
a.e[0 ] + b.e[0 ], a.e[1 ] + b.e[1 ], a.e[2 ] + b.e[2 ], a.e[3 ] + b.e[3 ],
a.e[4 ] + b.e[4 ], a.e[5 ] + b.e[5 ], a.e[6 ] + b.e[6 ], a.e[7 ] + b.e[7 ],
a.e[8 ] + b.e[8 ], a.e[9 ] + b.e[9 ], a.e[10] + b.e[10], a.e[11] + b.e[11],
a.e[12] + b.e[12], a.e[13] + b.e[13], a.e[14] + b.e[14], a.e[15] + b.e[15],
]!}
}
// vfmt on
}
// Subtraction of matrix, operator -
pub fn (a Mat4) - (b Mat4) Mat4 {
// vfmt off
return unsafe {
Mat4{ e: [
a.e[0 ] - b.e[0 ], a.e[1 ] - b.e[1 ], a.e[2 ] - b.e[2 ], a.e[3 ] - b.e[3 ],
a.e[4 ] - b.e[4 ], a.e[5 ] - b.e[5 ], a.e[6 ] - b.e[6 ], a.e[7 ] - b.e[7 ],
a.e[8 ] - b.e[8 ], a.e[9 ] - b.e[9 ], a.e[10] - b.e[10], a.e[11] - b.e[11],
a.e[12] - b.e[12], a.e[13] - b.e[13], a.e[14] - b.e[14], a.e[15] - b.e[15],
]!}
}
// vfmt on
}
// Multiplication of matrix, operator *
pub fn (a Mat4) * (b Mat4) Mat4 {
// vfmt off
return unsafe {
Mat4{ e: [
a.f[0][0] * b.f[0][0] + a.f[0][1] * b.f[1][0] + a.f[0][2] * b.f[2][0] + a.f[0][3] * b.f[3][0], // [0][0]
a.f[0][0] * b.f[0][1] + a.f[0][1] * b.f[1][1] + a.f[0][2] * b.f[2][1] + a.f[0][3] * b.f[3][1], // [0][1]
a.f[0][0] * b.f[0][2] + a.f[0][1] * b.f[1][2] + a.f[0][2] * b.f[2][2] + a.f[0][3] * b.f[3][2], // [0][2]
a.f[0][0] * b.f[0][3] + a.f[0][1] * b.f[1][3] + a.f[0][2] * b.f[2][3] + a.f[0][3] * b.f[3][3], // [0][3]
a.f[1][0] * b.f[0][0] + a.f[1][1] * b.f[1][0] + a.f[1][2] * b.f[2][0] + a.f[1][3] * b.f[3][0], // [1][0]
a.f[1][0] * b.f[0][1] + a.f[1][1] * b.f[1][1] + a.f[1][2] * b.f[2][1] + a.f[1][3] * b.f[3][1], // [1][1]
a.f[1][0] * b.f[0][2] + a.f[1][1] * b.f[1][2] + a.f[1][2] * b.f[2][2] + a.f[1][3] * b.f[3][2], // [1][2]
a.f[1][0] * b.f[0][3] + a.f[1][1] * b.f[1][3] + a.f[1][2] * b.f[2][3] + a.f[1][3] * b.f[3][3], // [1][3]
a.f[2][0] * b.f[0][0] + a.f[2][1] * b.f[1][0] + a.f[2][2] * b.f[2][0] + a.f[2][3] * b.f[3][0], // [2][0]
a.f[2][0] * b.f[0][1] + a.f[2][1] * b.f[1][1] + a.f[2][2] * b.f[2][1] + a.f[2][3] * b.f[3][1], // [2][1]
a.f[2][0] * b.f[0][2] + a.f[2][1] * b.f[1][2] + a.f[2][2] * b.f[2][2] + a.f[2][3] * b.f[3][2], // [2][2]
a.f[2][0] * b.f[0][3] + a.f[2][1] * b.f[1][3] + a.f[2][2] * b.f[2][3] + a.f[2][3] * b.f[3][3], // [2][3]
a.f[3][0] * b.f[0][0] + a.f[3][1] * b.f[1][0] + a.f[3][2] * b.f[2][0] + a.f[3][3] * b.f[3][0], // [3][0]
a.f[3][0] * b.f[0][1] + a.f[3][1] * b.f[1][1] + a.f[3][2] * b.f[2][1] + a.f[3][3] * b.f[3][1], // [3][1]
a.f[3][0] * b.f[0][2] + a.f[3][1] * b.f[1][2] + a.f[3][2] * b.f[2][2] + a.f[3][3] * b.f[3][2], // [3][2]
a.f[3][0] * b.f[0][3] + a.f[3][1] * b.f[1][3] + a.f[3][2] * b.f[2][3] + a.f[3][3] * b.f[3][3], // [3][3]
]!}
}
// vfmt on
}
// Sum of matrix function
pub fn add(a Mat4, b Mat4) Mat4 {
return a + b
}
// Subtraction of matrix function
pub fn sub(a Mat4, b Mat4) Mat4 {
return a - b
}
// Multiplication of matrix function
pub fn mul(a Mat4, b Mat4) Mat4 {
return a * b
}
// Multiply a Matrix by a vector
pub fn mul_vec(a Mat4, v Vec4) Vec4 {
// vfmt off
return unsafe {
Vec4{ e: [
a.e[0 ] * v.e[0] + a.e[1 ] * v.e[1] + a.e[2 ] * v.e[2] + a.e[3 ] * v.e[3],
a.e[4 ] * v.e[0] + a.e[5 ] * v.e[1] + a.e[6 ] * v.e[2] + a.e[7 ] * v.e[3],
a.e[8 ] * v.e[0] + a.e[9 ] * v.e[1] + a.e[10] * v.e[2] + a.e[11] * v.e[3],
a.e[12] * v.e[0] + a.e[13] * v.e[1] + a.e[14] * v.e[2] + a.e[15] * v.e[3],
]!}
}
// vfmt on
}
// Calculate the determinant of the Matrix
pub fn det(x Mat4) f32 {
unsafe {
mut t := [6]f32{}
x00 := x.f[0][0]
x10 := x.f[1][0]
x20 := x.f[2][0]
x30 := x.f[3][0]
x01 := x.f[0][1]
x11 := x.f[1][1]
x21 := x.f[2][1]
x31 := x.f[3][1]
x02 := x.f[0][2]
x12 := x.f[1][2]
x22 := x.f[2][2]
x32 := x.f[3][2]
x03 := x.f[0][3]
x13 := x.f[1][3]
x23 := x.f[2][3]
x33 := x.f[3][3]
t[0] = x22 * x33 - x23 * x32
t[1] = x12 * x33 - x13 * x32
t[2] = x12 * x23 - x13 * x22
t[3] = x02 * x33 - x03 * x32
t[4] = x02 * x23 - x03 * x22
t[5] = x02 * x13 - x03 * x12
// vfmt off
return
x00 * (x11 * t[0] - x21 * t[1] + x31 * t[2]) -
x10 * (x01 * t[0] - x21 * t[3] + x31 * t[4]) +
x20 * (x01 * t[1] - x11 * t[3] + x31 * t[5]) -
x30 * (x01 * t[2] - x11 * t[4] + x21 * t[5])
// vfmt on
}
return 0 // workaround -cstrict
}
// Calculate the inverse of the Matrix
pub fn (x Mat4) inverse() Mat4 {
unsafe {
mut t := [6]f32{}
mut inv_det := f32(0)
// vfmt off
a := x.f[0][0]
b := x.f[1][0]
c := x.f[2][0]
d := x.f[3][0]
e := x.f[0][1]
f := x.f[1][1]
g := x.f[2][1]
h := x.f[3][1]
i := x.f[0][2]
j := x.f[1][2]
k := x.f[2][2]
l := x.f[3][2]
m := x.f[0][3]
n := x.f[1][3]
o := x.f[2][3]
p := x.f[3][3]
t[0] = k * p - o * l
t[1] = j * p - n * l
t[2] = j * o - n * k
t[3] = i * p - m * l
t[4] = i * o - m * k
t[5] = i * n - m * j
mut dest := Mat4{}
dest.f[0][0] = f * t[0] - g * t[1] + h * t[2]
dest.f[0][1] = -(e * t[0] - g * t[3] + h * t[4])
dest.f[0][2] = e * t[1] - f * t[3] + h * t[5]
dest.f[0][3] = -(e * t[2] - f * t[4] + g * t[5])
dest.f[1][0] = -(b * t[0] - c * t[1] + d * t[2])
dest.f[1][1] = a * t[0] - c * t[3] + d * t[4]
dest.f[1][2] = -(a * t[1] - b * t[3] + d * t[5])
dest.f[1][3] = a * t[2] - b * t[4] + c * t[5]
t[0] = g * p - o * h
t[1] = f * p - n * h
t[2] = f * o - n * g
t[3] = e * p - m * h
t[4] = e * o - m * g
t[5] = e * n - m * f
dest.f[2][0] = b * t[0] - c * t[1] + d * t[2]
dest.f[2][1] = -(a * t[0] - c * t[3] + d * t[4])
dest.f[2][2] = a * t[1] - b * t[3] + d * t[5]
dest.f[2][3] = -(a * t[2] - b * t[4] + c * t[5])
t[0] = g * l - k * h
t[1] = f * l - j * h
t[2] = f * k - j * g
t[3] = e * l - i * h
t[4] = e * k - i * g
t[5] = e * j - i * f
dest.f[3][0] = -(b * t[0] - c * t[1] + d * t[2])
dest.f[3][1] = a * t[0] - c * t[3] + d * t[4]
dest.f[3][2] = -(a * t[1] - b * t[3] + d * t[5])
dest.f[3][3] = a * t[2] - b * t[4] + c * t[5]
// vfmt on
tmp := (a * dest.f[0][0] + b * dest.f[0][1] + c * dest.f[0][2] + d * dest.f[0][3])
if tmp != 0 {
inv_det = f32(1.0) / tmp
}
return dest.mul_scalar(inv_det)
}
return zero_m4()
}
/*********************************************************************
*
* Transformations
*
*********************************************************************/
// Get a rotation matrix using w as rotation axis vector, the angle is in radians
pub fn rotate(angle f32, w Vec4) Mat4 {
cs := f32(math.cos(angle))
sn := f32(math.sin(angle))
cv := f32(1.0) - cs
axis := w.normalize3()
unsafe {
ax := axis.e[0]
ay := axis.e[1]
az := axis.e[2]
// vfmt off
return Mat4{ e: [
(ax * ax * cv) + cs, (ax * ay * cv) + az * sn, (ax * az * cv) - ay * sn, 0,
(ay * ax * cv) - az * sn, (ay * ay * cv) + cs, (ay * az * cv) + ax * sn, 0,
(az * ax * cv) + ay * sn, (az * ay * cv) - ax * sn, (az * az * cv) + cs, 0,
0, 0, 0, 1,
]!}
// vfmt on
}
return zero_m4()
}
/*********************************************************************
*
* Graphic
*
*********************************************************************/
// Get a matrix translated by a vector w
pub fn (x Mat4) translate(w Vec4) Mat4 {
// vfmt off
return unsafe {
Mat4{ e: [
x.e[0], x.e[1], x.e[2 ], x.e[3 ],
x.e[4], x.e[5], x.e[6 ], x.e[7 ],
x.e[8], x.e[9], x.e[10], x.e[11],
x.e[12] + w.e[0], x.e[13] + w.e[1], x.e[14] + w.e[2], x.e[15],
]!}
}
// vfmt on
}
// Get a scale matrix, the scale vector is w, only xyz are evaluated.
pub fn scale(w Vec4) Mat4 {
// vfmt off
return unsafe {
Mat4{ e: [
w.e[0], 0, 0, 0,
0, w.e[1], 0, 0,
0, 0, w.e[2], 0,
0, 0, 0, 1,
]!}
}
// vfmt on
}
+250
View File
@@ -0,0 +1,250 @@
/**********************************************************************
* Simple vector/matrix utility
* Copyright (c) 2024 Dario Deledda. All rights reserved.
* Use of this source code is governed by an MIT license
* that can be found in the LICENSE file.
**********************************************************************/
module m4
import math
pub struct Vec4 {
pub mut:
e [4]f32
}
// str returns a `string` representation of `Vec4`.
pub fn (x Vec4) str() string {
return '|${x.e[0]:-6.3},${x.e[1]:-6.3},${x.e[2]:-6.3},${x.e[3]:-6.3}|'
}
// vec3 creates a Vec4 value, passing x,y,z as parameters. The w element is set to 1
@[inline]
pub fn vec3(x f32, y f32, z f32) Vec4 {
return Vec4{
e: [x, y, z, 1]!
}
}
// vec4 creates a Vec4 value, based on the x,y,z,w parameters
@[inline]
pub fn vec4(x f32, y f32, z f32, w f32) Vec4 {
return Vec4{
e: [x, y, z, w]!
}
}
// is_equal checks if two vector are equal using the module precision (10e-7)
@[direct_array_access]
pub fn (x Vec4) is_equal(y Vec4) bool {
unsafe {
for c, value in x.e {
if f32_abs(value - y.e[c]) > precision {
return false
}
}
return true
}
}
// clean returns a new vector, based on `x`, but with all the values < precision, set to 0
@[direct_array_access]
pub fn (x Vec4) clean() Vec4 {
mut n := x
for c, value in x.e {
if f32_abs(value) < precision {
n.e[c] = 0
}
}
return n
}
// copy sets all elements of `x` to `value`
pub fn (mut x Vec4) copy(value f32) {
x.e = [value, value, value, value]!
}
// mul_scalar returns the result of multiplying the vector `x`, by the scalar `value`
@[inline]
pub fn (x Vec4) mul_scalar(value f32) Vec4 {
return Vec4{
e: [x.e[0] * value, x.e[1] * value, x.e[2] * value, x.e[3] * value]!
}
}
// inv returns the reciprocal of the vector `x`
pub fn (x Vec4) inv() Vec4 {
return Vec4{
e: [
if x.e[0] != 0 { 1.0 / x.e[0] } else { 0 },
if x.e[1] != 0 { 1.0 / x.e[1] } else { 0 },
if x.e[2] != 0 { 1.0 / x.e[2] } else { 0 },
if x.e[3] != 0 { 1.0 / x.e[3] } else { 0 },
]!
}
}
// normalize returns a normalized form of the vector `x`
pub fn (x Vec4) normalize() Vec4 {
m := x.mod()
if m == 0 {
return zero_v4()
}
return Vec4{
e: [
x.e[0] * (1 / m),
x.e[1] * (1 / m),
x.e[2] * (1 / m),
x.e[3] * (1 / m),
]!
}
}
// normalize3 returns a normalized form of the vector `x`, where the w element is set to 0
pub fn (x Vec4) normalize3() Vec4 {
m := x.mod3()
if m == 0 {
return zero_v4()
}
return Vec4{
e: [
x.e[0] * (1 / m),
x.e[1] * (1 / m),
x.e[2] * (1 / m),
0,
]!
}
}
// mod returns a module of the vector `x`
@[inline]
pub fn (x Vec4) mod() f32 {
return math.sqrtf(x.e[0] * x.e[0] + x.e[1] * x.e[1] + x.e[2] * x.e[2] + x.e[3] * x.e[3])
}
// mod3 returns a module of the 3d vector `x`, ignoring the value of its w element
@[inline]
pub fn (x Vec4) mod3() f32 {
return math.sqrtf(x.e[0] * x.e[0] + x.e[1] * x.e[1] + x.e[2] * x.e[2])
}
/*********************************************************************
* Math
*********************************************************************/
// zero_v4 returns a zero vector (all elements set to 0)
@[inline]
pub fn zero_v4() Vec4 {
return Vec4{
e: [
f32(0),
0,
0,
0,
]!
}
}
// one_v4 returns a vector, where all elements are set to 1
@[inline]
pub fn one_v4() Vec4 {
return Vec4{
e: [
f32(1),
1,
1,
1,
]!
}
}
// blank_v4 returns a vector, where all elements are set to 0, except `w`, which is set to 1
pub fn blank_v4() Vec4 {
return Vec4{
e: [
f32(0),
0,
0,
1,
]!
}
}
// set_v4 returns a vector, where all elements are set to `value`
@[inline]
pub fn set_v4(value f32) Vec4 {
return Vec4{
e: [
value,
value,
value,
value,
]!
}
}
// sum returns a sum of all the elements
@[inline]
pub fn (x Vec4) sum() f32 {
return x.e[0] + x.e[1] + x.e[2] + x.e[3]
}
/*********************************************************************
* Operators
*********************************************************************/
// + returns `a` + `b` (corresponding elements are added)
@[inline]
pub fn (a Vec4) + (b Vec4) Vec4 {
return Vec4{
e: [
a.e[0] + b.e[0],
a.e[1] + b.e[1],
a.e[2] + b.e[2],
a.e[3] + b.e[3],
]!
}
}
// - returns `a` + `b` (corresponding elements are subtracted)
@[inline]
pub fn (a Vec4) - (b Vec4) Vec4 {
return Vec4{
e: [
a.e[0] - b.e[0],
a.e[1] - b.e[1],
a.e[2] - b.e[2],
a.e[3] - b.e[3],
]!
}
}
// * returns `a` * `b` (corresponding elements are multiplied, then summed), i.e. a dot product
@[inline]
pub fn (a Vec4) * (b Vec4) f32 {
return a.e[0] * b.e[0] + a.e[1] * b.e[1] + a.e[2] * b.e[2] + a.e[3] * b.e[3]
}
// % returns a cross product of the vectors `a` and `b`
@[inline]
pub fn (a Vec4) % (b Vec4) Vec4 {
return Vec4{
e: [
(a.e[1] * b.e[2]) - (a.e[2] * b.e[1]),
(a.e[2] * b.e[0]) - (a.e[0] * b.e[2]),
(a.e[0] * b.e[1]) - (a.e[1] * b.e[0]),
0,
]!
}
}
// mul_vec4 returns a vector, where the corresponding `x` and `y` elements are multiplied
@[inline]
pub fn (x Vec4) mul_vec4(y Vec4) Vec4 {
return Vec4{
e: [
x.e[0] * y.e[0],
x.e[1] * y.e[1],
x.e[2] * y.e[2],
x.e[3] * y.e[3],
]!
}
}
+61
View File
@@ -0,0 +1,61 @@
module gg
import encoding.base64
import sokol.sapp
import os
fn screenshot_stdout_payload(frame u64, png []u8) string {
return '${gg_record_stdout_prefix} frame=${frame} format=png encoding=base64 data=${base64.encode(png)}'
}
fn emit_recorded_frame_to_stdout(frame u64) ! {
screenshot_file_path := os.join_path(os.vtmp_dir(),
'vgg_record_stdout_${os.getpid()}_${frame}.png')
sapp.screenshot_png(screenshot_file_path)!
defer {
os.rm(screenshot_file_path) or {}
}
png := os.read_bytes(screenshot_file_path)!
println(screenshot_stdout_payload(frame, png))
flush_stdout()
}
// record_frame records the current frame to a file or stdout.
// record_frame acts according to settings specified in `gg.recorder_settings`.
@[if gg_record ?]
pub fn (mut ctx Context) record_frame() {
if ctx.frame in recorder_settings.screenshot_frames {
match recorder_settings.screenshot_output {
.stdout {
$if gg_record_trace ? {
eprintln('>>> screenshotting frame ${ctx.frame} to stdout')
}
emit_recorded_frame_to_stdout(ctx.frame) or { panic(err) }
}
.file {
screenshot_file_path := '${recorder_settings.screenshot_prefix}${ctx.frame}.png'
$if gg_record_trace ? {
eprintln('>>> screenshotting ${screenshot_file_path}')
}
sapp.screenshot_png(screenshot_file_path) or { panic(err) }
}
}
}
if ctx.frame == recorder_settings.stop_at_frame {
$if gg_record_trace ? {
eprintln('>>> exiting at frame ${ctx.frame}')
}
flush_stdout()
exit(0)
}
}
fn new_gg_recorder_settings() &SSRecorderSettings {
$if gg_record ? {
return new_gg_recorder_settings_from_env(os.environ(), os.executable())
} $else {
return &SSRecorderSettings{}
}
}
const recorder_settings = new_gg_recorder_settings()
+4
View File
@@ -0,0 +1,4 @@
module gg
@[if gg_record ?]
pub fn (mut ctx Context) record_frame() {}
+68
View File
@@ -0,0 +1,68 @@
module gg
import os
enum ScreenshotOutput {
file
stdout
}
const gg_record_stdout_prefix = '__V_GG_IMAGE__'
@[heap]
pub struct SSRecorderSettings {
pub mut:
stop_at_frame i64 = -1
screenshot_frames []u64
screenshot_folder string
screenshot_prefix string
screenshot_output ScreenshotOutput = .file
}
fn parse_screenshot_frames(value string) []u64 {
mut frames := []u64{}
for raw_frame in value.split(',') {
frame := raw_frame.trim_space()
if frame == '' {
continue
}
frames << frame.u64()
}
return frames
}
fn parse_screenshot_output(value string) ScreenshotOutput {
return match value.trim_space().to_lower() {
'stdout' { .stdout }
else { .file }
}
}
fn recorder_env_value(env map[string]string, key string, fallback string) string {
value := env[key]
if value == '' {
return fallback
}
return value
}
fn new_gg_recorder_settings_from_env(env map[string]string, executable string) &SSRecorderSettings {
mut stop_frame := recorder_env_value(env, 'VGG_STOP_AT_FRAME', '-1').i64()
mut frames := parse_screenshot_frames(env['VGG_SCREENSHOT_FRAMES'])
folder := env['VGG_SCREENSHOT_FOLDER']
output := parse_screenshot_output(env['VGG_SCREENSHOT_OUTPUT'])
if output == .stdout && frames.len == 0 {
frames << u64(1)
}
if output == .stdout && stop_frame < 0 && frames.len > 0 {
stop_frame = i64(frames[frames.len - 1])
}
prefix := os.join_path_single(folder, os.file_name(executable).all_before('.') + '_')
return &SSRecorderSettings{
stop_at_frame: stop_frame
screenshot_frames: frames
screenshot_folder: folder
screenshot_prefix: prefix
screenshot_output: output
}
}
+49
View File
@@ -0,0 +1,49 @@
// vtest build: macos && !sanitized_job?
import os
import stbi
@[direct_array_access]
fn png_has_non_black_pixels(path string) !bool {
img := stbi.load(path)!
defer {
img.free()
}
for i in 0 .. img.width * img.height {
unsafe {
if img.data[i * 4] != 0 || img.data[i * 4 + 1] != 0 || img.data[i * 4 + 2] != 0 {
return true
}
}
}
return false
}
fn test_streaming_r8_zero_buffer_stays_black_on_gl_backend() {
vexe := @VEXE
vroot := os.dir(vexe)
sample_path := os.join_path(vroot, 'vlib/gg/testdata/streaming_r8_zero_buffer_issue_10989.vv')
temp_dir := os.join_path(os.temp_dir(), 'v_gg_issue_10989_${os.getpid()}')
os.mkdir_all(temp_dir) or { panic(err) }
defer {
os.rmdir_all(temp_dir) or {}
}
exe_path := os.join_path(temp_dir, 'issue10989_repro')
// Force the GL backend so this exercises the packed texture upload path from issue #10989.
compile_cmd := '${os.quoted_path(vexe)} -d gg_record -d darwin_sokol_glcore33 -o ${os.quoted_path(exe_path)} ${os.quoted_path(sample_path)}'
compile_res := os.execute(compile_cmd)
assert compile_res.exit_code == 0, compile_res.output
run_cmd := 'VGG_STOP_AT_FRAME=2 VGG_SCREENSHOT_FRAMES=2 VGG_SCREENSHOT_FOLDER=${os.quoted_path(temp_dir)} ${os.quoted_path(exe_path)}'
run_res := os.execute(run_cmd)
if run_res.exit_code != 0 {
if run_res.output.contains('glpixelformat') || run_res.exit_code == 134 {
// OpenGL context creation failed (e.g. headless CI runners without GPU).
// Skip the test rather than failing.
eprintln('skipping: GL context creation failed on this system')
return
}
assert run_res.exit_code == 0, run_res.output
}
screenshot_path := os.join_path(temp_dir, 'issue10989_repro_2.png')
assert os.exists(screenshot_path)
assert png_has_non_black_pixels(screenshot_path)! == false
}
+16
View File
@@ -0,0 +1,16 @@
module main
import math
import gg
gg.start(
window_title: 'Arcs'
width: 200
height: 200
frame_fn: fn (mut ctx gg.Context) {
ctx.begin()
ctx.draw_arc_filled(100, 100, 35, 45, 0, f32(math.radians(290)), 30, gg.red)
ctx.draw_arc_empty(100, 100, 30, 50, 0, f32(math.radians(290)), 30, gg.white)
ctx.end()
}
)
+18
View File
@@ -0,0 +1,18 @@
module main
import gg
gg.start(
width: 300
height: 300
window_title: 'Circles'
frame_fn: fn (mut ctx gg.Context) {
ctx.begin()
ctx.draw_circle_empty(150, 150, 80, gg.blue)
ctx.draw_circle_filled(150, 150, 40, gg.yellow)
ctx.draw_circle_line(150, 150, 80, 6, gg.red)
ctx.draw_circle_line(150, 150, 120, 6, gg.green)
ctx.draw_circle_line(150, 150, 150, 8, gg.white)
ctx.end()
}
)
+23
View File
@@ -0,0 +1,23 @@
import gg
const thickness = f32(5)
gg.start(
window_title: 'Ellipses'
width: 600
height: 400
frame_fn: fn (mut ctx gg.Context) {
ctx.begin()
drot := f32(ctx.frame) / 60
ctx.draw_ellipse_filled(100, 100, 100, 50, gg.red)
ctx.draw_ellipse_empty(100, 100, 50, 25, gg.black)
ctx.draw_ellipse_filled_rotate(300, 100, 100, 50, -drot, gg.blue)
ctx.draw_ellipse_empty_rotate(300, 100, 50, 25, -drot, gg.black)
ctx.draw_ellipse_filled_rotate(500, 100, 100, 50, drot, gg.yellow)
ctx.draw_ellipse_empty_rotate(500, 100, 50, 25, drot, gg.black)
ctx.draw_ellipse_thick(100, 300, 100, 50, thickness, gg.green)
ctx.draw_ellipse_thick_rotate(300, 300, 100, 50, thickness, drot, gg.purple)
ctx.draw_ellipse_thick_rotate(500, 300, 100, 50, thickness, -drot, gg.indigo)
ctx.end()
}
)
+18
View File
@@ -0,0 +1,18 @@
module main
import gg
gg.start(
bg_color: gg.rgb(174, 198, 255)
width: 600
height: 400
window_title: 'Polygons'
frame_fn: fn (mut ctx gg.Context) {
ctx.begin()
ctx.draw_convex_poly([f32(100.0), 100.0, 200.0, 100.0, 300.0, 200.0, 200.0, 300.0, 100.0,
300.0], gg.blue)
ctx.draw_poly_empty([f32(50.0), 50.0, 70.0, 60.0, 90.0, 80.0, 70.0, 110.0], gg.black)
ctx.draw_triangle_filled(450, 142, 530, 280, 370, 280, gg.red)
ctx.end()
}
)
+23
View File
@@ -0,0 +1,23 @@
module main
import gg
gg.start(
bg_color: gg.white
window_title: 'Rectangles'
width: 250
height: 100
frame_fn: fn (ctx &gg.Context) {
ctx.begin()
ctx.draw_rect_filled(10, 10, 10, 10, gg.blue)
ctx.draw_rect_empty(30, 10, 10, 10, gg.red)
ctx.draw_rect_filled(50, 10, 30, 10, gg.green)
ctx.draw_rect_empty(100, 10, 30, 10, gg.black)
ctx.draw_rect_empty(10, 50, 10, 10, gg.blue)
ctx.draw_rect_filled(30, 50, 10, 10, gg.red)
ctx.draw_rect_empty(50, 50, 30, 10, gg.green)
ctx.draw_rect_filled(100, 50, 30, 10, gg.black)
ctx.end()
}
)
+28
View File
@@ -0,0 +1,28 @@
module main
import gg
gg.start(
width: 325
height: 275
window_title: 'Rounded Rectangles'
frame_fn: fn (mut ctx gg.Context) {
ctx.begin()
// these should be rounded rectangles
ctx.draw_rounded_rect_empty(10, 10, 50, 100, 5, gg.blue)
ctx.draw_rounded_rect_empty(25, 25, 50, 100, 15, gg.yellow)
ctx.draw_rounded_rect_empty(50, 50, 50, 100, 50, gg.red)
ctx.draw_rounded_rect_empty(75, 75, 50, 100, 100, gg.green)
ctx.draw_rounded_rect_empty(100, 100, 50, 100, 1000, gg.white)
ctx.draw_rounded_rect_empty(110, 10, 100, 50, 5, gg.blue)
ctx.draw_rounded_rect_empty(125, 25, 100, 50, 15, gg.yellow)
ctx.draw_rounded_rect_empty(150, 50, 100, 50, 50, gg.red)
ctx.draw_rounded_rect_empty(175, 75, 100, 50, 100, gg.green)
ctx.draw_rounded_rect_empty(200, 100, 100, 50, 1000, gg.white)
// this should be a perfect circle
ctx.draw_rounded_rect_empty(10, 200, 50, 50, 1000, gg.magenta)
// this should be a perfect square
ctx.draw_rounded_rect_empty(250, 200, 50, 50, 0, gg.cyan)
ctx.end()
}
)
+28
View File
@@ -0,0 +1,28 @@
module main
import gg
gg.start(
width: 325
height: 275
window_title: 'Rounded Rectangles'
frame_fn: fn (mut ctx gg.Context) {
ctx.begin()
// these should be rounded rectangles
ctx.draw_rounded_rect_filled(10, 10, 50, 100, 5, gg.blue)
ctx.draw_rounded_rect_filled(25, 25, 50, 100, 15, gg.yellow)
ctx.draw_rounded_rect_filled(50, 50, 50, 100, 50, gg.red)
ctx.draw_rounded_rect_filled(75, 75, 50, 100, 100, gg.green)
ctx.draw_rounded_rect_filled(100, 100, 50, 100, 1000, gg.white)
ctx.draw_rounded_rect_filled(110, 10, 100, 50, 5, gg.blue)
ctx.draw_rounded_rect_filled(125, 25, 100, 50, 15, gg.yellow)
ctx.draw_rounded_rect_filled(150, 50, 100, 50, 50, gg.red)
ctx.draw_rounded_rect_filled(175, 75, 100, 50, 100, gg.green)
ctx.draw_rounded_rect_filled(200, 100, 100, 50, 1000, gg.white)
// this should be a perfect circle
ctx.draw_rounded_rect_filled(10, 200, 50, 50, 1000, gg.magenta)
// this should be a perfect square
ctx.draw_rounded_rect_filled(250, 200, 50, 50, 0, gg.cyan)
ctx.end()
}
)
+82
View File
@@ -0,0 +1,82 @@
module main
import gg
struct App {
mut:
gg &gg.Context = unsafe { nil }
rotation f32 = f32(0)
edge int = 3
}
fn main() {
println('rotation: left arrow key, right arrow key')
println('center polygon edge: up arrow key, down arrow key')
mut app := &App{}
app.gg = gg.new_context(
window_title: 'Simple Polygons'
width: 500
height: 500
bg_color: gg.black
event_fn: event
frame_fn: render
user_data: app
)
app.gg.run()
}
fn render(app &App) {
app.gg.begin()
color := gg.Color{
r: 175
g: 0
b: 0
a: 200
}
mut edge := 3
for i := 0; i <= 5; i++ {
for j := 0; j <= 5; j++ {
if i == 0 || j == 0 || i == 5 || j == 5 {
app.gg.draw_polygon_filled(50 + 80 * i, 50 + 80 * j, 30, edge, app.rotation,
color)
edge++
}
}
}
app.gg.draw_polygon_filled(250, 250, 150, app.edge, app.rotation, color)
app.gg.end()
}
fn event(e &gg.Event, mut app App) {
match e.typ {
.key_down {
match e.key_code {
.up {
app.edge++
}
.down {
if app.edge > 3 {
app.edge--
}
}
.right {
app.rotation++
}
.left {
app.rotation--
}
.escape {
app.gg.quit()
}
else {}
}
}
else {}
}
}
+45
View File
@@ -0,0 +1,45 @@
module main
import gg
gg.start(
width: 640
height: 300
window_title: 'Circles'
frame_fn: fn (mut ctx gg.Context) {
ctx.begin()
ctx.draw_text(10, 10, 'This text is at x: 10, y: 10', gg.TextCfg{ color: gg.white, size: 20 })
ctx.draw_text(10, 40, 'This text is at x: 10, y: 40 and bold', gg.TextCfg{
color: gg.white
bold: true
size: 20
})
ctx.draw_text(10, 70, 'This text is at x: 10, y: 70 and italic', gg.TextCfg{
color: gg.white
size: 20
italic: true
})
ctx.draw_text(10, 100, 'This text is at x: 10, y: 110 and mono', gg.TextCfg{
color: gg.white
size: 20
mono: true
})
ctx.draw_text(10, 130, 'This text is at x: 10, y: 130 and twice as big', gg.TextCfg{
color: gg.white
size: 40
})
ctx.draw_text(10, 180, 'This text is at x: 10, y: 180 and red', gg.TextCfg{
color: gg.red
size: 20
})
ctx.draw_text(10, 210, 'This text is at x: 10, y: 210 and blue', gg.TextCfg{
color: gg.blue
size: 20
})
ctx.draw_text(10, 240, 'This text is at x: 10, y: 240 and green', gg.TextCfg{
color: gg.green
size: 20
})
ctx.end()
}
)
+40
View File
@@ -0,0 +1,40 @@
module main
import gg
import sokol.gfx
fn main() {
mut context := gg.new_context(
frame_fn: frame
width: 500
height: 500
)
context.run()
}
fn frame(mut ctx gg.Context) {
ctx.begin()
id := ctx.new_streaming_image(ctx.width, ctx.height, 4, pixel_format: .rgba8)
mut img := ctx.get_cached_image_by_idx(id)
mut bytes := []u8{len: img.width * img.height * 4, cap: img.width * img.height * 4}
for y in 0 .. img.height {
for x in 0 .. img.width {
unsafe {
bytes[(x + img.width * y) * 4] = 100
bytes[(x + img.width * y) * 4 + 1] = 100
bytes[(x + img.width * y) * 4 + 2] = 100
bytes[(x + img.width * y) * 4 + 3] = 255
}
}
}
unsafe {
img.update_pixel_data(&bytes[0])
}
ctx.draw_image(0, 0, ctx.width, ctx.height, img)
ctx.remove_cached_image_by_idx(id)
ctx.end()
gfx.destroy_image(img.simg)
unsafe {
free(&bytes[0])
}
}
@@ -0,0 +1,37 @@
module main
import gg
const win_width = 100
const win_height = 100
struct App {
mut:
gg &gg.Context = unsafe { nil }
image int
}
fn main() {
mut app := &App{}
app.gg = gg.new_context(
width: win_width
height: win_height
window_title: 'issue 10989'
frame_fn: frame
user_data: app
init_fn: initialize
)
app.gg.run()
}
fn initialize(mut app App) {
buffer := []u8{len: 10 * 10}
app.image = app.gg.new_streaming_image(10, 10, 1, pixel_format: .r8)
app.gg.update_pixel_data(app.image, buffer.data)
}
fn frame(mut app App) {
app.gg.begin()
app.gg.draw_image_by_id(0, 0, 10, 10, app.image)
app.gg.end()
}
+48
View File
@@ -0,0 +1,48 @@
module main
import gg
struct App {
mut:
gg &gg.Context = unsafe { nil }
radius f64 = 10.0
}
fn main() {
mut app := &App{}
app.gg = gg.new_context(
bg_color: gg.white
width: 300
height: 300
window_title: 'Circles'
frame_fn: frame
event_fn: on_event
user_data: app
)
app.gg.run()
}
fn on_event(e &gg.Event, mut app App) {
match e.typ {
.key_down {
match e.key_code {
.up {
app.radius += 1
}
.down {
app.radius -= 1
}
else {}
}
}
else {}
}
}
fn frame(mut app App) {
app.gg.begin()
app.gg.draw_circle_empty(150, 150, f32(app.radius), gg.blue)
app.gg.draw_text(20, 20, 'radius: ${app.radius}')
// app.gg.draw_text(20, 50, 'circle_segment_size: ${circle_segment_size}')
app.gg.end()
}
+338
View File
@@ -0,0 +1,338 @@
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license that can be found in the LICENSE file.
module gg
import fontstash
import sokol.sfons
import sokol.sgl
import os
import os.font
pub struct FT {
pub:
fons &fontstash.Context = unsafe { nil }
font_normal int
font_bold int
font_mono int
font_italic int
pub mut:
fonts_map map[string]int // for storing custom fonts, provided via cfg.family in draw_text()
scale f32 = 1.0
}
pub enum HorizontalAlign {
left = C.FONS_ALIGN_LEFT
center = C.FONS_ALIGN_CENTER
right = C.FONS_ALIGN_RIGHT
}
pub enum VerticalAlign {
top = C.FONS_ALIGN_TOP
middle = C.FONS_ALIGN_MIDDLE
bottom = C.FONS_ALIGN_BOTTOM
baseline = C.FONS_ALIGN_BASELINE
}
const initial_text_atlas_size = int($d('gg_text_buff_size', 2048))
const max_text_atlas_size = 8192
fn expand_atlas_callback(uptr voidptr, error int, _val int) {
if error != C.FONS_ATLAS_FULL {
return
}
fons := unsafe { &fontstash.Context(uptr) }
width, height := fons.get_atlas_size()
mut next_width := if width > 0 { width } else { initial_text_atlas_size }
mut next_height := if height > 0 { height } else { initial_text_atlas_size }
if next_width < max_text_atlas_size {
next_width = if next_width * 2 > max_text_atlas_size {
max_text_atlas_size
} else {
next_width * 2
}
}
if next_height < max_text_atlas_size {
next_height = if next_height * 2 > max_text_atlas_size {
max_text_atlas_size
} else {
next_height * 2
}
}
if next_width == width && next_height == height {
return
}
fons.expand_atlas(next_width, next_height)
}
fn new_ft(c FTConfig) ?&FT {
if c.font_path == '' {
if c.bytes_normal.len > 0 {
fons := sfons.create(initial_text_atlas_size, initial_text_atlas_size, 1)
bytes_normal := c.bytes_normal
bytes_bold := if c.bytes_bold.len > 0 {
c.bytes_bold
} else {
debug_font_println('setting bold variant to normal')
bytes_normal
}
bytes_mono := if c.bytes_mono.len > 0 {
c.bytes_mono
} else {
debug_font_println('setting mono variant to normal')
bytes_normal
}
bytes_italic := if c.bytes_italic.len > 0 {
c.bytes_italic
} else {
debug_font_println('setting italic variant to normal')
bytes_normal
}
fons.set_error_callback(expand_atlas_callback, fons)
return &FT{
fons: fons
font_normal: fons.add_font_mem('sans', bytes_normal.clone(), true)
font_bold: fons.add_font_mem('sans', bytes_bold.clone(), true)
font_mono: fons.add_font_mem('sans', bytes_mono.clone(), true)
font_italic: fons.add_font_mem('sans', bytes_italic.clone(), true)
scale: c.scale
}
} else {
// Load default font
}
}
if c.font_path == '' || !os.exists(c.font_path) {
$if !android {
println('failed to load font "${c.font_path}"')
return none
}
}
mut normal_path := c.font_path
mut bytes := []u8{}
$if android {
// First try any filesystem paths
bytes = os.read_bytes(c.font_path) or { []u8{} }
if bytes.len == 0 {
// ... then try the APK asset path
bytes = os.read_apk_asset(c.font_path) or {
println('failed to load font "${c.font_path}"')
return none
}
}
} $else {
bytes = os.read_bytes(c.font_path) or {
println('failed to load font "${c.font_path}"')
return none
}
}
mut bold_path := if c.custom_bold_font_path != '' {
c.custom_bold_font_path
} else {
font.get_path_variant(c.font_path, .bold)
}
bytes_bold := os.read_bytes(bold_path) or {
debug_font_println('failed to load font "${bold_path}"')
bold_path = c.font_path
bytes
}
mut mono_path := font.get_path_variant(c.font_path, .mono)
bytes_mono := os.read_bytes(mono_path) or {
debug_font_println('failed to load font "${mono_path}"')
mono_path = c.font_path
bytes
}
mut italic_path := font.get_path_variant(c.font_path, .italic)
bytes_italic := os.read_bytes(italic_path) or {
debug_font_println('failed to load font "${italic_path}"')
italic_path = c.font_path
bytes
}
fons := sfons.create(initial_text_atlas_size, initial_text_atlas_size, 1)
debug_font_println('Font used for font_normal : ${normal_path}')
debug_font_println('Font used for font_bold : ${bold_path}')
debug_font_println('Font used for font_mono : ${mono_path}')
debug_font_println('Font used for font_italic : ${italic_path}')
fons.set_error_callback(expand_atlas_callback, fons)
return &FT{
fons: fons
font_normal: fons.add_font_mem('sans', bytes.clone(), true)
font_bold: fons.add_font_mem('sans', bytes_bold.clone(), true)
font_mono: fons.add_font_mem('sans', bytes_mono.clone(), true)
font_italic: fons.add_font_mem('sans', bytes_italic.clone(), true)
scale: c.scale
}
}
// set_text_cfg sets the current text configuration
pub fn (ctx &Context) set_text_cfg(cfg TextCfg) {
if !ctx.font_inited {
return
}
if cfg.family != '' {
// println('set text cfg family=${cfg.family}')
mut f := ctx.ft.fonts_map[cfg.family]
if f == 0 {
// No such font in the cache yet, create it
bytes := os.read_bytes(cfg.family) or {
debug_font_println('failed to load font "${cfg.family}"')
return
}
f = ctx.ft.fons.add_font_mem(cfg.family, bytes.clone(), true)
unsafe {
ctx.ft.fonts_map[cfg.family] = f
}
}
ctx.ft.fons.set_font(f)
} else if cfg.bold {
ctx.ft.fons.set_font(ctx.ft.font_bold)
} else if cfg.mono {
ctx.ft.fons.set_font(ctx.ft.font_mono)
} else if cfg.italic {
ctx.ft.fons.set_font(ctx.ft.font_italic)
} else {
ctx.ft.fons.set_font(ctx.ft.font_normal)
}
scale := if ctx.ft.scale == 0 { f32(1) } else { ctx.ft.scale }
size := if cfg.mono { cfg.size - 2 } else { cfg.size }
ctx.ft.fons.set_size(scale * f32(size))
ctx.ft.fons.set_align(int(cfg.align) | int(cfg.vertical_align))
color := sfons.rgba(cfg.color.r, cfg.color.g, cfg.color.b, cfg.color.a)
if cfg.color.a != 255 {
sgl.load_pipeline(ctx.pipeline.alpha)
}
ctx.ft.fons.set_color(color)
ascender := f32(0.0)
descender := f32(0.0)
lh := f32(0.0)
ctx.ft.fons.vert_metrics(&ascender, &descender, &lh)
}
@[params]
pub struct DrawTextParams {
pub:
x int
y int
text string
color Color = black
size int = 16
align HorizontalAlign = .left
vertical_align VerticalAlign = .top
max_width int
family string
bold bool
mono bool
italic bool
}
pub fn (ctx &Context) draw_text2(p DrawTextParams) {
ctx.draw_text(p.x, p.y, p.text, TextCfg{
color: p.color
size: p.size
align: p.align
vertical_align: p.vertical_align
max_width: p.max_width
family: p.family
bold: p.bold
mono: p.mono
italic: p.italic
}) // TODO: perf once it's the only function to draw text
}
// draw_text draws the string in `text_` starting at top-left position `x`,`y`.
// Text settings can be provided with `cfg`.
pub fn (ctx &Context) draw_text(x int, y int, text_ string, cfg TextCfg) {
$if macos {
if ctx.native_rendering {
if cfg.align == align_right {
width := ctx.text_width(text_)
// println('draw text ctx.height = ${ctx.height}')
C.darwin_draw_string(x - width, ctx.height - y, text_, cfg)
} else {
C.darwin_draw_string(x, ctx.height - y, text_, cfg)
}
return
}
}
if !ctx.font_inited {
eprintln('gg: draw_text(): font not initialized')
return
}
// text := text_.trim_space() // TODO: remove/optimize
// mut text := text_
// if text.contains('\t') {
// text = text.replace('\t', ' ')
// }
ctx.set_text_cfg(cfg)
scale := if ctx.ft.scale == 0 { f32(1) } else { ctx.ft.scale }
ctx.ft.fons.draw_text(x * scale, y * scale, text_) // TODO: check offsets/alignment
}
// draw_text draws the string in `text_` starting at top-left position `x`,`y` using
// default text settings.
pub fn (ctx &Context) draw_text_def(x int, y int, text string) {
ctx.draw_text(x, y, text)
}
// flush prepares the font for use.
pub fn (ft &FT) flush() {
sfons.flush(ft.fons)
}
@[inline]
fn (ctx &Context) text_metrics(s string) (f32, [4]f32) {
mut bounds := [4]f32{}
advance := ctx.ft.fons.text_bounds(0, 0, s, &bounds[0])
return advance, bounds
}
// text_width returns the width of the `string` `s` in pixels.
pub fn (ctx &Context) text_width(s string) int {
$if macos {
if ctx.native_rendering {
return C.darwin_text_width(s)
}
}
// ctx.set_text_cfg(cfg) TODO
if !ctx.font_inited {
return 0
}
advance, _ := ctx.text_metrics(s)
return int(advance / ctx.scale)
}
// text_height returns the height of the `string` `s` in pixels.
pub fn (ctx &Context) text_height(s string) int {
// ctx.set_text_cfg(cfg) TODO
if !ctx.font_inited {
return 0
}
_, bounds := ctx.text_metrics(s)
return int((bounds[3] - bounds[1]) / ctx.scale)
}
// text_size returns the width and height of the `string` `s` in pixels.
pub fn (ctx &Context) text_size(s string) (int, int) {
// ctx.set_text_cfg(cfg) TODO
if !ctx.font_inited {
return 0, 0
}
advance, bounds := ctx.text_metrics(s)
return int(advance / ctx.scale), int((bounds[3] - bounds[1]) / ctx.scale)
}
// text_width returns the width of the `string` `s` in pixels.
pub fn (ctx &Context) text_width_f(s string) f32 {
$if macos {
if ctx.native_rendering {
return C.darwin_text_width(s)
}
}
// ctx.set_text_cfg(cfg) TODO
if !ctx.font_inited {
return 0
}
advance, _ := ctx.text_metrics(s)
return advance / ctx.scale
}
+20
View File
@@ -0,0 +1,20 @@
module gg
pub enum HorizontalAlign {
left
center
right
}
pub enum VerticalAlign {
top
middle
bottom
baseline
}
pub fn (mut ctx Context) draw_text(x int, y int, text_ string, cfg TextCfg) {
ctx.context.fillStyle = cfg.color.to_css_string().str
ctx.context.font = cfg.to_css_string().str
ctx.context.fillText(text_.str, JS.Number(x), JS.Number(y))
}
+59
View File
@@ -0,0 +1,59 @@
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license that can be found in the LICENSE file.
module gg
pub const align_right = HorizontalAlign.right
pub const align_left = HorizontalAlign.left
struct FTConfig {
font_path string
custom_bold_font_path string
scale f32 = 1.0
font_size int
bytes_normal []u8
bytes_bold []u8
bytes_mono []u8
bytes_italic []u8
}
struct StringToRender {
x int
y int
text string
cfg TextCfg
}
@[if debug_font ?]
fn debug_font_println(s string) {
println(s)
}
@[markused; params]
pub struct TextCfg {
pub:
color Color = black
size int = 16
align HorizontalAlign = .left
vertical_align VerticalAlign = .top
max_width int
family string
bold bool
mono bool
italic bool
}
// to_css_string returns a CSS compatible string of the TextCfg `cfg`.
// For example: `'mono 14px serif'`.
pub fn (cfg &TextCfg) to_css_string() string {
mut font_style := ''
if cfg.bold {
font_style += 'bold '
}
if cfg.mono {
font_style += 'mono '
}
if cfg.italic {
font_style += 'italic '
}
return '${font_style} ${cfg.size}px ${cfg.family}'
}
+82
View File
@@ -0,0 +1,82 @@
// vtest build: !msvc
// vtest flaky: true
// vtest retry: 1
module gg
import fontstash
const embedded_test_mono_font = $embed_file('@VEXEROOT/examples/assets/fonts/RobotoMono-Regular.ttf')
fn new_test_text_context() (&Context, &fontstash.Context) {
return new_test_text_context_with_atlas(512, 512)
}
fn new_test_text_context_with_atlas(width int, height int) (&Context, &fontstash.Context) {
mut fons := fontstash.create_internal(&C.FONSparams{
width: width
height: height
flags: 0
})
assert fons != unsafe { nil }
font_id := fons.add_font_mem('mono', embedded_test_mono_font.to_bytes().clone(), true)
assert font_id != fontstash.invalid
fons.set_font(font_id)
fons.set_size(16)
fons.set_error_callback(expand_atlas_callback, fons)
return &Context{
ft: &FT{
fons: fons
}
font_inited: true
scale: 1.0
}, fons
}
fn text_advance_and_bounds_width(fons &fontstash.Context, text string) (f32, f32) {
mut bounds := [4]f32{}
advance := fons.text_bounds(0, 0, text, &bounds[0])
return advance, bounds[2] - bounds[0]
}
fn test_text_width_uses_font_advance_for_monospaced_fonts() {
ctx, fons := new_test_text_context()
defer {
fontstash.delete_internal(fons)
}
i_advance, i_bounds_width := text_advance_and_bounds_width(fons, 'i')
m_advance, m_bounds_width := text_advance_and_bounds_width(fons, 'm')
assert int(i_advance / ctx.scale) == int(m_advance / ctx.scale)
assert int(i_bounds_width / ctx.scale) != int(m_bounds_width / ctx.scale)
assert ctx.text_width('i') == int(i_advance / ctx.scale)
assert ctx.text_width('m') == int(m_advance / ctx.scale)
width, height := ctx.text_size('i')
assert width == ctx.text_width('i')
assert height > 0
}
fn test_text_width_counts_trailing_space_advance() {
ctx, fons := new_test_text_context()
defer {
fontstash.delete_internal(fons)
}
advance, bounds_width := text_advance_and_bounds_width(fons, 'a ')
assert int(advance / ctx.scale) > int(bounds_width / ctx.scale)
assert ctx.text_width('a ') == int(advance / ctx.scale)
width, _ := ctx.text_size('a ')
assert width == int(advance / ctx.scale)
}
fn test_text_atlas_expands_on_overflow() {
_, fons := new_test_text_context_with_atlas(64, 64)
defer {
fontstash.delete_internal(fons)
}
before_width, before_height := fons.get_atlas_size()
mut bounds := [4]f32{}
for size in [48, 64, 80] {
fons.set_size(size)
_ = fons.text_bounds(0, 0, 'Some very long text here 0123456789', &bounds[0])
}
after_width, after_height := fons.get_atlas_size()
assert after_width > before_width || after_height > before_height
}