Yiz

Artifact [64547d0120]
Login

Artifact [64547d0120]

Artifact 64547d01209ce5abd188bd843db4e9f4cddc0609:


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)
}