Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

313 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Siwin

siwin

Cross-platform window creation and event handling library.

Can be used as an alternative to GLFW/GLUT/windy

Language Code size Latest version

Features

  • works with: OpenGL, Vulkan, Metal (on MacOS), software rendering
  • works on: Linux(X11 and Wayland), Windows, MacOS
  • handles events from: mouse, keyboard
  • and also supports: clipboard, offscreen rendering, interactive move/resize, multithreading, etc.

Examples

simple window

Create a window with continous event polling:

import siwin, opengl

let window = newOpenglWindow()
opengl.loadExtensions()  # load opengl functions

window.eventsHandler.onRender = proc(e: RenderEvent) =
  glClearColor(0.1, 0.1, 0.1, 1)
  glClear(GlColorBufferBit or GlDepthBufferBit)

run window

event loop window

Run an application with a more efficient blocking event loop:

import siwin, opengl

let globals = newSiwinGlobals()
let window = globals.newOpenglWindow()
opengl.loadExtensions()  # load opengl functions

window.eventsHandler.onRender = proc(e: RenderEvent) =
  glClearColor(0.1, 0.1, 0.1, 1)
  glClear(GlColorBufferBit or GlDepthBufferBit)

globals.runEventDriven(window)

This approach handles many windows with a shared event loop.

software-rendering window

import siwin, vmath

