Skip to content

Instantly share code, notes, and snippets.

@naranyala
Last active July 26, 2025 23:55
Show Gist options
  • Select an option

  • Save naranyala/5787fd0d487cec503a2a1d193b3daf91 to your computer and use it in GitHub Desktop.

Select an option

Save naranyala/5787fd0d487cec503a2a1d193b3daf91 to your computer and use it in GitHub Desktop.
for the community, a sliding-up drawer (bottom sheets) with backdrop using nim and raylib
import raylib as rl
# === Reactive System with Explicit Type Safety ===
type
ObservableWatcher = proc(): void {.closure.}
Observable*[T] = ref object
value*: T
watchers: seq[ObservableWatcher]
proc newObservable*[T](initialValue: T): Observable[T] =
## Creates a new Observable with explicit initial value and empty watchers
result = Observable[T](
value: initialValue,
watchers: @[]
)
proc setValue*[T](observable: Observable[T], newValue: T): void =
## Sets observable value with change detection and notification
if observable.value != newValue:
observable.value = newValue
for watcher in observable.watchers:
watcher()
proc getValue*[T](observable: Observable[T]): T =
## Gets current observable value
return observable.value
proc addWatcher*[T](observable: Observable[T], callback: ObservableWatcher): void =
## Adds a watcher callback to observable
observable.watchers.add(callback)
# === UI Constants with Explicit Types ===
const
ScreenWidth: int32 = 400
ScreenHeight: int32 = 300
DrawerHeight: float32 = 150.0
DrawerAnimationSpeed: float32 = 10.0
BackdropAlpha: uint8 = 150
TextOffsetX: int32 = 20
TextOffsetY: int32 = 20
FontSize: int32 = 20
ToggleInstructionY: int32 = 40
ToggleInstructionX: int32 = 60
DrawerCornerRadius: float32 = 0.1
DrawerCornerSegments: int32 = 5
# === Application State with Explicit Initialization ===
type
ApplicationState = object
drawerVisible: Observable[bool]
drawerCurrentY: float32
proc newApplicationState(): ApplicationState =
## Creates new application state with explicit default values
result = ApplicationState(
drawerVisible: newObservable[bool](false),
drawerCurrentY: float32(ScreenHeight)
)
var appState: ApplicationState = newApplicationState()
# === Utility Functions ===
proc createColor(red: uint8, green: uint8, blue: uint8, alpha: uint8): rl.Color =
## Creates Color with explicit RGBA values
result = rl.Color(r: red, g: green, b: blue, a: alpha)
proc minFloat32(a: float32, b: float32): float32 =
## Returns minimum of two float32 values
if a < b: a else: b
proc maxFloat32(a: float32, b: float32): float32 =
## Returns maximum of two float32 values
if a > b: a else: b
# === UI Component Functions ===
proc renderBackdrop(): bool =
## Renders semi-transparent backdrop and returns true if clicked outside drawer
let backdropColor: rl.Color = createColor(0, 0, 0, BackdropAlpha)
rl.drawRectangle(0, 0, ScreenWidth, ScreenHeight, backdropColor)
let mousePosition: rl.Vector2 = rl.getMousePosition()
let isMouseReleased: bool = rl.isMouseButtonReleased(rl.MouseButton.LEFT)
let drawerBounds = rl.Rectangle(
x: 0.0,
y: appState.drawerCurrentY,
width: float32(ScreenWidth),
height: DrawerHeight
)
let isClickOutsideDrawer: bool = not rl.checkCollisionPointRec(mousePosition, drawerBounds)
return isMouseReleased and isClickOutsideDrawer
proc updateAndRenderDrawer(): void =
## Updates drawer animation and renders drawer content
# Calculate target position based on visibility state
let isVisible: bool = getValue(appState.drawerVisible)
let targetY: float32 = if isVisible:
float32(ScreenHeight) - DrawerHeight
else:
float32(ScreenHeight)
# Animate drawer position towards target
if appState.drawerCurrentY < targetY:
appState.drawerCurrentY = minFloat32(appState.drawerCurrentY + DrawerAnimationSpeed, targetY)
elif appState.drawerCurrentY > targetY:
appState.drawerCurrentY = maxFloat32(appState.drawerCurrentY - DrawerAnimationSpeed, targetY)
# Define drawer bounds for rendering
let drawerBounds = rl.Rectangle(
x: 0.0,
y: appState.drawerCurrentY,
width: float32(ScreenWidth),
height: DrawerHeight
)
# Render drawer background
rl.drawRectangleRounded(
drawerBounds,
DrawerCornerRadius,
DrawerCornerSegments,
rl.DARKGRAY
)
# Render drawer content text
let textX: int32 = TextOffsetX
let textY: int32 = int32(appState.drawerCurrentY) + TextOffsetY
rl.drawText("Drawer Content", textX, textY, FontSize, rl.WHITE)
proc handleToggleInput(): void =
## Handles space key input for toggling drawer visibility
if rl.isKeyPressed(rl.KeyboardKey.SPACE):
let currentVisibility: bool = getValue(appState.drawerVisible)
setValue(appState.drawerVisible, not currentVisibility)
proc renderInstructions(): void =
## Renders toggle instructions
let instructionText: string = "Press SPACE to toggle drawer"
rl.drawText(instructionText, ToggleInstructionX, ToggleInstructionY, FontSize, rl.DARKGRAY)
proc handleBackdropInteraction(): void =
## Handles backdrop click to close drawer
let isDrawerVisible: bool = getValue(appState.drawerVisible)
if isDrawerVisible and renderBackdrop():
setValue(appState.drawerVisible, false)
# === Main Application Loop ===
proc main(): void =
# Initialize window with explicit dimensions
rl.initWindow(ScreenWidth, ScreenHeight, "Sliding Drawer with Backdrop")
rl.setTargetFPS(60)
# Main rendering and interaction loop
while not rl.windowShouldClose():
rl.beginDrawing()
rl.clearBackground(rl.RAYWHITE)
# Handle user input
handleToggleInput()
# Render UI instructions
renderInstructions()
# Handle backdrop interaction (only when drawer is visible)
handleBackdropInteraction()
# Update and render drawer
updateAndRenderDrawer()
rl.endDrawing()
# Cleanup resources
rl.closeWindow()
# Entry point with explicit module guard
when isMainModule:
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment