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 yc "yiz:core"
import yw "yiz:widgets"
AppState :: struct {
windowTitle: cstring,
windowSize: yc.Dimensions,
}
// TODO: Have procs for everything that accesses the uiCtx directly
// - Users of the lib should not have access to that!
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)
// Init windowing system & renderer
windowingSystem := createWindowProvider()
renderer := createRenderer()
// Init UI
yc.initUI(
context.allocator,
renderer,
windowingSystem,
{title = "Hello World!", size = {600, 400}, position = {100, 200}},
)
defer yc.destroyUI()
// Create window
windowID := yw.window({windowFlags = {.Decorated, .Draggable, .Resizeable}})
// Create a container
{
layout := yw.DEFAULT_CONTAINER_LAYOUT
layout.sizing = {
width = .FillAvailable,
height = .Fixed,
}
layout.mainAxisAlignment = .Start
layout.crossAxisAlignment = .Center
containerID := yw.container(
windowID,
{
layout = layout,
desiredSize = yc.Dimensions{height = 100},
bgColor = yc.Color{255, 0, 0, 255},
},
)
// Add button to container
styles := yw.ButtonStyles {
default = yw.DEFAULT_BUTTON_STYLE_DEFAULT,
hovered = yw.DEFAULT_BUTTON_STYLE_HOVERED,
clicked = yw.DEFAULT_BUTTON_STYLE_CLICKED,
}
styles.default.padding = yc.uniformInsets(8)
styles.hovered.padding = yc.uniformInsets(8)
styles.clicked.padding = yc.uniformInsets(8)
yw.button(containerID, {
labelText = "Click Me",
styles = styles,
onClick = proc(_: ^yc.Widget, _: rawptr) {
log.debug("Clicked!!")
},
})
}
// Container with clipped button
{
layout := yw.DEFAULT_CONTAINER_LAYOUT
layout.sizing = {
width = .Fixed,
height = .FitToContent,
}
layout.mainAxisAlignment = .Start
layout.crossAxisAlignment = .Start
containerID := yw.container(
windowID,
{layout = layout, desiredSize = yc.Dimensions{width = 75}, zIndex = 10},
)
// Add a clipped button
layout = yw.DEFAULT_BUTTON_LAYOUT
layout.crossAxisAlignment = .Start
layout.sizing.width = .Fixed
styles := yw.ButtonStyles {
default = yw.DEFAULT_BUTTON_STYLE_DEFAULT,
hovered = yw.DEFAULT_BUTTON_STYLE_HOVERED,
clicked = yw.DEFAULT_BUTTON_STYLE_CLICKED,
disabled = yw.DEFAULT_BUTTON_STYLE_DISABLED,
}
styles.default.padding = yc.uniformInsets(8)
styles.hovered.padding = yc.uniformInsets(8)
styles.clicked.padding = yc.uniformInsets(8)
yw.button(
containerID,
{
labelText = "Cut Off",
styles = styles,
layout = layout,
desiredSize = yc.Dimensions{width = 150},
flags = yc.WidgetFlags{.Visible, .Focusable},
},
)
}
// Add a floating button
{
layout := yw.DEFAULT_BUTTON_LAYOUT
layout.positioning = .Absolute
layout.relativePosition = {
anchor = .BottomRight,
offset = {-50, -50},
}
styles := yw.ButtonStyles {
default = yw.DEFAULT_BUTTON_STYLE_DEFAULT,
hovered = yw.DEFAULT_BUTTON_STYLE_HOVERED,
clicked = yw.DEFAULT_BUTTON_STYLE_CLICKED,
}
styles.default.padding = yc.uniformInsets(10)
styles.hovered.padding = yc.uniformInsets(10)
styles.clicked.padding = yc.uniformInsets(12)
styles.default.border.radii = {10, 10, 10, 10}
styles.clicked.bgColor = {255, 0, 255, 100}
yw.button(
windowID,
{
iconID = yc.MINIMIZE_ICON_ID,
layout = layout,
styles = styles,
zIndex = 1,
},
)
}
{
layout := yw.DEFAULT_CONTAINER_LAYOUT
layout.sizing = {
width = .FillAvailable,
height = .FillAvailable,
}
layout.mainAxisAlignment = .Center
layout.crossAxisAlignment = .Center
containerID := yw.container(
windowID,
{
layout = layout,
desiredSize = yc.Dimensions{height = 100},
bgColor = yc.Color{0, 0, 255, 255},
},
)
yw.button(
containerID,
{iconID = yc.MINIMIZE_ICON_ID, labelText = "COMBINED BTN"},
)
}
// Scrollbox
{
layout := yw.DEFAULT_SCROLLBOX_LAYOUT
layout.positioning = .Absolute
layout.relativePosition = {
anchor = .TopLeft,
offset = {50, 30},
}
scrollbarStyles := [2]yw.ScrollbarStyle {
yw.DEFAULT_SCROLLBAR_STYLE,
yw.DEFAULT_SCROLLBAR_STYLE,
}
// scrollbarStyles.x.bgColor = {0, 255, 0, 255}
// scrollbarStyles.y.bgColor = {0, 255, 0, 255}
scrollboxID := yw.scrollbox(
windowID,
{
layout = layout,
zIndex = 10,
viewportProps = {size = {300, 200}},
scrollbarProps = {
{
style = scrollbarStyles.x,
displayMode = yw.ScrollbarDisplayMode.Overflow,
},
{
style = scrollbarStyles.y,
displayMode = yw.ScrollbarDisplayMode.Overflow,
},
},
},
)
}
// Main loop
yc.run(true)
}
convertColor :: proc(color: yc.Color) -> sdl.Color {
return {u8(color.r), u8(color.g), u8(color.b), u8(color.a)}
}
// ------------------------------------------------------------------------
// Inputs
// ------------------------------------------------------------------------
mapInputs :: proc() {
// Clear transient input state
yc.uiCtx.inputState.pointer.pressed = {}
yc.uiCtx.inputState.pointer.released = {}
event: sdl.Event
for sdl.PollEvent(&event) {
#partial switch event.type {
// ------------------------------------------------------------------------
// Handle quit events
// ------------------------------------------------------------------------
case .QUIT:
fallthrough
case .TERMINATING:
yc.requestWindowClose()
// ------------------------------------------------------------------------
// Handle window events
// ------------------------------------------------------------------------
case .WINDOW_RESIZED:
yc.uiCtx.windowingSystem.size = {
width = f32(event.window.data1),
height = f32(event.window.data2),
}
yc.markDirty(yc.uiCtx.rootID.?, false)
// ------------------------------------------------------------------------
// TODO: Handle keyboard events
// ------------------------------------------------------------------------
case .KEY_DOWN:
if event.key.scancode == .Q {
yc.requestWindowClose()
}
case .KEY_UP:
// ------------------------------------------------------------------------
// Handle mouse events
// ------------------------------------------------------------------------
case .MOUSE_MOTION:
yc.setPointerPosition({event.motion.x, event.motion.y})
case .MOUSE_BUTTON_DOWN:
switch event.button.button {
case sdl.BUTTON_LEFT:
yc.setPointerPressed(.Btn1)
yc.setPointerHeld(.Btn1, true)
case sdl.BUTTON_RIGHT:
yc.setPointerPressed(.Btn2)
yc.setPointerHeld(.Btn2, true)
}
case .MOUSE_BUTTON_UP:
switch event.button.button {
case sdl.BUTTON_LEFT:
yc.setPointerReleased(.Btn1)
yc.setPointerHeld(.Btn1, false)
case sdl.BUTTON_RIGHT:
yc.setPointerReleased(.Btn2)
yc.setPointerHeld(.Btn2, false)
}
}}
}
// ------------------------------------------------------------------------
// Renderer
// ------------------------------------------------------------------------
RendererState :: struct {
sdlRenderer: ^sdl.Renderer,
fontMap: map[yc.FontID]^ttf.Font,
iconMap: map[yc.ImageID]TextureInfo,
textMap: map[string]TextureInfo,
}
TextureInfo :: struct {
surface: ^sdl.Surface,
texture: ^sdl.Texture,
}
createRenderer :: proc() -> yc.Renderer {
return {
init = initRenderer,
destroy = destroyRenderer,
beginFrame = beginFrame,
endFrame = endFrame,
measureText = measureText,
getTextHeightOffset = getTextHeightOffset,
renderText = renderText,
renderRect = renderRect,
renderImage = renderImage,
setClip = setClip,
clearClip = clearClip,
}
}
initRenderer :: proc(renderer: ^yc.Renderer) {
// Init state
state := new(RendererState)
// Init sdl renderer
sdlWindow := cast(^sdl.Window)yc.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[yc.FontID]^ttf.Font)
// TODO: Vendor default font? (add to assests)
state.fontMap[yc.DEFAULT_FONT_ID] = ttf.OpenFont(
"examples/hello_world/assets/fonts/FreeSans.ttf",
yc.DEFAULT_FONT_SIZE,
)
// Init text texture array
state.textMap = make(map[string]TextureInfo)
// ------------------------------------------------------------------------
// Init texture map
// ------------------------------------------------------------------------
state.iconMap = make(map[yc.ImageID]TextureInfo)
exitIconSurface := image.Load("examples/hello_world/assets/icons/exit.svg")
exitIconTexture := sdl.CreateTextureFromSurface(
state.sdlRenderer,
exitIconSurface,
)
state.iconMap[yc.EXIT_ICON_ID] = {exitIconSurface, exitIconTexture}
minIconSurface := image.Load(
"examples/hello_world/assets/icons/minimize.svg",
)
minIconTexture := sdl.CreateTextureFromSurface(
state.sdlRenderer,
minIconSurface,
)
state.iconMap[yc.MINIMIZE_ICON_ID] = {minIconSurface, minIconTexture}
maxIconSurface := image.Load(
"examples/hello_world/assets/icons/maximize.svg",
)
maxIconTexture := sdl.CreateTextureFromSurface(
state.sdlRenderer,
maxIconSurface,
)
state.iconMap[yc.MAXIMIZE_ICON_ID] = {maxIconSurface, maxIconTexture}
// Set the renderer's state
renderer.state = yc.RendererState(state)
}
destroyRenderer :: proc(renderer: ^yc.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)
}
}
beginFrame :: proc(self: ^yc.Renderer) {
data := cast(^RendererState)self.state
mapInputs()
sdl.SetRenderDrawBlendMode(data.sdlRenderer, {.BLEND})
sdl.SetRenderDrawColor(data.sdlRenderer, 0, 0, 0, 0)
sdl.RenderClear(data.sdlRenderer)
}
endFrame :: proc(self: ^yc.Renderer) {
data := cast(^RendererState)self.state
sdl.RenderPresent(data.sdlRenderer)
}
// TODO: Handle font style!
measureText :: proc(
self: ^yc.Renderer,
text: string,
fontID: yc.FontID,
fontSize: f32,
fontStyle: yc.FontStyle,
spacing: f32,
) -> yc.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: ^yc.Renderer) -> f32 {
return 0
}
// TODO: cache text by the text, font, and color
// TODO: Handle font style!
// TODO: Handle char spacing!
renderText :: proc(
self: ^yc.Renderer,
text: string,
position: yc.Vec2,
fontID: yc.FontID,
fontSize: f32,
fontStyle: yc.FontStyle,
charSpacing: f32,
textColor: yc.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: ^yc.Renderer,
rect: yc.Rect,
bgColor: yc.Color,
radii: yc.CornerRadii,
) {
SEGMENTS :: 64
sdlRenderer := (cast(^RendererState)self.state).sdlRenderer
if yc.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 := generateRoundedRect2(
{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: yc.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.5, rect.y + rect.h * 0.5},
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]yc.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
}
generateRoundedRect2 :: proc(
rect: sdl.FRect,
radii: yc.CornerRadii,
numSegments: int,
color: sdl.FColor,
) -> (
vertices: [dynamic]sdl.Vertex,
indices: [dynamic]i32,
) {
vertices = make([dynamic]sdl.Vertex)
indices = make([dynamic]i32)
append(
&vertices,
sdl.Vertex {
position = {rect.x + rect.w * 0.5, rect.y + rect.h * 0.5},
color = color,
},
)
cornerCenters := [4]yc.Vec2 {
{rect.x + radii.topLeft, rect.y + radii.topLeft},
{rect.x + rect.w - radii.topRight, rect.y + radii.topRight},
{rect.x + rect.w - radii.bottomRight, rect.y + rect.h - radii.bottomRight},
{rect.x + radii.bottomLeft, rect.y + rect.h - radii.bottomLeft},
}
cornerRadii := [4]f32 {
radii.topLeft,
radii.topRight,
radii.bottomRight,
radii.bottomLeft,
}
startingAngles := [4]f32{math.PI, -math.PI / 2, 0, math.PI / 2}
for corner in 0 ..< 4 {
radius := cornerRadii[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
}
for i in 0 ..= numSegments {
t := f32(i) / f32(numSegments)
angle := startingAngles[corner] + math.PI * 0.5 * t
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})
}
}
for i in 1 ..< len(vertices) - 1 {
append(&indices, 0)
append(&indices, i32(i))
append(&indices, i32(i + 1))
}
append(&indices, 0)
append(&indices, i32(len(vertices) - 1))
append(&indices, 1)
return vertices, indices
}
renderImage :: proc(
self: ^yc.Renderer,
iconID: yc.ImageID,
bounds: yc.Rect,
tintColor: yc.Color,
scale: f32,
) {
state := cast(^RendererState)self.state
tintColor := convertColor(tintColor)
texture := state.iconMap[iconID].texture
// TODO: Handle scaling
sdl.SetTextureBlendMode(texture, {.BLEND})
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},
)
}
setClip :: proc(self: ^yc.Renderer, clip: yc.Rect) {
state := cast(^RendererState)self.state
sdlClip := sdl.Rect {
i32(clip.x),
i32(clip.y),
i32(clip.width),
i32(clip.height),
}
sdl.SetRenderClipRect(state.sdlRenderer, &sdlClip)
}
clearClip :: proc(self: ^yc.Renderer) {
state := cast(^RendererState)self.state
sdl.SetRenderClipRect(state.sdlRenderer, nil)
}
// ------------------------------------------------------------------------
// Window Provider
// ------------------------------------------------------------------------
createWindowProvider :: proc() -> yc.WindowingSystem {
return {
init = initWindowProvider,
destroy = destroyWindowProvider,
createOSWindow = createOSWindow,
setWindowResizeable = setWindowResizeable,
setWindowPosition = setWindowPosition,
setWindowSize = setWindowSize,
configWindowHitTest = configWindowHitTest,
minimizeWindow = minimizeWindow,
maximizeWindow = maximizeWindow,
restoreWindow = restoreWindow,
}
}
initWindowProvider :: proc(self: ^yc.WindowingSystem) {
if !sdl.Init({.VIDEO, .EVENTS}) {
sdl.Log("SDL initalization failed: %s", sdl.GetError())
return
}
}
destroyWindowProvider :: proc(self: ^yc.WindowingSystem) {
sdl.DestroyWindow(cast(^sdl.Window)self.window)
free(self.hitTestData.windowFlags)
}
createOSWindow :: proc(
self: ^yc.WindowingSystem,
title: string,
size: yc.Dimensions,
position: yc.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 = yc.OSWindow(window)
self.title = title
self.size = size
self.position = position
}
setWindowResizeable :: proc(self: ^yc.WindowingSystem) {
window := cast(^sdl.Window)self.window
sdl.SetWindowResizable(window, true)
}
setWindowPosition :: proc(self: ^yc.WindowingSystem, position: yc.Vec2) {
sdl.SetWindowPosition(
cast(^sdl.Window)self.window,
i32(position.x),
i32(position.y),
)
self.position = position
}
setWindowSize :: proc(self: ^yc.WindowingSystem, size: yc.Dimensions) {
sdl.SetWindowSize(
cast(^sdl.Window)self.window,
i32(size.width),
i32(size.height),
)
self.size = size
}
configWindowHitTest :: proc(
self: ^yc.WindowingSystem,
borderStyle: yc.BorderStyle,
borderBounds: yc.Rect,
titlebarBounds: yc.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: yc.BorderStyle,
borderBounds: yc.Rect,
titlebarBounds: yc.Rect,
windowFlags: rawptr,
}
data := cast(^HitTestData)userdata
borderStyle := data.borderStyle
borderBounds := data.borderBounds
titlebarBounds := data.titlebarBounds
windowFlags := cast(^yw.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 onTopEdge && onLeftEdge:
return .RESIZE_TOPLEFT
case onTopEdge && onRightEdge:
return .RESIZE_TOPRIGHT
case onBottomEdge && onLeftEdge:
return .RESIZE_BOTTOMLEFT
case onBottomEdge && onRightEdge:
return .RESIZE_BOTTOMRIGHT
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 {
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(yw.WindowFlags)
}
rootWindow :=
yc.getWidget(yc.uiCtx.rootID.?).? or_else panic("Root not found")
windowFlags := cast(^yw.WindowFlags)self.hitTestData.windowFlags
windowFlags^ = (cast(^yw.WindowData)rootWindow.data).flags
// Actually setup the hittesting
sdlWindow := cast(^sdl.Window)self.window
sdl.SetWindowHitTest(sdlWindow, hitTest, &self.hitTestData)
}
minimizeWindow :: proc(self: ^yc.WindowingSystem) {
sdlWindow := cast(^sdl.Window)self.window
sdl.MinimizeWindow(sdlWindow)
}
maximizeWindow :: proc(self: ^yc.WindowingSystem) {
sdlWindow := cast(^sdl.Window)self.window
sdl.MaximizeWindow(sdlWindow)
}
restoreWindow :: proc(self: ^yc.WindowingSystem) {
sdlWindow := cast(^sdl.Window)self.window
sdl.RestoreWindow(sdlWindow)
}