package hello_world_example
import "base:runtime"
import "core:c"
import "core:fmt"
import "core:log"
import "core:math"
import "core:mem"
import "core:strings"
import sdl "vendor:sdl3"
import "vendor:sdl3/image"
import "vendor:sdl3/ttf"
import yiz "yiz:."
import yizw "yiz:widgets"
AppState :: struct {
windowTitle: cstring,
windowSize: yiz.Dimensions,
}
main :: proc() {
// Init tracking allocator
track: mem.Tracking_Allocator
mem.tracking_allocator_init(&track, context.allocator)
context.allocator = mem.tracking_allocator(&track)
defer {
if len(track.allocation_map) > 0 {
fmt.eprintf(
"=== %v allocations not freed: ===\n",
len(track.allocation_map),
)
for _, entry in track.allocation_map {
fmt.eprintf("- %v bytes @ %v\n", entry.size, entry.location)
}
}
fmt.printfln("TOTAL ALLOCATIONS: {0}", track.total_allocation_count)
fmt.printfln(
"TOTAL MEM ALLOCATED: {0} Kb",
track.total_memory_allocated / 1000,
)
fmt.printfln("TOTAL MEM FREED: {0} Kb", track.total_memory_freed / 1000)
fmt.printfln("PEAK MEM: {0} Kb", track.peak_memory_allocated / 1000)
mem.tracking_allocator_destroy(&track)
}
// Init logger
logger := log.create_console_logger(opt = {.Level})
context.logger = logger
defer log.destroy_console_logger(logger)
// Define root window
windowTitle := "Hello World!"
windowSize := yiz.Dimensions{600, 400}
windowPosition := yiz.Vec2{100, 200}
// Init SDL
windowingSystem := createWindowProvider()
// Init renderer
renderer := createRenderer()
// Init UI
yiz.initUI(
context.allocator,
renderer,
windowingSystem,
{title = windowTitle, size = windowSize, position = windowPosition},
)
defer yiz.destroyUI()
// Create window
windowID := yizw.window({.Decorated, .Draggable, .Resizeable})
// Create a container
layout := yizw.DEFAULT_CONTAINER_LAYOUT
layout.sizing = {.FitToContent, .FillAvailable}
containerID := yizw.container(
windowID,
layout = layout,
// fixedSize = yiz.Dimensions{100, 100},
bgColor = {255, 0, 0, 255},
)
log.debug("ID: ", containerID)
// Add button to container
yizw.button(containerID, labelText = "Click Me")
// Main loop
sdlRenderer := (cast(^RendererState)yiz.getRendererState()).sdlRenderer
for !yiz.windowCloseRequested() {
// TODO: Abstract away all of these so you don't need to manually call anything.
// - Add a `beginMainLoop()` proc
mapInputs()
sdl.SetRenderDrawBlendMode(sdlRenderer, {.BLEND})
sdl.SetRenderDrawColor(sdlRenderer, 0, 0, 0, 0)
sdl.RenderClear(sdlRenderer)
yiz.updateUI()
yiz.layoutUI()
// log.debug(cnt.layout.position, cnt.layout.size)
yiz.renderUI()
sdl.RenderPresent(sdlRenderer)
}
}
convertColor :: proc(color: yiz.Color) -> sdl.Color {
return {u8(color.r), u8(color.g), u8(color.b), u8(color.a)}
}
// ------------------------------------------------------------------------
// Inputs
// ------------------------------------------------------------------------
// TODO: Move to windowProvider and call before each loop
mapInputs :: proc() {
// Clear transient input state
yiz.uiCtx.inputState.pointer.pressed = {}
yiz.uiCtx.inputState.pointer.released = {}
event: sdl.Event
for sdl.PollEvent(&event) {
#partial switch event.type {
// ------------------------------------------------------------------------
// Handle quit events
// ------------------------------------------------------------------------
case .QUIT:
fallthrough
case .TERMINATING:
yiz.requestWindowClose()
// ------------------------------------------------------------------------
// Handle window events
// ------------------------------------------------------------------------
case .WINDOW_RESIZED:
// yiz.uiCtx.windowProvider.rootWindow.size.width = f32(event.window.data1)
// yiz.uiCtx.windowProvider.rootWindow.size.height = f32(event.window.data2)
// ------------------------------------------------------------------------
// TODO: Handle keyboard events
// ------------------------------------------------------------------------
case .KEY_DOWN:
if event.key.scancode == .Q {
yiz.requestWindowClose()
}
case .KEY_UP:
// ------------------------------------------------------------------------
// Handle mouse events
// ------------------------------------------------------------------------
case .MOUSE_MOTION:
yiz.setPointerPosition({event.motion.x, event.motion.y})
case .MOUSE_BUTTON_DOWN:
switch event.button.button {
case sdl.BUTTON_LEFT:
yiz.setPointerPressed(.Btn1)
yiz.setPointerHeld(.Btn1, true)
case sdl.BUTTON_RIGHT:
yiz.setPointerPressed(.Btn2)
yiz.setPointerHeld(.Btn2, true)
}
case .MOUSE_BUTTON_UP:
switch event.button.button {
case sdl.BUTTON_LEFT:
yiz.setPointerReleased(.Btn1)
yiz.setPointerHeld(.Btn1, false)
case sdl.BUTTON_RIGHT:
yiz.setPointerReleased(.Btn2)
yiz.setPointerHeld(.Btn2, false)
}
}}
}
// ------------------------------------------------------------------------
// Renderer
// ------------------------------------------------------------------------
RendererState :: struct {
sdlRenderer: ^sdl.Renderer,
fontMap: map[yiz.FontID]^ttf.Font,
iconMap: map[yiz.IconID]TextureInfo,
textMap: map[string]TextureInfo,
}
TextureInfo :: struct {
surface: ^sdl.Surface,
texture: ^sdl.Texture,
}
createRenderer :: proc() -> yiz.Renderer {
return {
init = initRenderer,
destroy = destroyRenderer,
measureText = measureText,
getTextHeightOffset = getTextHeightOffset,
renderText = renderText,
renderRect = renderRect,
renderIcon = renderIcon,
}
}
initRenderer :: proc(renderer: ^yiz.Renderer) {
// Init state
state := new(RendererState)
// Init sdl renderer
sdlWindow := cast(^sdl.Window)yiz.uiCtx.windowingSystem.window
state.sdlRenderer = sdl.CreateRenderer(sdlWindow, nil)
// Init font map
if !ttf.Init() do panic("Failed to initalize TTF library")
state.fontMap = make(map[yiz.FontID]^ttf.Font)
// TODO: Vendor default font? (add to assests)
state.fontMap[yiz.DEFAULT_FONT_ID] = ttf.OpenFont(
"examples/hello_world/assets/fonts/FreeSans.ttf",
yiz.DEFAULT_FONT_SIZE,
)
// Init text texture array
state.textMap = make(map[string]TextureInfo)
// ------------------------------------------------------------------------
// Init texture map
// ------------------------------------------------------------------------
state.iconMap = make(map[yiz.IconID]TextureInfo)
exitIconSurface := image.Load("examples/hello_world/assets/icons/exit.svg")
exitIconTexture := sdl.CreateTextureFromSurface(
state.sdlRenderer,
exitIconSurface,
)
state.iconMap[yiz.EXIT_ICON_ID] = {exitIconSurface, exitIconTexture}
minIconSurface := image.Load(
"examples/hello_world/assets/icons/minimize.svg",
)
minIconTexture := sdl.CreateTextureFromSurface(
state.sdlRenderer,
minIconSurface,
)
state.iconMap[yiz.MINIMIZE_ICON_ID] = {minIconSurface, minIconTexture}
maxIconSurface := image.Load(
"examples/hello_world/assets/icons/maximize.svg",
)
maxIconTexture := sdl.CreateTextureFromSurface(
state.sdlRenderer,
maxIconSurface,
)
state.iconMap[yiz.MAXIMIZE_ICON_ID] = {maxIconSurface, maxIconTexture}
// Set the renderer's state
renderer.state = yiz.RendererState(state)
}
destroyRenderer :: proc(renderer: ^yiz.Renderer) {
if renderer.state != nil {
state := cast(^RendererState)renderer.state
// Destroy icon map
for _, iconInfo in state.iconMap {
sdl.DestroyTexture(iconInfo.texture)
sdl.DestroySurface(iconInfo.surface)
}
delete(state.iconMap)
// Destory font map
for _, font in state.fontMap {
ttf.CloseFont(font)
}
delete(state.fontMap)
ttf.Quit()
// Destroy text textures
for _, textureInfo in state.textMap {
sdl.DestroyTexture(textureInfo.texture)
sdl.DestroySurface(textureInfo.surface)
}
delete(state.textMap)
// Destroy the renderer
sdl.DestroyRenderer(state.sdlRenderer)
// Destroy the renderer specific data
free(renderer.state)
}
}
// TODO: Handle font style!
measureText :: proc(
self: ^yiz.Renderer,
text: string,
fontID: yiz.FontID,
fontSize: f32,
fontStyle: yiz.FontStyle,
spacing: f32,
) -> yiz.Dimensions {
state := cast(^RendererState)self.state
fontID := state.fontMap[fontID]
textStr := strings.clone_to_cstring(text)
defer delete(textStr)
width: i32
height: i32
ttf.SetFontSize(fontID, fontSize)
ttf.GetStringSize(fontID, textStr, len(textStr), &width, &height)
return {f32(width), f32(height)}
}
getTextHeightOffset :: proc(self: ^yiz.Renderer) -> f32 {
return 0
}
// TODO: Handle font style!
// TODO: Handle char spacing!
renderText :: proc(
self: ^yiz.Renderer,
text: string,
position: yiz.Vec2,
fontID: yiz.FontID,
fontSize: f32,
fontStyle: yiz.FontStyle,
charSpacing: f32,
textColor: yiz.Color,
) {
state := cast(^RendererState)self.state
textStr := strings.clone_to_cstring(text)
defer delete(textStr)
// Retrieve texture from map or create new one if necessary
cachedTextureInfo, exists := state.textMap[text]
if !exists {
font := state.fontMap[fontID]
ttf.SetFontSize(font, fontSize)
// Create texture from the text
textSurface := ttf.RenderText_Blended(
font,
textStr,
len(textStr),
convertColor(textColor),
)
if textSurface != nil {
textTexture := sdl.CreateTextureFromSurface(
state.sdlRenderer,
textSurface,
)
cachedTextureInfo = {textSurface, textTexture}
state.textMap[text] = cachedTextureInfo
}
}
// Render the texture
dst := sdl.FRect {
position.x,
position.y,
f32(cachedTextureInfo.surface.w),
f32(cachedTextureInfo.surface.h),
}
sdl.RenderTexture(state.sdlRenderer, cachedTextureInfo.texture, nil, &dst)
}
renderRect :: proc(
self: ^yiz.Renderer,
rect: yiz.Rect,
bgColor: yiz.Color,
radii: yiz.CornerRadii,
) {
SEGMENTS :: 16
sdlRenderer := (cast(^RendererState)self.state).sdlRenderer
if yiz.isZeroRadius(radii) {
color := convertColor(bgColor)
sdl.SetRenderDrawColor(sdlRenderer, color.r, color.g, color.b, color.a)
sdl.RenderFillRect(
sdlRenderer,
&sdl.FRect{rect.x, rect.y, rect.width, rect.height},
)
} else {
vertices, indices := generateRoundedRect(
{rect.x, rect.y, rect.width, rect.height},
radii,
SEGMENTS,
{bgColor.r / 255, bgColor.g / 255, bgColor.b / 255, bgColor.a / 255},
)
defer {
delete(vertices)
delete(indices)
}
sdl.RenderGeometry(
sdlRenderer,
nil,
&vertices[0],
i32(len(vertices)),
&indices[0],
i32(len(indices)),
)
}
}
generateRoundedRect :: proc(
rect: sdl.FRect,
radii: yiz.CornerRadii,
numSegments: int,
color: sdl.FColor,
) -> (
vertices: [dynamic]sdl.Vertex,
indices: [dynamic]i32,
) {
vertices = make([dynamic]sdl.Vertex)
indices = make([dynamic]i32)
// Adds the center point of the rectangle
// All other mesh triangles will share this point
append(
&vertices,
sdl.Vertex {
position = {rect.x + rect.w * 0.05, rect.y + rect.h * 0.05},
color = color,
},
)
// The rounded rectangle is made from four quarter-circle corners
//
// Instead of manually specifying every point, we define the center
// of each corner arc and generate points around the circle
//
// Each corner center is offset inward by the radius
cornerCenters := [4]yiz.Vec2 {
{rect.x + radii.topLeft, rect.y + radii.topLeft}, // top-left
{rect.x + rect.w - radii.topRight, rect.y + radii.topRight}, // top-right
{
rect.x + rect.w - radii.bottomRight,
rect.y + rect.h - radii.bottomRight,
}, // bottom-right
{rect.x + radii.bottomLeft, rect.y + rect.h - radii.bottomLeft}, // bottom-left
}
// A circle is generated using:
//
// x = center.x + cos(angle) * radius
// y = center.y + sin(angle) * radius
//
// A full circle is 2*PI radians
// Each corner only needs a quarter circle:
//
// 2*PI / 4 = PI/2
//
// These values define where each quarter-circle starts
startingAngles := [4]f32{math.PI, -math.PI / 2, 0, math.PI / 2}
// Generate the points along each rounded corner
cornerRadii := [4]f32 {
radii.topLeft,
radii.topRight,
radii.bottomRight,
radii.bottomLeft,
}
for corner in 0 ..< 4 {
radius := cornerRadii[corner]
// ------------------------------------------------------------------------
// Non-rounded corner
// ------------------------------------------------------------------------
if radius == 0 {
cornerPos: sdl.FPoint
switch corner {
case 0:
cornerPos = {rect.x, rect.y}
case 1:
cornerPos = {rect.x + rect.w, rect.y}
case 2:
cornerPos = {rect.x + rect.w, rect.y + rect.h}
case 3:
cornerPos = {rect.x, rect.y + rect.h}
}
append(&vertices, sdl.Vertex{position = cornerPos, color = color})
continue
}
// ------------------------------------------------------------------------
// Rounded corner
// ------------------------------------------------------------------------
// Generate points along one quarter-circle
//
// i goes from 0 to segments inclusive so that the final
// point reaches the end of the arc
for i in 0 ..= numSegments {
// Move from the starting angle through a 90 degree arc
angle :=
startingAngles[corner] + (math.PI * 0.5) * f32(i) / f32(numSegments)
// Convert polar coordinates (angle + radius)
// into a screen position
//
// This gives us a point around the corner circle
x := cornerCenters[corner].x + math.cos(angle) * radius
y := cornerCenters[corner].y + math.sin(angle) * radius
append(&vertices, sdl.Vertex{position = {x, y}, color = color})
}
}
// Triangle fan:
// Create triangles from the center vertex to every pair
// of neighboring edge vertices
for i in 1 ..< len(vertices) - 1 {
append(&indices, 0)
append(&indices, i32(i))
append(&indices, i32(i + 1))
}
// Close the shape:
// The loop above stops before connecting the final edge vertex
// back to the first one, so add the closing triangle manually
append(&indices, 0)
append(&indices, i32(len(vertices) - 1))
append(&indices, 1)
return vertices, indices
}
renderIcon :: proc(
self: ^yiz.Renderer,
iconID: yiz.IconID,
bounds: yiz.Rect,
tintColor: yiz.Color,
scale: f32,
) {
state := cast(^RendererState)self.state
tintColor := convertColor(tintColor)
texture := state.iconMap[iconID].texture
sdl.SetTextureColorMod(texture, tintColor.r, tintColor.g, tintColor.b)
sdl.SetTextureAlphaMod(texture, tintColor.a)
sdl.RenderTexture(
state.sdlRenderer,
texture,
nil,
&sdl.FRect{bounds.x, bounds.y, bounds.width, bounds.height},
)
}
// ------------------------------------------------------------------------
// Window Provider
// ------------------------------------------------------------------------
createWindowProvider :: proc() -> yiz.WindowingSystem {
return {
init = initWindowProvider,
destroy = destroyWindowProvider,
createOSWindow = createOSWindow,
setWindowResizeable = setWindowResizeable,
setWindowPosition = setWindowPosition,
setWindowSize = setWindowSize,
configWindowHitTest = configWindowHitTest,
minimizeWindow = minimizeWindow,
maximizeWindow = maximizeWindow,
}
}
initWindowProvider :: proc(self: ^yiz.WindowingSystem) {
if !sdl.Init({.VIDEO, .EVENTS}) {
sdl.Log("SDL initalization failed: %s", sdl.GetError())
return
}
}
destroyWindowProvider :: proc(self: ^yiz.WindowingSystem) {
free(self.hitTestData.windowFlags)
sdl.DestroyWindow(cast(^sdl.Window)self.window)
}
createOSWindow :: proc(
self: ^yiz.WindowingSystem,
title: string,
size: yiz.Dimensions,
position: yiz.Vec2,
) {
titleStr := strings.clone_to_cstring(title)
defer delete(titleStr)
window := sdl.CreateWindow(
titleStr,
i32(size.width),
i32(size.height),
{.BORDERLESS, .HIGH_PIXEL_DENSITY, .TRANSPARENT},
)
if window == nil {
sdl.Log("SDL window initalization failed: %s", sdl.GetError())
sdl.Quit()
return
}
self.window = yiz.OSWindow(window)
self.title = title
self.size = size
self.position = position
}
setWindowResizeable :: proc(self: ^yiz.WindowingSystem) {
window := cast(^sdl.Window)self.window
sdl.SetWindowResizable(window, true)
}
setWindowPosition :: proc(self: ^yiz.WindowingSystem, position: yiz.Vec2) {
sdl.SetWindowPosition(
cast(^sdl.Window)self.window,
i32(position.x),
i32(position.y),
)
self.position = position
}
setWindowSize :: proc(self: ^yiz.WindowingSystem, size: yiz.Dimensions) {
sdl.SetWindowSize(
cast(^sdl.Window)self.window,
i32(size.width),
i32(size.height),
)
self.size = size
}
configWindowHitTest :: proc(
self: ^yiz.WindowingSystem,
borderStyle: yiz.BorderStyle,
borderBounds: yiz.Rect,
titlebarBounds: yiz.Rect,
) {
hitTest :: proc "cdecl" (
window: ^sdl.Window,
point: ^sdl.Point,
userdata: rawptr,
) -> sdl.HitTestResult {
x := f32(point.x)
y := f32(point.y)
HitTestData :: struct {
borderStyle: yiz.BorderStyle,
borderBounds: yiz.Rect,
titlebarBounds: yiz.Rect,
windowFlags: rawptr,
}
data := cast(^HitTestData)userdata
borderStyle := data.borderStyle
borderBounds := data.borderBounds
titlebarBounds := data.titlebarBounds
windowFlags := cast(^yizw.WindowFlags)data.windowFlags
left: f32 = 0
top: f32 = 0
right := borderBounds.width
bottom := borderBounds.height
onLeftEdge := x >= left && x <= left + borderStyle.thickness
onRightEdge := x <= right && x >= right - borderStyle.thickness
onTopEdge := y >= top && y <= top + borderStyle.thickness
onBottomEdge := y <= bottom && y >= bottom - borderStyle.thickness
// Handle resizing
// Only does this if window is resizeable
if .Resizeable in windowFlags {
switch {
case onLeftEdge:
return .RESIZE_LEFT
case onRightEdge:
return .RESIZE_RIGHT
case onTopEdge:
return .RESIZE_TOP
case onBottomEdge:
return .RESIZE_BOTTOM
}
}
// Handle dragging
// Only does this if window is draggable
if .Draggable in windowFlags {
// buttonWidth :=
// yiz.DEFAULT_ICON_SIZE.width +
// yizw.DEFAULT_ICON_BUTTON_STYLE.padding.left +
// yizw.DEFAULT_ICON_BUTTON_STYLE.padding.right
// buttonSpacing := yizw.DEFAULT_TITLEBAR_STYLE.buttonSpacing
// adjustedTitlebarWidth :=
// titlebarBounds.width - (3 * buttonWidth) - (4 * buttonSpacing)
// if x < titlebarBounds.x + adjustedTitlebarWidth &&
// y < titlebarBounds.y + titlebarBounds.height {
// return .DRAGGABLE
// }
if x < titlebarBounds.x + titlebarBounds.width &&
y < titlebarBounds.y + titlebarBounds.height {
return .DRAGGABLE
}
}
return .NORMAL
}
// Set the hittest data
self.hitTestData.borderStyle = borderStyle
self.hitTestData.borderBounds = borderBounds
self.hitTestData.titlebarBounds = titlebarBounds
// Define hittest window flags to be the flags of the root window widget
if self.hitTestData.windowFlags == nil {
self.hitTestData.windowFlags = new(yizw.WindowFlags)
}
rootWindow :=
yiz.getWidget(yiz.uiCtx.rootID.?).? or_else panic("Root not found")
windowFlags := cast(^yizw.WindowFlags)self.hitTestData.windowFlags
windowFlags^ = (cast(^yizw.WindowData)rootWindow.data).flags
// Actually setup the hittesting
sdlWindow := cast(^sdl.Window)self.window
sdl.SetWindowHitTest(sdlWindow, hitTest, &self.hitTestData)
}
minimizeWindow :: proc(self: ^yiz.WindowingSystem) {
sdlWindow := cast(^sdl.Window)self.window
sdl.MinimizeWindow(sdlWindow)
}
maximizeWindow :: proc(self: ^yiz.WindowingSystem) {
sdlWindow := cast(^sdl.Window)self.window
sdl.MaximizeWindow(sdlWindow)
}