const color = [32'u8, 32, 32, 255]

run newSoftwareRenderingWindow(), WindowEventsHandler(
  onRender: proc(e: RenderEvent) =
    let pixelBuffer = e.window.pixelBuffer
    
    for y in 0..<pixelBuffer.size.y:
      for x in 0..<pixelBuffer.size.x:
        cast[ptr UncheckedArray[array[4, uint8]]](pixelBuffer.data)[y * pixelBuffer.size.x + x] = color

    convertPixelsInplace(pixelBuffer.data, pixelBuffer.size, PixelBufferFormat.bgrx_32bit, pixelBuffer.format)
  ,
  onKey: proc(e: KeyEvent) =
    if (not e.pressed) and e.key == Key.escape:
      close e.window
)

OpenGL

import siwin, opengl, vmath

let siwinGlobals = newSiwinGlobals(
  preferedPlatform = (when defined(linux): x11 else: defaultPreferedPlatform)
  # note: glBegin and other non- OpenGL ES functions don't work on Wayland,
  #       so we should set preferedPlatform to x11 on linux when using regular OpenGL.
  #       see tests/t_opengl_es.nim for more complex, wayland-compatible opengl example
)

var window = siwinGlobals.newOpenglWindow(
  title="OpenGL example",
)
loadExtensions()  # init opengl

run window, WindowEventsHandler(
  onResize: proc(e: ResizeEvent) =
    glViewport 0, 0, e.size.x.GLsizei, e.size.y.GLsizei
    glMatrixMode GlProjection
    glLoadIdentity()
    glOrtho -30, 30, -30, 30, -30, 30
    glMatrixMode(GlModelView)
  ,
  onRender: proc(e: RenderEvent) =
    glClearColor 0.3, 0.3, 0.3, 1
    glClear GlColorBufferBit or GlDepthBufferBit

    glShadeModel GlSmooth

    glLoadIdentity()
    glTranslatef -15, -15, 0

    glBegin GlTriangles
    glColor3f 1, 0, 0
    glVertex2f 0, 0
    glColor3f 0, 1, 0
    glVertex2f 30, 0
    glColor3f 0, 0, 1
    glVertex2f 0, 30
    glEnd()
)

note: call redraw(window) every time you want window.render to be called. siwin will automatically call window.render only when window resizes.
note: opengl 1.x and 2.x functions (like glBegin), is not supported on Wayland, due to Wayland only beeng able to initialize with EGL

Vulkan

see t_vulkan.nim

import siwin, nimgl/vulkan, sequtils

doassert vkInit()

let exts = getRequiredVulkanExtensions()
var cexts = exts.mapit(it[0].addr)

var appInfo = newVkApplicationInfo(
  pApplicationName = "siwin Vulkan example",
  applicationVersion = vkMakeVersion(1, 0, 0),
  pEngineName = "No Engine",
  engineVersion = vkMakeVersion(1, 0, 0),
  apiVersion = vkApiVersion1_1
)

var instanceCreateInfo = newVkInstanceCreateInfo(
  pApplicationInfo = appInfo.addr,
  enabledExtensionCount = exts.len,
  ppEnabledExtensionNames = cast[cstringArray](cexts[0].addr),
  enabledLayerCount = 0,
  ppEnabledLayerNames = nil,
)

var instance: VkInstance
doassert vkCreateInstance(instanceCreateInfo.addr, nil, result.addr) == VKSuccess

let siwinGlobals = newSiwinGlobals()

let window = siwinGlobals.newVulkanWindow(cast[pointer](instance), title="Vulkan example")
let surface = cast[VkSurfaceKHR](window.vulkanSurface)

# do other initialization using instance and surface...

run window, WindowEventsHandler(
  onRender: proc(e: RenderEvent) =
    ## do rendering...
  ,
  onClose: proc(e: CloseEvent) =
    ## uninitialize before surface destruction
)

# surface already destroyed, continue uninitializing...

pixie

note: very slow, but useful if opengl not needed and if window is used to just display one single image

import siwin, pixie

var image: Image

let siwinGlobals = newSiwinGlobals()

run siwinGlobals.newSoftwareRenderingWindow(title="pixie example"), WindowEventsHandler(
  onResize: proc(e: ResizeEvent) =
    if e.size.x * e.size.y <= 0: return
    image = newImage(e.size.x, e.size.y)
  ,
  onRender: proc(e: RenderEvent) =
    if e.window.size.x * e.window.size.y <= 0: return
    image.fill(rgba(255, 255, 255, 255))

    let ctx = image.newContext
    ctx.fillStyle = rgba(0, 255, 0, 255)

    let
      wh = vec2(250, 250)
      pos = vec2(image.width.float, image.height.float) / 2 - wh / 2
    
    ctx.fillRoundedRect(rect(pos, wh), 25.0)

    let pixelBuffer = e.window.pixelBuffer
    copyMem(pixelBuffer.data, image.data[0].addr, pixelBuffer.size.x * pixelBuffer.size.y * Color32bit.sizeof)
    convertPixelsInplace(pixelBuffer.data, pixelBuffer.size, PixelBufferFormat.rgbx_32bit, pixelBuffer.format)
  ,
  onKey: proc(e: KeyEvent) =
    if (not e.pressed) and e.key == Key.escape:
      close e.window
)

popup windows

This api adds popup windows. On Wayland these are required to do popups, but on other platforms these will just be frameless windows.

import siwin, vmath

let globals = newSiwinGlobals()
let parent = globals.newSoftwareRenderingWindow()

let placement = PopupPlacement(
  anchorRectPos: ivec2(100, 100),
  anchorRectSize: ivec2(120, 40),
  size: ivec2(320, 220),
  anchor: Edge.bottomLeft,
  gravity: Edge.topLeft,
  offset: ivec2(0, 8),
  constraintAdjustment: {PopupConstraintAdjustment.pcaSlideX, PopupConstraintAdjustment.pcaFlipY},
  reactive: true,
)

let popup = globals.newPopupWindow(parent, placement)

clipboard

let clipboard = window.clipboard

echo clipboard.text
clipboard.text = "some text"

offscreen rendering

note: this will create invisible window. ctx mustn't be discarded as its destructor will close the window.
If you have multiple contexts, use makeCurrent to select.

import siwin/offscreen, opengl

let siwinGlobals = newSiwinGlobals()

let ctx {.used.} = siwinGlobals.newOpenglContext()
loadExtensions()

# do any opengl computing

manual main cycle

import std/times
import siwin, opengl

let globals = newSiwinGlobals()
let window = globals.newOpenglWindow()
opengl.loadExtensions()

var elapsed = initDuration()
window.eventsHandler = WindowEventsHandler(
  onTick: proc(event: TickEvent) =
    elapsed += event.deltaTime
    event.window.redraw()
  ,
  onRender: proc(event: RenderEvent) =
    let brightness = (elapsed.inMilliseconds mod 1000).float32 / 1000
    glClearColor(brightness, brightness, brightness, 1)
    glClear(GlColorBufferBit or GlDepthBufferBit)
)

window.firstStep(makeVisible = true)
while window.opened:
  window.step()

manual event loop cycle

The blocking event loop approach is recommended when apps don't need continuous onTick events. Wake events can be added for short lived animations or other needs. This can significantly reduce CPU usage over the polling approach.

Switch applications from run to runEvenDrive which waits once for native input or an explicit wake event and then services every window. This means onTick callbacks and others will only be called on wake events or when there's new events.

import siwin

let globals = newSiwinGlobals()
let window = globals.newSoftwareRenderingWindow(title = "Siwin event loop")

window.eventsHandler = WindowEventsHandler(
  onKey: proc(event: KeyEvent) =
    if not event.pressed and event.key == Key.escape:
      event.window.close()
)

globals.runEventDriven(window)

runEventDriven is a convenience runner built from lower-level event-loop APIs. Use them directly when integrating Siwin with another event loop, scheduler, or application queue:

  • globals.pollEvents() dispatches available native events and returns immediately.
  • globals.waitEvents() waits for native input or an explicit application wakeup.
  • globals.waitEvents(timeout) also accepts a deadline and returns eventActivity or eventTimeout.
  • window.serviceWindow() performs one nonblocking tick, render, and presentation pass after the application has handled the dispatched work.

Applications that need to drain another event loop or queue can own the wait directly. Install one copied EventLoopWaker on each application-thread destination queue instead of sharing all of SiwinGlobals. Every producer must enqueue its message before waking the application thread:

The queue names below are illustrative; use the queue owned by your runtime:

let waker = globals.eventLoopWaker()

# On a producer thread:
destinationQueue.send(message)
waker.wake()

# On the application thread, after waitEvents returns:
destinationQueue.drain()

Wakeups carry no data and may be coalesced, so the destination queue remains the source of truth. Drain every relevant queue after each waitEvents return, then call serviceWindow for every open window. A copied waker is safe to retain and becomes harmless after its event loop shuts down.

The C ABI provides the same lifetime model through the independently retained opaque SiwinEventLoopWaker handle. Create it with siwin_event_loop_waker, wake it from a producer with siwin_event_loop_waker_wake, and release it with siwin_destroy_event_loop_waker; the handle does not require the producer to retain SiwinGlobals.

For animation, pass the next real deadline instead of scheduling an unconditional 16 ms wake:

discard globals.waitEvents(timeUntilNextAnimation)
window.serviceWindow()

See text_input_demo.nim for a complete loop that combines native input, cursor blinking, and scroll-decay deadlines.

running multiple windows

import siwin

let siwinGlobals = newSiwinGlobals()

let win1 = siwinGlobals.newOpenglWindow()
let win2 = siwinGlobals.newOpenglWindow()
loadExtensions()

let win1_eventsHandler = WindowEventsHandler(
  onResize: proc(e: ResizeEvent) =
    makeCurrent e.window
    #...
  ,
  onRender: proc(e: RenderEvent) =
    makeCurrent e.window
    #...
)
let win2_eventsHandler = WindowEventsHandler(
  onResize: proc(e: ResizeEvent) =
    makeCurrent e.window
    #...
  ,
  onRender: proc(e: RenderEvent) =
    makeCurrent e.window
    #...
)

runMultiple(
  (window: win1, eventsHandler: win1_eventsHandler, makeVisible: true),
  (window: win2, eventsHandler: win2_eventsHandler, makeVisible: true),
)

Use runMultipleEventDriven instead when the application doesn't need continous onTick events and can use the more efficient blocking call:

siwinGlobals.runMultipleEventDriven(
  (window: win1, eventsHandler: win1_eventsHandler, makeVisible: true),
  (window: win2, eventsHandler: win2_eventsHandler, makeVisible: true),
)

client-side decorations

import siwin

let siwinGlobals = newSiwinGlobals()

let window = siwinGlobals.newOpenglWindow(transparent=true, frameless=true)
loadExtensions()

run window, WindowEventsHandler(
  onMouseMove: proc(e: MouseMoveEvent) =
    if MouseButton.left in e.window.mouse.pressed:
      window.startInteractiveMove()
      # see also: startInteractiveResize
)

transparent backdrop blur

Backdrop blur uses the same API on macOS, Windows, Wayland, and X11. It is available on macOS, Windows 11 build 22621 or newer, and KDE compositors that advertise the KWin blur extension. Create the window with an alpha-capable surface so the effect can show through transparent pixels.

import siwin

let window = newSoftwareRenderingWindow(transparent = true, frameless = true)

if window.supports(wvcBackdropBlur):
  discard window.trySetBackdrop(initWindowBackdrop()) # whole-window blur

# Empty regions mean the whole window. Non-empty regions use Siwin window coordinates.
# Regional blur is currently available on macOS and KDE only.
# discard window.trySetBackdrop(initWindowBackdrop(regions))

window.clearBackdrop()

macOS also supports system materials such as wbmSidebar and wbmHud:

if window.supports(wvcBackdropMaterial):
  window.setBackdrop(initWindowBackdrop(wbmSidebar))

See backdrop_blur_demo.nim for a runnable cross-platform example.

all methods and events

see siwin/platforms/any/window

I want to get system handle of window and do some magic, but it is private?

import std/importutils
import siwin/platforms/x11/window
privateAccess WindowX11Obj
# ...
window.handle

Contributions

If you want to support this project, here is some tasks to do:

  • See issues
  • Any bugfixes is always accepted, just describe somewhere what you fixed
  • Refactoring (my code is bad, i know it)
    • if you doing very big refactoring, first create issue to ask is all your changes needed, and if it is, refactor
  • Documentation
  • Optimization
  • MacOS support
  • Android/IOS support
  • Web support
  • copy/paste images
  • Make cool site that adverts siwin

Just fork levovix0/siwin to your account, make changes and submit a pull request.
Or if it requires new repository to be created, create it and add an "change dependency" issue.

About

Simple window creation library

Resources

Stars

34 stars

Watchers

3 watching

Forks

Releases

Contributors

Languages