diff --git a/README.md b/README.md
index 619f68d..42c72f8 100644
--- a/README.md
+++ b/README.md
@@ -190,6 +190,30 @@ run siwinGlobals.newSoftwareRenderingWindow(title="pixie example"), WindowEvents
)
```
+
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.
+
+```nim
+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
```nim
diff --git a/examples/popup_demo.nim b/examples/popup_demo.nim
new file mode 100644
index 0000000..d142e59
--- /dev/null
+++ b/examples/popup_demo.nim
@@ -0,0 +1,371 @@
+import std/[strformat, math]
+import vmath
+import siwin
+import siwin/colorutils
+
+const
+ TitleBarHeight = 52'i32
+ CloseButtonSize = 40'i32
+ CloseButtonMargin = 12'i32
+ ResizeBorderWidth = 8'f32
+ ResizeCornerWidth = 18'f32
+
+ PopupSize = ivec2(520, 420)
+ ButtonSize = ivec2(220, 68)
+
+type
+ MainDemoState = object
+ rgbaBuffer: seq[Color32bit]
+ hoverButton: bool
+
+ PopupDemoState = object
+ rgbaBuffer: seq[Color32bit]
+ hoverClose: bool
+
+proc rgba(r, g, b: byte, a: byte = 255): Color32bit =
+ [r, g, b, a]
+
+proc insideRect(pos: Vec2, x, y, w, h: int32): bool =
+ pos.x >= x.float32 and pos.x < (x + w).float32 and pos.y >= y.float32 and
+ pos.y < (y + h).float32
+
+proc ensureBuffer[T](rgbaBuffer: var seq[T], size: IVec2) =
+ let pixelCount = max(1, (size.x * size.y).int)
+ if rgbaBuffer.len != pixelCount:
+ rgbaBuffer.setLen(pixelCount)
+
+proc fill(rgbaBuffer: var seq[Color32bit], size: IVec2, color: Color32bit) =
+ for i in 0 ..< size.x * size.y:
+ rgbaBuffer[i] = color
+
+proc fillRect(
+ rgbaBuffer: var seq[Color32bit], size: IVec2, x, y, w, h: int32, color: Color32bit
+) =
+ let
+ x0 = max(0, x)
+ y0 = max(0, y)
+ x1 = min(size.x, x + w)
+ y1 = min(size.y, y + h)
+
+ if x0 >= x1 or y0 >= y1:
+ return
+
+ for py in y0 ..< y1:
+ let row = py * size.x
+ for px in x0 ..< x1:
+ rgbaBuffer[row + px] = color
+
+proc px(scale: float32, value: int32): int32 =
+ round(value.float64 * scale.float64).int32
+
+proc pf(scale: float32, value: float32): float32 =
+ (value.float64 * scale.float64).float32
+
+proc popupButtonRect(size: IVec2): tuple[x, y, w, h: int32] =
+ (
+ x: max(28, (size.x - ButtonSize.x) div 2),
+ y: max(32, (size.y - ButtonSize.y) div 2),
+ w: ButtonSize.x,
+ h: ButtonSize.y,
+ )
+
+proc closeButtonRect(size: IVec2): tuple[x, y, w, h: int32] =
+ (
+ x: size.x - CloseButtonSize - CloseButtonMargin,
+ y: (TitleBarHeight - CloseButtonSize) div 2,
+ w: CloseButtonSize,
+ h: CloseButtonSize,
+ )
+
+proc sizeToString(size: IVec2): string =
+ fmt"({size.x}, {size.y})"
+
+proc posToString(pos: Vec2): string =
+ fmt"({pos.x:.1f}, {pos.y:.1f})"
+
+proc rectToString(rect: tuple[x, y, w, h: int32]): string =
+ fmt"(x={rect.x}, y={rect.y}, w={rect.w}, h={rect.h})"
+
+proc logPopupCloseButton(window: Window, reason: string) =
+ let size = window.size
+ let closeRect = closeButtonRect(size)
+ echo fmt"[popup_demo] {reason}: popupSize={sizeToString(size)} closeButton={rectToString(closeRect)}"
+
+proc logPopupPlacementState(reason: string, parent, popup: Window, placement: PopupPlacement) =
+ let expectedPos = parent.pos + placement.popupRelativePos()
+ echo fmt"[popup_demo] {reason}: parentPos={sizeToString(parent.pos)} popupPos={sizeToString(popup.pos)} expectedPos={sizeToString(expectedPos)} relativePos={sizeToString(placement.popupRelativePos())} popupSize={sizeToString(popup.size)}"
+
+proc logPopupPointer(
+ kind: string,
+ window: Window,
+ pos: Vec2,
+ button: MouseButton,
+ pressed = false,
+ double = false,
+) =
+ let closeRect = closeButtonRect(window.size)
+ let insideClose =
+ insideRect(pos, closeRect.x, closeRect.y, closeRect.w, closeRect.h)
+ echo fmt"[popup_demo] {kind}: button={$button} pressed={pressed} double={double} pos={posToString(pos)} closeButton={rectToString(closeRect)} insideClose={insideClose}"
+
+proc popupPlacement(size: IVec2): PopupPlacement =
+ let buttonRect = popupButtonRect(size)
+ PopupPlacement(
+ anchorRectPos: ivec2(buttonRect.x, buttonRect.y),
+ anchorRectSize: ivec2(buttonRect.w, buttonRect.h),
+ size: PopupSize,
+ anchor: Edge.bottomLeft,
+ gravity: Edge.bottomLeft,
+ offset: ivec2(0, 14),
+ constraintAdjustment: {},
+ reactive: false,
+ )
+
+proc applyPopupDragRegions(window: Window) =
+ let size = window.size
+ let titleWidth = max(1'i32, size.x - CloseButtonSize - CloseButtonMargin * 2)
+ window.setTitleRegion(vec2(0, 0), vec2(titleWidth.float32, TitleBarHeight.float32))
+ window.setBorderWidth(ResizeBorderWidth, 0, ResizeCornerWidth)
+ window.logPopupCloseButton("applyPopupDragRegions")
+
+proc drawMainWindow(state: var MainDemoState, size: IVec2, popupOpen: bool) =
+ state.rgbaBuffer.ensureBuffer(size)
+ state.rgbaBuffer.fill(size, rgba(245, 247, 250))
+
+ state.rgbaBuffer.fillRect(size, 48, 52, size.x - 96, 84, rgba(225, 231, 239))
+ state.rgbaBuffer.fillRect(size, 48, 164, size.x - 96, 132, rgba(255, 255, 255))
+ state.rgbaBuffer.fillRect(
+ size, 48, size.y - 132, size.x - 96, 72, rgba(225, 231, 239)
+ )
+
+ let buttonRect = popupButtonRect(size)
+ let buttonColor =
+ if popupOpen:
+ rgba(48, 103, 214)
+ elif state.hoverButton:
+ rgba(71, 125, 232)
+ else:
+ rgba(37, 88, 196)
+ state.rgbaBuffer.fillRect(
+ size, buttonRect.x, buttonRect.y, buttonRect.w, buttonRect.h, buttonColor
+ )
+ state.rgbaBuffer.fillRect(
+ size,
+ buttonRect.x + 6,
+ buttonRect.y + 6,
+ buttonRect.w - 12,
+ buttonRect.h - 12,
+ rgba(243, 247, 255),
+ )
+ state.rgbaBuffer.fillRect(
+ size,
+ buttonRect.x + 18,
+ buttonRect.y + 18,
+ buttonRect.w - 36,
+ buttonRect.h - 36,
+ buttonColor,
+ )
+
+proc drawCloseGlyph(
+ rgbaBuffer: var seq[Color32bit], size: IVec2, rect: tuple[x, y, w, h: int32]
+) =
+ let glyphInset = 12
+ for i in 0 ..< (rect.w - glyphInset * 2):
+ let x1 = rect.x + glyphInset + i
+ let y1 = rect.y + glyphInset + i
+ let x2 = rect.x + rect.w - glyphInset - 1 - i
+ let y2 = rect.y + glyphInset + i
+ if x1 >= 0 and x1 < size.x and y1 >= 0 and y1 < size.y:
+ rgbaBuffer[y1 * size.x + x1] = rgba(245, 247, 250)
+ if x2 >= 0 and x2 < size.x and y2 >= 0 and y2 < size.y:
+ rgbaBuffer[y2 * size.x + x2] = rgba(245, 247, 250)
+
+proc drawPopupWindow(state: var PopupDemoState, size: IVec2) =
+ state.rgbaBuffer.ensureBuffer(size)
+ state.rgbaBuffer.fill(size, rgba(238, 241, 245))
+
+ state.rgbaBuffer.fillRect(size, 0, 0, size.x, TitleBarHeight, rgba(23, 34, 46))
+ state.rgbaBuffer.fillRect(
+ size, 18, 82, size.x - 36, size.y - 100, rgba(255, 255, 255)
+ )
+ state.rgbaBuffer.fillRect(size, 42, 118, size.x - 84, 92, rgba(226, 232, 240))
+ state.rgbaBuffer.fillRect(size, 42, 232, size.x - 84, 92, rgba(212, 226, 255))
+ state.rgbaBuffer.fillRect(size, 42, 346, (size.x - 96) div 2, 42, rgba(255, 224, 201))
+ state.rgbaBuffer.fillRect(
+ size, size.x div 2 + 8, 346, (size.x - 96) div 2, 42, rgba(208, 244, 226)
+ )
+
+ let closeRect = closeButtonRect(size)
+ state.rgbaBuffer.fillRect(
+ size,
+ closeRect.x,
+ closeRect.y,
+ closeRect.w,
+ closeRect.h,
+ (if state.hoverClose: rgba(206, 62, 68) else: rgba(153, 33, 40)),
+ )
+ state.rgbaBuffer.drawCloseGlyph(size, closeRect)
+
+let globals = newSiwinGlobals()
+let window =
+ globals.newSoftwareRenderingWindow(size = ivec2(300, 200), title = "siwin popup demo")
+
+var
+ mainState: MainDemoState
+ popupState: PopupDemoState
+ popup: PopupWindow
+
+proc popupIsOpen(): bool =
+ popup != nil
+
+proc updatePopupPlacement() =
+ if popup != nil and popup.opened:
+ let placement = window.size.popupPlacement()
+ popup.reposition(placement)
+ logPopupPlacementState("updatePopupPlacement", window, popup, placement)
+
+proc closePopup() =
+ if popup != nil and popup.opened:
+ popup.close()
+
+proc installPopupHandlers() =
+ popup.eventsHandler = WindowEventsHandler(
+ onResize: proc(e: ResizeEvent) =
+ e.window.applyPopupDragRegions()
+ if popup != nil:
+ logPopupPlacementState("popup.onResize", window, e.window, e.window.placement)
+ redraw e.window
+ ,
+ onRender: proc(e: RenderEvent) =
+ let pixelBuffer = e.window.pixelBuffer
+ popupState.drawPopupWindow(pixelBuffer.size)
+ copyMem(
+ pixelBuffer.data,
+ popupState.rgbaBuffer[0].addr,
+ popupState.rgbaBuffer.len * sizeof(Color32bit),
+ )
+ convertPixelsInplace(
+ pixelBuffer.data, pixelBuffer.size, PixelBufferFormat.rgba_32bit,
+ pixelBuffer.format,
+ ),
+ onMouseMove: proc(e: MouseMoveEvent) =
+ let closeRect = closeButtonRect(e.window.size)
+ let newHover =
+ insideRect(e.pos, closeRect.x, closeRect.y, closeRect.w, closeRect.h)
+ if popupState.hoverClose != newHover:
+ popupState.hoverClose = newHover
+ redraw e.window
+ ,
+ onMouseButton: proc(e: MouseButtonEvent) =
+ if e.button == MouseButton.left:
+ logPopupPointer(
+ "onMouseButton",
+ e.window,
+ e.window.mouse.pos,
+ e.button,
+ pressed = e.pressed,
+ )
+ if e.button == MouseButton.left and e.pressed:
+ let closeRect = closeButtonRect(e.window.size)
+ if insideRect(
+ e.window.mouse.pos,
+ closeRect.x,
+ closeRect.y,
+ closeRect.w,
+ closeRect.h,
+ ):
+ e.window.close()
+ ,
+ onClick: proc(e: ClickEvent) =
+ if e.button == MouseButton.left:
+ logPopupPointer(
+ "onClick",
+ e.window,
+ e.pos,
+ e.button,
+ double = e.double,
+ )
+ let closeRect = closeButtonRect(e.window.size)
+ if e.button == MouseButton.left and
+ insideRect(e.pos, closeRect.x, closeRect.y, closeRect.w, closeRect.h):
+ e.window.close()
+ ,
+ onKey: proc(e: KeyEvent) =
+ if e.pressed and not e.generated and e.key == Key.escape:
+ e.window.close()
+ ,
+ onClose: proc(e: CloseEvent) =
+ popup = nil
+ popupState.hoverClose = false
+ redraw window
+ ,
+ )
+
+proc openPopup() =
+ if popup != nil:
+ return
+
+ let placement = window.size.popupPlacement()
+ popup = globals.newPopupWindow(window, placement, grab = true)
+ popupState.hoverClose = false
+ popup.applyPopupDragRegions()
+ installPopupHandlers()
+ popup.logPopupCloseButton("openPopup")
+ logPopupPlacementState("openPopup", window, popup, placement)
+ popup.firstStep(makeVisible = true)
+ redraw popup
+ redraw window
+
+window.eventsHandler = WindowEventsHandler(
+ onResize: proc(e: ResizeEvent) =
+ updatePopupPlacement()
+ redraw e.window
+ ,
+ onWindowMove: proc(e: WindowMoveEvent) =
+ updatePopupPlacement(),
+ onRender: proc(e: RenderEvent) =
+ let pixelBuffer = e.window.pixelBuffer
+ mainState.drawMainWindow(pixelBuffer.size, popupIsOpen())
+ copyMem(
+ pixelBuffer.data,
+ mainState.rgbaBuffer[0].addr,
+ mainState.rgbaBuffer.len * sizeof(Color32bit),
+ )
+ convertPixelsInplace(
+ pixelBuffer.data, pixelBuffer.size, PixelBufferFormat.rgba_32bit,
+ pixelBuffer.format,
+ ),
+ onMouseMove: proc(e: MouseMoveEvent) =
+ let buttonRect = popupButtonRect(e.window.size)
+ let newHover =
+ insideRect(e.pos, buttonRect.x, buttonRect.y, buttonRect.w, buttonRect.h)
+ if mainState.hoverButton != newHover:
+ mainState.hoverButton = newHover
+ redraw e.window
+ ,
+ onClick: proc(e: ClickEvent) =
+ let buttonRect = popupButtonRect(e.window.size)
+ if e.button == MouseButton.left and
+ insideRect(e.pos, buttonRect.x, buttonRect.y, buttonRect.w, buttonRect.h):
+ if popupIsOpen():
+ closePopup()
+ else:
+ openPopup()
+ ,
+ onClose: proc(e: CloseEvent) =
+ closePopup(),
+ onKey: proc(e: KeyEvent) =
+ if e.pressed and not e.generated and e.key == Key.escape:
+ if popup != nil:
+ closePopup()
+ else:
+ close e.window
+ ,
+)
+
+window.firstStep(makeVisible = true)
+while window.opened or popup != nil:
+ if window.opened:
+ window.step()
+ if popup != nil:
+ popup.step()
diff --git a/src/siwin/platforms/any/window.nim b/src/siwin/platforms/any/window.nim
index f3717c3..51421ed 100644
--- a/src/siwin/platforms/any/window.nim
+++ b/src/siwin/platforms/any/window.nim
@@ -122,6 +122,29 @@ type
rejected
accepted
+ PopupConstraintAdjustment* {.siwin_enum.} = enum
+ pcaSlideX
+ pcaSlideY
+ pcaFlipX
+ pcaFlipY
+ pcaResizeX
+ pcaResizeY
+
+ PopupDismissReason* {.siwin_enum.} = enum
+ pdrClientClosed
+ pdrCompositorDismissed
+ pdrParentClosed
+
+ PopupPlacement* = object
+ anchorRectPos*: IVec2
+ anchorRectSize*: IVec2
+ size*: IVec2
+ anchor*: Edge
+ gravity*: Edge
+ offset*: IVec2
+ constraintAdjustment*: set[PopupConstraintAdjustment]
+ reactive*: bool
+
AnyWindowEvent* = object of RootObj
window*: Window
@@ -191,6 +214,9 @@ type
value*: bool
kind*: StateBoolChangedEventKind
isExternal*: bool ## changed by user via compositor (server-side change)
+
+ PopupEvent* = object of AnyWindowEvent
+ reason*: PopupDismissReason
DropEvent* = object of AnyWindowEvent
@@ -219,6 +245,8 @@ type
## binary state of focus/fullscreen/maximized/frameless changed
## fullscreen and maximized changes are sent before ResizeEvent
+ onPopupDone*: proc(e: PopupEvent) ## popup was dismissed or explicitly closed
+
onDrop*: proc(e: DropEvent) ## drag&drop clipboard content is beeng pasted to this window
@@ -240,6 +268,11 @@ type
m_frameless: bool
m_cursor: Cursor
m_separateTouch: bool
+ m_isPopup: bool
+ m_popupGrab: bool
+ m_popupDismissed: bool
+ m_popupParent: Window
+ m_popupPlacement: PopupPlacement
m_size: IVec2
m_pos: IVec2
@@ -267,6 +300,106 @@ method height*(screen: Screen): int32 {.base.} = discard
proc size*(screen: Screen): IVec2 = ivec2(screen.width, screen.height)
+type PopupWindow* = Window
+
+func popupSize*(placement: PopupPlacement): IVec2 =
+ if placement.size.x > 0 and placement.size.y > 0:
+ placement.size
+ elif placement.anchorRectSize.x > 0 and placement.anchorRectSize.y > 0:
+ placement.anchorRectSize
+ else:
+ ivec2(1, 1)
+
+func popupAnchorOffset*(anchor: Edge, size: IVec2): IVec2 =
+ case anchor
+ of Edge.topLeft: ivec2(0, 0)
+ of Edge.top: ivec2(size.x div 2, 0)
+ of Edge.topRight: ivec2(size.x, 0)
+ of Edge.left: ivec2(0, size.y div 2)
+ of Edge.right: ivec2(size.x, size.y div 2)
+ of Edge.bottomLeft: ivec2(0, size.y)
+ of Edge.bottom: ivec2(size.x div 2, size.y)
+ of Edge.bottomRight: ivec2(size.x, size.y)
+
+func popupRelativePos*(placement: PopupPlacement): IVec2 =
+ let anchorPoint = placement.anchorRectPos + placement.anchor.popupAnchorOffset(placement.anchorRectSize)
+ anchorPoint - placement.gravity.popupAnchorOffset(placement.popupSize()) + placement.offset
+
+proc flipPopupEdgeX*(edge: Edge): Edge =
+ case edge
+ of Edge.topLeft: Edge.topRight
+ of Edge.topRight: Edge.topLeft
+ of Edge.left: Edge.right
+ of Edge.right: Edge.left
+ of Edge.bottomLeft: Edge.bottomRight
+ of Edge.bottomRight: Edge.bottomLeft
+ else: edge
+
+proc flipPopupEdgeY*(edge: Edge): Edge =
+ case edge
+ of Edge.topLeft: Edge.bottomLeft
+ of Edge.top: Edge.bottom
+ of Edge.topRight: Edge.bottomRight
+ of Edge.bottomLeft: Edge.topLeft
+ of Edge.bottom: Edge.top
+ of Edge.bottomRight: Edge.topRight
+ else: edge
+
+proc popupOverflowX*(posX, width, boundsWidth: int32): int32 {.inline.} =
+ max(0'i32, -posX) + max(0'i32, posX + width - boundsWidth)
+
+proc popupOverflowY*(posY, height, boundsHeight: int32): int32 {.inline.} =
+ max(0'i32, -posY) + max(0'i32, posY + height - boundsHeight)
+
+proc resolvePopupRect*(parentPos, boundsPos, boundsSize: IVec2, placement: PopupPlacement): tuple[pos, size: IVec2] =
+ proc popupRectFor(placement: PopupPlacement): tuple[pos, size: IVec2] =
+ (parentPos + placement.popupRelativePos(), placement.popupSize())
+
+ var resolvedPlacement = placement
+ result = popupRectFor(resolvedPlacement)
+
+ if PopupConstraintAdjustment.pcaFlipX in placement.constraintAdjustment:
+ var flipped = resolvedPlacement
+ flipped.anchor = flipped.anchor.flipPopupEdgeX()
+ flipped.gravity = flipped.gravity.flipPopupEdgeX()
+ let flippedRect = popupRectFor(flipped)
+ if popupOverflowX(flippedRect.pos.x - boundsPos.x, flippedRect.size.x, boundsSize.x) <
+ popupOverflowX(result.pos.x - boundsPos.x, result.size.x, boundsSize.x):
+ resolvedPlacement = flipped
+ result = flippedRect
+
+ if PopupConstraintAdjustment.pcaFlipY in placement.constraintAdjustment:
+ var flipped = resolvedPlacement
+ flipped.anchor = flipped.anchor.flipPopupEdgeY()
+ flipped.gravity = flipped.gravity.flipPopupEdgeY()
+ let flippedRect = popupRectFor(flipped)
+ if popupOverflowY(flippedRect.pos.y - boundsPos.y, flippedRect.size.y, boundsSize.y) <
+ popupOverflowY(result.pos.y - boundsPos.y, result.size.y, boundsSize.y):
+ resolvedPlacement = flipped
+ result = flippedRect
+
+ if PopupConstraintAdjustment.pcaSlideX in placement.constraintAdjustment:
+ result.pos.x = clamp(result.pos.x, boundsPos.x, max(boundsPos.x, boundsPos.x + boundsSize.x - result.size.x))
+
+ if PopupConstraintAdjustment.pcaSlideY in placement.constraintAdjustment:
+ result.pos.y = clamp(result.pos.y, boundsPos.y, max(boundsPos.y, boundsPos.y + boundsSize.y - result.size.y))
+
+ if PopupConstraintAdjustment.pcaResizeX in placement.constraintAdjustment:
+ if result.pos.x < boundsPos.x:
+ result.size.x -= boundsPos.x - result.pos.x
+ result.pos.x = boundsPos.x
+ if result.pos.x + result.size.x > boundsPos.x + boundsSize.x:
+ result.size.x = max(1'i32, boundsPos.x + boundsSize.x - result.pos.x)
+ result.size.x = max(1'i32, result.size.x)
+
+ if PopupConstraintAdjustment.pcaResizeY in placement.constraintAdjustment:
+ if result.pos.y < boundsPos.y:
+ result.size.y -= boundsPos.y - result.pos.y
+ result.pos.y = boundsPos.y
+ if result.pos.y + result.size.y > boundsPos.y + boundsSize.y:
+ result.size.y = max(1'i32, boundsPos.y + boundsSize.y - result.pos.y)
+ result.size.y = max(1'i32, result.size.y)
+
proc closed*(window: Window): bool = window.m_closed
proc opened*(window: Window): bool = not window.closed
@@ -280,6 +413,8 @@ proc frameless*(window: Window): bool = window.m_frameless
proc cursor*(window: Window): Cursor = window.m_cursor
proc separateTouch*(window: Window): bool = window.m_separateTouch
## enable/disable handling touch events separately from mouse events
+proc isPopup*(window: Window): bool = window.m_isPopup
+proc popupGrab*(window: Window): bool = window.m_popupGrab
method reportedSize*(window: Window): IVec2 {.base.} = window.m_size
## Size reported to API users/events (backing pixels on HiDPI platforms).
@@ -295,6 +430,24 @@ proc minSize*(window: Window): IVec2 = window.m_minSize
proc maxSize*(window: Window): IVec2 = window.m_maxSize
proc focused*(window: Window): bool = window.m_focused
+method parentWindow*(window: Window): Window {.base.} = window.m_popupParent
+method placement*(window: Window): PopupPlacement {.base.} = window.m_popupPlacement
+proc popupOpen*(window: Window): bool = window.opened and window.visible
+
+proc initPopupState*(window, parent: Window, placement: PopupPlacement, grab: bool) =
+ window.m_isPopup = true
+ window.m_popupGrab = grab
+ window.m_popupDismissed = false
+ window.m_popupParent = parent
+ window.m_popupPlacement = placement
+ window.m_size = placement.popupSize()
+
+proc notifyPopupDone*(window: Window, reason: PopupDismissReason) =
+ if not window.m_isPopup or window.m_popupDismissed:
+ return
+ window.m_popupDismissed = true
+ if window.eventsHandler.onPopupDone != nil:
+ window.eventsHandler.onPopupDone(PopupEvent(window: window, reason: reason))
method uiScale*(window: Window): float32 {.base.} = 1'f32
## UI scale factor (device pixels per logical point).
@@ -316,6 +469,12 @@ method `cursor=`*(window: Window, v: Cursor) {.base.} = discard
method `separateTouch=`*(window: Window, v: bool) {.base.} = discard
## enable/disable handling touch events separately from mouse events
+method `placement=`*(window: Window, v: PopupPlacement) {.base.} =
+ window.m_popupPlacement = v
+
+method reposition*(window: Window, v: PopupPlacement) {.base.} =
+ window.placement = v
+
method `size=`*(window: Window, v: IVec2) {.base.} = discard
## resize window
diff --git a/src/siwin/platforms/cocoa/window.nim b/src/siwin/platforms/cocoa/window.nim
index 2629642..ff065b2 100644
--- a/src/siwin/platforms/cocoa/window.nim
+++ b/src/siwin/platforms/cocoa/window.nim
@@ -1,4 +1,4 @@
-import std/[importutils, tables, times, os, unicode, uri, sequtils, strutils]
+import std/[importutils, tables, times, os, unicode, uri, sequtils, strutils, strformat, math]
import pkg/[vmath]
from pkg/darwin/quartz_core/calayer import CALayer
from pkg/darwin/quartz_core/cametal_layer import CAMetalLayer
@@ -19,6 +19,8 @@ template autoreleasepool(body: untyped) =
finally:
pool.release()
+proc isFlipped(v: NSView): bool {.objc: "isFlipped".}
+
type
ScreenCocoa* = ref object of Screen
id: int32
@@ -774,6 +776,7 @@ method close*(window: WindowCocoa) =
if window.m_closed:
return
`=destroy` window[]
+ window.eventsHandler.pushEvent onClose, CloseEvent(window: window)
method `size=`*(window: WindowCocoa, v: IVec2) =
if v.x <= 0 or v.y <= 0:
@@ -815,6 +818,93 @@ method `pos=`*(window: WindowCocoa, v: IVec2) =
true
)
+proc framePos*(window: WindowCocoa): IVec2 =
+ if window == nil or window.handle == nil:
+ return window.m_pos
+
+ let frame = window.handle.frame
+ let screen = window.handle.screen
+ result = window.m_pos
+ if screen != nil:
+ let screenFrame = screen.frame
+ result = vec2(
+ frame.origin.x,
+ screenFrame.size.height - frame.origin.y - frame.size.height - 1
+ ).ivec2
+
+proc contentPos*(window: WindowCocoa): IVec2 =
+ let framePos = window.framePos()
+ if window == nil or window.handle == nil:
+ return framePos
+
+ let frame = window.handle.frame
+ let contentRect = window.handle.contentRectForFrameRect(frame)
+ let leftInset = contentRect.origin.x - frame.origin.x
+ let topInset =
+ frame.size.height -
+ ((contentRect.origin.y - frame.origin.y) + contentRect.size.height)
+ result = ivec2(
+ framePos.x + leftInset.int32,
+ framePos.y + topInset.int32,
+ )
+
+proc syncPosFromHandle(window: WindowCocoa) =
+ if window == nil or window.handle == nil:
+ return
+ window.m_pos = window.framePos()
+
+proc toPoints(distance: int32, scale: float32): int32 =
+ if scale <= 0'f32:
+ return distance
+ round(distance.float64 / scale.float64).int32
+
+proc toPoints(size: IVec2, scale: float32): IVec2 =
+ ivec2(
+ max(1'i32, toPoints(size.x, scale)),
+ max(1'i32, toPoints(size.y, scale)),
+ )
+
+proc setContentSizeInPoints(window: WindowCocoa, size: IVec2) =
+ let frame = window.handle.frame
+ let contentRect = window.handle.contentRectForFrameRect(frame)
+ let borderW = frame.size.width - contentRect.size.width
+ let borderH = frame.size.height - contentRect.size.height
+ window.handle.setFrame(
+ NSMakeRect(
+ frame.origin.x,
+ frame.origin.y,
+ size.x.float64 + borderW,
+ size.y.float64 + borderH
+ ),
+ true
+ )
+
+proc applyPopupPlacement(window: WindowCocoa, placement: PopupPlacement) =
+ window.m_popupPlacement = placement
+
+ if not window.m_isPopup:
+ return
+
+ let parent = window.parentWindow
+ if parent == nil:
+ return
+
+ let popupSize = placement.popupSize()
+ let parentScale = max(1'f32, parent.uiScale)
+ let popupSizePoints = popupSize.toPoints(parentScale)
+ if window.m_size != popupSize:
+ window.m_size = popupSize
+ if window of WindowCocoaSoftwareRendering:
+ window.WindowCocoaSoftwareRendering.resizeSoftwarePixelBuffer(popupSize)
+ window.setContentSizeInPoints(popupSizePoints)
+
+ let parentContentPos =
+ if parent of WindowCocoa:
+ parent.WindowCocoa.contentPos()
+ else:
+ parent.pos
+ window.pos = parentContentPos + placement.popupRelativePos().toPoints(parentScale)
+
method `fullscreen=`*(window: WindowCocoa, v: bool) =
if window.m_fullscreen == v:
return
@@ -884,6 +974,12 @@ method `maxSize=`*(window: WindowCocoa, v: IVec2) =
window.m_maxSize = v
window.handle.setMaxSize(NSMakeSize(v.x.float64, v.y.float64))
+method `placement=`*(window: WindowCocoa, v: PopupPlacement) =
+ window.applyPopupPlacement(v)
+
+method reposition*(window: WindowCocoa, v: PopupPlacement) =
+ window.placement = v
+
method `icon=`*(window: WindowCocoa, _: nil.typeof) =
if NSApp == nil:
discard NSApplication.sharedApplication()
@@ -1076,20 +1172,40 @@ proc init =
let
bounds = contentView.bounds
- backingBounds = contentView.convertRectToBacking(bounds)
- backingPointRect = contentView.convertRectToBacking(NSMakeRect(location.x, location.y, 0, 0))
+ boundsW = max(1e-6, bounds.size.width)
+ boundsH = max(1e-6, bounds.size.height)
+ y =
+ if contentView.isFlipped():
+ location.y.float32
+ else:
+ (bounds.size.height - location.y).float32
+ scaleX = window.m_size.x.float32 / boundsW.float32
+ scaleY = window.m_size.y.float32 / boundsH.float32
vec2(
- backingPointRect.origin.x.float32,
- (backingBounds.size.height - backingPointRect.origin.y).float32
+ location.x.float32 * scaleX,
+ y * scaleY
)
+ proc logPopupMouseButton(
+ window: WindowCocoa,
+ phase: string,
+ button: MouseButton,
+ pressed: bool,
+ location: NsPoint,
+ pos: Vec2,
+ ) =
+ if not window.m_isPopup or button != MouseButton.left:
+ return
+ echo fmt"[siwin/cocoa popup] {phase}: button={$button} pressed={pressed} raw=({location.x:.1f}, {location.y:.1f}) pos=({pos.x:.1f}, {pos.y:.1f}) size=({window.m_size.x}, {window.m_size.y})"
+
proc updateMousePos(window: WindowCocoa, location: NsPoint, kind: MouseMoveKind) =
window.mouse.pos = scaledMousePos(window, location)
window.eventsHandler.pushEvent onMouseMove, MouseMoveEvent(window: window, pos: window.mouse.pos, kind: kind)
proc handleMouseButton(window: WindowCocoa, button: MouseButton, pressed: bool, location: NsPoint) =
window.mouse.pos = scaledMousePos(window, location)
+ window.logPopupMouseButton("mouseButton", button, pressed, location, window.mouse.pos)
if pressed:
window.mouse.pressed.incl button
window.clicking.incl button
@@ -1098,6 +1214,13 @@ proc init =
window.mouse.pressed.excl button
if button in window.clicking:
+ window.logPopupMouseButton(
+ "clickDispatch",
+ button,
+ pressed,
+ location,
+ window.mouse.pos,
+ )
window.eventsHandler.pushEvent onClick, ClickEvent(
window: window, button: button, pos: window.mouse.pos,
double: (nows - window.lastClickTime[button]).inMilliseconds < 200
@@ -1145,13 +1268,7 @@ proc init =
addMethod "windowDidMove:", proc(self: Id, cmd: Sel, notification: NsNotification): Id {.cdecl.} =
getWindow(self)
autoreleasepool:
- let
- windowFrame = window.handle.frame
- screenFrame = window.handle.screen.frame
- window.m_pos = vec2(
- windowFrame.origin.x,
- screenFrame.size.height - windowFrame.origin.y - windowFrame.size.height - 1
- ).ivec2
+ window.syncPosFromHandle()
window.eventsHandler.pushEvent onWindowMove, WindowMoveEvent(window: window, pos: window.m_pos)
addMethod "canBecomeKeyWindow", proc(self: Id, cmd: Sel): bool {.cdecl.} =
@@ -1537,6 +1654,7 @@ method firstStep*(window: WindowCocoa, makeVisible = true) =
if event == nil:
break
NSApp.sendEvent(event)
+ window.syncPosFromHandle()
# Ensure all visible windows are present in the initial z-order. Without
# this, some macOS setups only show the most recently shown window until
# the user cycles windows manually.
@@ -1615,6 +1733,26 @@ proc newSoftwareRenderingWindowCocoa*(
result.title = title
if not resizable: result.resizable = false
+proc newPopupWindowCocoa*(
+ parent: WindowCocoa, placement: PopupPlacement, transparent = false, grab = true
+): WindowCocoaSoftwareRendering =
+ if parent == nil:
+ raise ValueError.newException("Popup windows require a parent window")
+ result = newSoftwareRenderingWindowCocoa(
+ size = placement.popupSize(),
+ title = "",
+ screen = defaultScreenCocoa(),
+ resizable = false,
+ fullscreen = false,
+ frameless = true,
+ transparent = transparent,
+ )
+ result.initPopupState(parent, placement, grab)
+ result.placement = placement
+ # Interactive popups need to accept focus to receive full mouse/key input.
+ result.canBecomeKeyWindow = true
+ result.canBecomeMainWindow = false
+
proc newOpenglWindowCocoa*(
size = ivec2(1280, 720),
diff --git a/src/siwin/platforms/wayland/window.nim b/src/siwin/platforms/wayland/window.nim
index 7716b32..6fa5f47 100644
--- a/src/siwin/platforms/wayland/window.nim
+++ b/src/siwin/platforms/wayland/window.nim
@@ -16,6 +16,7 @@ type
WindowWaylandKind* {.pure.} = enum
XdgSurface
+ PopupSurface
LayerSurface
WindowWayland* = ref WindowWaylandObj
@@ -24,6 +25,7 @@ type
surface: Wl_surface
xdgSurface: Xdg_surface
xdgToplevel: Xdg_toplevel
+ xdgPopup: Xdg_popup
serverDecoration: Zxdg_toplevel_decoration_v1
# will be nil if compositor doesn't support this protocol
@@ -60,6 +62,8 @@ type
lastKeyRepeatedTime: Time
bufferScaleFactor: int32
fractionalScaleFactor: float32
+ popupRepositionToken: uint32
+ releasing: bool
viewport: Wp_viewport
fractionalScaleObj: Wp_fractional_scale_v1
toplevelIcon: Xdg_toplevel_icon_v1
@@ -78,6 +82,7 @@ type
WindowWaylandSoftwareRenderingObj* = object of WindowWayland
buffer: SharedBuffer
oldBuffer: SharedBuffer
+ softwarePresentEnabled*: bool
proc waylandKeyToKey(keycode: uint32): Key =
@@ -308,6 +313,10 @@ method release(window: WindowWayland) {.base, raises: [].} =
f x
x = typeof(x).default
+ if window == nil or window.releasing:
+ return
+ window.releasing = true
+
if window.surface != nil:
if window.globals.associatedWindows_queueRemove_insteadOf_removingInstantly:
window.globals.associatedWindows_removeQueue.add window.surface.proxy.raw.id
@@ -318,6 +327,7 @@ method release(window: WindowWayland) {.base, raises: [].} =
clearToplevelIconResources(window)
destroy window.idleInhibitor, destroy
destroy window.layerShellSurface, destroy
+ destroy window.xdgPopup, destroy
destroy window.fractionalScaleObj, destroy
destroy window.viewport, destroy
destroy window.plasmaSurface, destroy
@@ -386,6 +396,16 @@ proc bufferSize(window: WindowWayland, logicalSize: IVec2): IVec2 {.inline.} =
let scale = window.effectiveUiScale()
ivec2(scaledBufferLength(logicalSize.x, scale), scaledBufferLength(logicalSize.y, scale))
+proc reportedCoord(window: WindowWayland; logical: int32): int32 {.inline.} =
+ let scale = window.effectiveUiScale()
+ if scale <= 1'f32:
+ logical
+ else:
+ round(logical.float32 * scale).int32
+
+proc reportedPos(window: WindowWayland; logicalPos: IVec2): IVec2 {.inline.} =
+ ivec2(window.reportedCoord(logicalPos.x), window.reportedCoord(logicalPos.y))
+
proc toLogicalLength(window: WindowWayland; backing: float32): float32 {.inline.} =
let scale = window.effectiveUiScale()
if scale <= 1'f32:
@@ -396,6 +416,23 @@ proc toLogicalLength(window: WindowWayland; backing: float32): float32 {.inline.
proc toLogicalLength(window: WindowWayland; backing: int32): int32 {.inline.} =
max(1'i32, (window.toLogicalLength(backing.float32) + 0.5'f32).int32)
+proc toLogicalCoord(window: WindowWayland; backing: int32): int32 {.inline.} =
+ if window == nil:
+ backing
+ else:
+ round(window.toLogicalLength(backing.float32)).int32
+
+proc toLogicalSize(window: WindowWayland; backing: int32): int32 {.inline.} =
+ if backing <= 0:
+ backing
+ elif window == nil:
+ backing
+ else:
+ max(1'i32, round(window.toLogicalLength(backing.float32)).int32)
+
+proc toLogicalSize(window: WindowWayland; backing: IVec2): IVec2 {.inline.} =
+ ivec2(window.toLogicalSize(backing.x), window.toLogicalSize(backing.y))
+
proc reportedPointerPos(window: WindowWayland, surfaceX, surfaceY: float32): Vec2 {.inline.} =
let scale = window.effectiveUiScale()
if scale <= 1'f32:
@@ -621,6 +658,73 @@ proc setupOpaqueRegion(window: WindowWayland, size: IVec2, transparent: bool) =
window.surface.set_opaque_region(opaqueRegion)
destroy opaqueRegion
+proc toXdgPositionerAnchor(edge: Edge): `Xdg_positioner / Anchor` =
+ case edge
+ of Edge.topLeft: `Xdg_positioner / Anchor`.top_left
+ of Edge.top: `Xdg_positioner / Anchor`.top
+ of Edge.topRight: `Xdg_positioner / Anchor`.top_right
+ of Edge.left: `Xdg_positioner / Anchor`.left
+ of Edge.right: `Xdg_positioner / Anchor`.right
+ of Edge.bottomLeft: `Xdg_positioner / Anchor`.bottom_left
+ of Edge.bottom: `Xdg_positioner / Anchor`.bottom
+ of Edge.bottomRight: `Xdg_positioner / Anchor`.bottom_right
+
+proc toXdgPositionerGravity(edge: Edge): `Xdg_positioner / Gravity` =
+ let xdgEdge = edge.flipPopupEdgeX().flipPopupEdgeY()
+ case xdgEdge
+ of Edge.topLeft: `Xdg_positioner / Gravity`.top_left
+ of Edge.top: `Xdg_positioner / Gravity`.top
+ of Edge.topRight: `Xdg_positioner / Gravity`.top_right
+ of Edge.left: `Xdg_positioner / Gravity`.left
+ of Edge.right: `Xdg_positioner / Gravity`.right
+ of Edge.bottomLeft: `Xdg_positioner / Gravity`.bottom_left
+ of Edge.bottom: `Xdg_positioner / Gravity`.bottom
+ of Edge.bottomRight: `Xdg_positioner / Gravity`.bottom_right
+
+proc toXdgConstraintAdjustment(
+ adjustments: set[PopupConstraintAdjustment]
+): uint32 =
+ for adjustment in adjustments:
+ result = result or (
+ case adjustment
+ of PopupConstraintAdjustment.pcaSlideX:
+ `Xdg_positioner / Constraint_adjustment`.slide_x.uint32
+ of PopupConstraintAdjustment.pcaSlideY:
+ `Xdg_positioner / Constraint_adjustment`.slide_y.uint32
+ of PopupConstraintAdjustment.pcaFlipX:
+ `Xdg_positioner / Constraint_adjustment`.flip_x.uint32
+ of PopupConstraintAdjustment.pcaFlipY:
+ `Xdg_positioner / Constraint_adjustment`.flip_y.uint32
+ of PopupConstraintAdjustment.pcaResizeX:
+ `Xdg_positioner / Constraint_adjustment`.resize_x.uint32
+ of PopupConstraintAdjustment.pcaResizeY:
+ `Xdg_positioner / Constraint_adjustment`.resize_y.uint32
+ )
+
+proc createPopupPositioner(window: WindowWayland): Xdg_positioner =
+ let placement = window.m_popupPlacement
+ let parent = window.parentWindow().WindowWayland
+ let popupSize = parent.toLogicalSize(placement.popupSize())
+ result = window.globals.xdgWmBase.create_positioner()
+ result.set_size(popupSize.x, popupSize.y)
+ result.set_anchor_rect(
+ parent.toLogicalCoord(placement.anchorRectPos.x),
+ parent.toLogicalCoord(placement.anchorRectPos.y),
+ max(1'i32, parent.toLogicalSize(placement.anchorRectSize.x)),
+ max(1'i32, parent.toLogicalSize(placement.anchorRectSize.y)),
+ )
+ result.set_anchor(placement.anchor.toXdgPositionerAnchor())
+ result.set_gravity(placement.gravity.toXdgPositionerGravity())
+ result.set_constraint_adjustment(placement.constraintAdjustment.toXdgConstraintAdjustment())
+ result.set_offset(
+ parent.toLogicalCoord(placement.offset.x),
+ parent.toLogicalCoord(placement.offset.y),
+ )
+ if placement.reactive:
+ result.set_reactive()
+ if parent != nil:
+ result.set_parent_size(parent.m_size.x, parent.m_size.y)
+
proc resize(window: WindowWayland, size: IVec2) =
if size.x <= 0 or size.y <= 0:
@@ -636,6 +740,10 @@ proc resize(window: WindowWayland, size: IVec2) =
window.toplevelSetMaxSize(window.m_size.x, window.m_size.y)
window.eventsHandler.onResize.pushEvent ResizeEvent(window: window, size: window.size)
+ of WindowWaylandKind.PopupSurface:
+ if window.xdgSurface != nil:
+ window.xdgSurface.set_window_geometry(0, 0, window.m_size.x, window.m_size.y)
+ window.eventsHandler.onResize.pushEvent ResizeEvent(window: window, size: window.size)
of WindowWaylandKind.LayerSurface:
window.layerShellSurface.set_size(window.m_size.x.uint32, window.m_size.y.uint32)
@@ -681,6 +789,7 @@ proc setFrameless(window: WindowWayland, v: bool) =
method `fullscreen=`*(window: WindowWayland, v: bool) =
+ if window.kind != WindowWaylandKind.XdgSurface: return
if window.m_fullscreen == v: return
window.m_fullscreen = v
window.toplevelSetFullscreen(v)
@@ -689,6 +798,7 @@ method `fullscreen=`*(window: WindowWayland, v: bool) =
method `frameless=`*(window: WindowWayland, v: bool) =
if window.m_frameless == v: return
window.m_frameless = v
+ if window.kind != WindowWaylandKind.XdgSurface: return
if window.m_fullscreen: return # no system decorations needed for fullscreen windows
window.setFrameless(v)
@@ -728,6 +838,18 @@ method `cursor=`*(window: WindowWayland, v: Cursor) =
if v.kind == builtin and window.m_cursor.kind == builtin and v.builtin == window.m_cursor.builtin: return
## todo
+method reposition*(window: WindowWayland, v: PopupPlacement) =
+ procCall window.Window.reposition(v)
+ if window.kind != WindowWaylandKind.PopupSurface or window.xdgPopup == nil:
+ return
+
+ let positioner = window.createPopupPositioner()
+ defer: destroy positioner
+
+ inc window.popupRepositionToken
+ window.xdgPopup.reposition(positioner, window.popupRepositionToken)
+ redraw window
+
proc applyToplevelIcon(window: WindowWayland, icon: Xdg_toplevel_icon_v1) =
let manager = window.globals.xdgToplevelIconManager
if manager == nil or window.surface == nil:
@@ -811,6 +933,8 @@ method pixelBuffer*(window: WindowWaylandSoftwareRendering): PixelBuffer =
method swapBuffers(window: WindowWaylandSoftwareRendering) =
if window.m_closed:
return
+ if not window.softwarePresentEnabled:
+ return
if window.buffer == nil:
if window.m_size.x <= 0 or window.m_size.y <= 0:
@@ -827,6 +951,7 @@ method swapBuffers(window: WindowWaylandSoftwareRendering) =
method `maximized=`*(window: WindowWayland, v: bool) =
+ if window.kind != WindowWaylandKind.XdgSurface: return
if window.m_maximized == v: return
if window.fullscreen:
window.fullscreen = false
@@ -1018,7 +1143,7 @@ proc initSeatEvents*(globals: SiwinGlobalsWayland) =
globals.associatedWindows_queueRemove_insteadOf_removingInstantly = false
globals.removeQueuedAssociatedWindows()
- for window in globals.associatedWindows.values:
+ for window in globals.associatedWindows.values.toSeq():
let window = window.WindowWayland
if (
(window.mouse.pressed.len == 0) and
@@ -1049,7 +1174,7 @@ proc initSeatEvents*(globals: SiwinGlobalsWayland) =
globals.associatedWindows_queueRemove_insteadOf_removingInstantly = false
globals.removeQueuedAssociatedWindows()
- for window in globals.associatedWindows.values:
+ for window in globals.associatedWindows.values.toSeq():
let window = window.WindowWayland
if (
(state != `WlPointer / Button_state`.released or button notin window.mouse.pressed) and
@@ -1493,6 +1618,36 @@ proc setupWindow(window: WindowWayland, fullscreen, frameless, transparent: bool
if window.globals.plasmaShell != nil:
window.plasmaSurface = window.globals.plasmaShell.get_surface(window.surface)
+ of PopupSurface:
+ let parent = window.parentWindow().WindowWayland
+ if parent == nil:
+ raise ValueError.newException("Wayland popup windows require a parent window")
+ if parent.xdgSurface == nil:
+ raise ValueError.newException("Wayland popup parent must expose an xdg_surface")
+
+ window.xdgSurface = window.globals.xdgWmBase.get_xdg_surface(window.surface)
+ let positioner = window.createPopupPositioner()
+ window.xdgPopup = window.xdgSurface.get_popup(parent.xdgSurface, positioner)
+ destroy positioner
+
+ window.xdgSurface.onConfigure:
+ window.xdgSurface.ack_configure(serial)
+ redraw window
+
+ window.setupOpaqueRegion(size, transparent)
+ window.m_frameless = true
+
+ if window.m_popupGrab and window.globals.seat != nil and window.globals.lastSeatEventSerial != 0:
+ window.xdgPopup.grab(window.globals.seat, window.globals.lastSeatEventSerial)
+
+ window.xdgPopup.onPopup_done:
+ window.notifyPopupDone(PopupDismissReason.pdrCompositorDismissed)
+ window.m_closed = true
+
+ window.xdgPopup.onConfigure:
+ window.m_pos = parent.pos + window.reportedPos(ivec2(x, y))
+ window.resize(ivec2(width, height))
+
of LayerSurface:
window.layerShellSurface = window.globals.layerShell.get_layer_surface(
window.surface,
@@ -1875,8 +2030,45 @@ proc newSoftwareRenderingWindowWayland*(
): WindowWaylandSoftwareRendering =
new result
result.globals = globals
+ result.softwarePresentEnabled = true
result.initSoftwareRenderingWindow(size, screen, fullscreen, frameless, transparent, (if class == "": title else: class))
result.title = title
if not resizable: result.resizable = false
+proc newPopupWindowWayland*(
+ globals: SiwinGlobalsWayland,
+ parent: WindowWayland,
+ placement: PopupPlacement,
+ transparent = false,
+ grab = true,
+): WindowWaylandSoftwareRendering =
+ if parent == nil:
+ raise ValueError.newException("Popup windows require a parent window")
+ let logicalPopupSize = parent.toLogicalSize(placement.popupSize())
+ new result
+ result.globals = globals
+ result.softwarePresentEnabled = true
+ result.kind = WindowWaylandKind.PopupSurface
+ result.basicInitWindow(logicalPopupSize, globals.defaultScreenWayland())
+ result.initPopupState(parent, placement, grab)
+ result.m_size = logicalPopupSize
+ result.setupWindow(
+ fullscreen = false,
+ frameless = true,
+ transparent = transparent,
+ size = logicalPopupSize,
+ class = "",
+ )
+ result.buffer = result.globals.create(
+ result.globals.shm,
+ result.bufferSize(logicalPopupSize),
+ (if transparent: argb8888 else: xrgb8888),
+ bufferCount = 2,
+ )
+ result.pos = parent.pos + placement.popupRelativePos()
+ result.visible = true
+
+proc setSoftwarePresentEnabled*(window: WindowWaylandSoftwareRendering, enabled: bool) =
+ window.softwarePresentEnabled = enabled
+
export Layer, LayerEdge, LayerInteractivityMode
diff --git a/src/siwin/platforms/winapi/window.nim b/src/siwin/platforms/winapi/window.nim
index 03d56dc..f66b30a 100644
--- a/src/siwin/platforms/winapi/window.nim
+++ b/src/siwin/platforms/winapi/window.nim
@@ -794,3 +794,20 @@ proc newSoftwareRenderingWindowWinapi*(
result.initWindow(size, screen, fullscreen, frameless, transparent)
result.title = title
if not resizable: result.resizable = false
+
+proc newPopupWindowWinapi*(
+ parent: WindowWinapi, placement: PopupPlacement, transparent = false, grab = true
+): WindowWinapiSoftwareRendering =
+ if parent == nil:
+ raise ValueError.newException("Popup windows require a parent window")
+ result = newSoftwareRenderingWindowWinapi(
+ size = placement.popupSize(),
+ title = "",
+ screen = defaultScreenWinapi(),
+ resizable = false,
+ fullscreen = false,
+ frameless = true,
+ transparent = transparent,
+ )
+ result.initPopupState(parent, placement, grab)
+ result.pos = parent.pos + placement.popupRelativePos()
diff --git a/src/siwin/platforms/x11/siwinGlobals.nim b/src/siwin/platforms/x11/siwinGlobals.nim
index ca54770..4b115ab 100644
--- a/src/siwin/platforms/x11/siwinGlobals.nim
+++ b/src/siwin/platforms/x11/siwinGlobals.nim
@@ -17,7 +17,7 @@ type
atoms*: tuple[
frameless, wmDeleteWindow, utf8String, netWmName, netWmIconName,
netWmState, netWmStateFullscreen, netWmStateMaximizedHorz, netWmStateMaximizedVert, netWmStateHidden, netWmMoveResize,
- netWmSyncRequest, netWmSyncRequestCounter,
+ netWmSyncRequest, netWmSyncRequestCounter, netFrameExtents,
clipboard, siwin_clipboardTargetProperty, targets, text, primary,
xDndAware, xDndEnter, xDndTypeList, xDndSelection, xDndPosition, xDndLeave, xDndDrop, xDndFinished, xDndStatus, xDndActionCopy, xDndActionPrivate
: Atom
@@ -56,6 +56,7 @@ proc newX11Globals*: SiwinGlobalsX11 {.raises: [OsError].} =
result.atoms.netWmMoveResize = result.display.XInternAtom("_NET_WM_MOVERESIZE", 0)
result.atoms.netWmSyncRequest = result.display.XInternAtom("_NET_WM_SYNC_REQUEST", 0)
result.atoms.netWmSyncRequestCounter = result.display.XInternAtom("_NET_WM_SYNC_REQUEST_COUNTER", 0)
+ result.atoms.netFrameExtents = result.display.XInternAtom("_NET_FRAME_EXTENTS", 0)
result.atoms.clipboard = result.display.XInternAtom("CLIPBOARD", 0)
result.atoms.siwin_clipboardTargetProperty = result.display.XInternAtom("siwin_clipboardTargetProperty", 0)
result.atoms.targets = result.display.XInternAtom("TARGETS", 0)
diff --git a/src/siwin/platforms/x11/window.nim b/src/siwin/platforms/x11/window.nim
index 5f93714..800faf4 100644
--- a/src/siwin/platforms/x11/window.nim
+++ b/src/siwin/platforms/x11/window.nim
@@ -1,7 +1,7 @@
when not (compiles do: import pkg/x11/xutil):
{.error: "x11 library not installed, required to cross compile to linux\n please run `nimble install x11`".}
-import std/[times, importutils, strformat, sequtils, os, options, tables, uri, strutils]
+import std/[times, importutils, strformat, sequtils, os, options, tables, uri, strutils, dynlib]
import pkg/[vmath, chroma]
import pkg/x11/xlib except Screen
import pkg/x11/x except Window, Cursor, Time
@@ -87,12 +87,46 @@ type
pixels: pointer
softwarePresentEnabled*: bool
+ PXineramaScreenInfo = ptr XineramaScreenInfo
+ XineramaScreenInfo = object
+ screen_number: cint
+ x_org: int16
+ y_org: int16
+ width: int16
+ height: int16
+
+ XineramaIsActiveProc = proc(dpy: PDisplay): XBool {.cdecl.}
+ XineramaQueryScreensProc = proc(dpy: PDisplay, number: Pcint): PXineramaScreenInfo {.cdecl.}
+
+var
+ xineramaLib: LibHandle
+ xineramaLoadAttempted = false
+ xineramaIsActiveProc: XineramaIsActiveProc
+ xineramaQueryScreensProc: XineramaQueryScreensProc
+
proc nativeDisplayHandle*(window: WindowX11): pointer =
cast[pointer](window.globals.display)
proc nativeWindowHandle*(window: WindowX11): uint64 =
cast[uint64](window.handle)
+proc xineramaAvailable(): bool =
+ if xineramaLoadAttempted:
+ return xineramaLib != nil and xineramaIsActiveProc != nil and xineramaQueryScreensProc != nil
+
+ xineramaLoadAttempted = true
+ for libname in ["libXinerama.so.1", "libXinerama.so"]:
+ xineramaLib = loadLib(libname)
+ if xineramaLib != nil:
+ break
+
+ if xineramaLib == nil:
+ return false
+
+ xineramaIsActiveProc = cast[XineramaIsActiveProc](symAddr(xineramaLib, "XineramaIsActive"))
+ xineramaQueryScreensProc = cast[XineramaQueryScreensProc](symAddr(xineramaLib, "XineramaQueryScreens"))
+ xineramaIsActiveProc != nil and xineramaQueryScreensProc != nil
+
const libXExt* =
when defined(macosx):
@@ -303,6 +337,46 @@ proc geometry(globals: SiwinGlobalsX11, xwin: x.Window): tuple[root: x.Window; p
discard globals.display.XGetGeometry(xwin, root.addr, x.addr, y.addr, w.addr, h.addr, borderW.addr, depth.addr)
(root, ivec2(x.int32, y.int32), ivec2(w.int32, h.int32), borderW.int, depth.int)
+proc absolutePos(globals: SiwinGlobalsX11, xwin: x.Window): IVec2 =
+ let geom = globals.geometry(xwin)
+ var
+ child: x.Window
+ rootX, rootY: cint
+ discard globals.display.XTranslateCoordinates(
+ xwin,
+ geom.root,
+ 0,
+ 0,
+ rootX.addr,
+ rootY.addr,
+ child.addr,
+ )
+ ivec2(rootX.int32, rootY.int32)
+
+proc popupConstraintBounds(globals: SiwinGlobalsX11, anchorPos: IVec2): tuple[pos, size: IVec2] =
+ result = (ivec2(0, 0), globals.geometry(globals.display.DefaultRootWindow).size)
+
+ if not xineramaAvailable():
+ return
+
+ if xineramaIsActiveProc(globals.display) == 0:
+ return
+
+ var screenCount: cint
+ let screens = xineramaQueryScreensProc(globals.display, screenCount.addr)
+ if screens == nil or screenCount <= 0:
+ return
+ defer:
+ discard XFree(screens)
+
+ for i in 0 ..< screenCount:
+ let screen = cast[ptr UncheckedArray[XineramaScreenInfo]](screens)[i]
+ let pos = ivec2(screen.x_org.int32, screen.y_org.int32)
+ let size = ivec2(screen.width.int32, screen.height.int32)
+ if anchorPos.x >= pos.x and anchorPos.x < pos.x + size.x and
+ anchorPos.y >= pos.y and anchorPos.y < pos.y + size.y:
+ return (pos, size)
+
proc asXImage(globals: SiwinGlobalsX11, data: pointer, size: IVec2, transparent = false): XImage = XImage(
width: cint size.x,
height: cint size.y,
@@ -389,7 +463,6 @@ proc pushEvent[T](event: proc(e: T), args: T) =
proc resizePixelBuffer(window: WindowX11SoftwareRendering, size: IVec2) =
window.pixels = window.pixels.realloc(size.x * size.y * Color32bit.sizeof)
-
proc basicInitWindow(window: WindowX11; size: IVec2; screen: ScreenX11) =
window.screen = screen.id
window.m_size = size
@@ -568,8 +641,30 @@ method `size=`*(window: WindowX11, v: IVec2) =
method `pos=`*(window: WindowX11, v: IVec2) =
if window.m_fullscreen: return
+ window.m_pos = v
discard window.globals.display.XMoveWindow(window.handle, v.x.cint, v.y.cint)
+method reposition*(window: WindowX11, v: PopupPlacement) =
+ procCall window.Window.reposition(v)
+ if not window.isPopup:
+ return
+
+ let parent = window.parentWindow().WindowX11
+ if parent == nil:
+ return
+
+ let parentPos = parent.globals.absolutePos(parent.handle)
+ let bounds = parent.globals.popupConstraintBounds(parentPos + v.anchorRectPos)
+ let rect = resolvePopupRect(
+ parentPos,
+ bounds.pos,
+ bounds.size,
+ v,
+ )
+ if window.m_size != rect.size:
+ window.size = rect.size
+ window.pos = rect.pos
+
proc setX11Cursor(window: WindowX11, v: Cursor) =
if window.xCursor != 0:
@@ -1023,7 +1118,7 @@ method firstStep*(window: WindowX11, makeVisible = true) =
if makeVisible:
window.visible = true
- window.m_pos = window.globals.geometry(window.handle).pos
+ window.m_pos = window.globals.absolutePos(window.handle)
window.mouse.pos = (window.globals.cursor().pos - window.m_pos).vec2
if window of WindowX11SoftwareRendering:
@@ -1191,8 +1286,9 @@ method step*(window: WindowX11) =
window.WindowX11SoftwareRendering.resizePixelBuffer(window.m_size)
window.eventsHandler.onResize.pushEvent ResizeEvent(window: window, size: window.m_size, initial: false)
- if ev.xconfigure.x.int != window.m_pos.x or ev.xconfigure.y.int != window.m_pos.y:
- window.m_pos = ivec2(ev.xconfigure.x.int32, ev.xconfigure.y.int32)
+ let absolutePos = window.globals.absolutePos(window.handle)
+ if absolutePos != window.m_pos:
+ window.m_pos = absolutePos
window.mouse.pos = (window.globals.cursor().pos - window.m_pos).vec2
window.eventsHandler.onWindowMove.pushEvent WindowMoveEvent(window: window, pos: window.m_pos)
@@ -1525,5 +1621,69 @@ proc newSoftwareRenderingWindowX11*(
result.title = title
if not resizable: result.resizable = false
+proc newPopupWindowX11*(
+ globals: SiwinGlobalsX11,
+ parent: WindowX11,
+ placement: PopupPlacement,
+ transparent = false,
+ grab = true,
+): WindowX11SoftwareRendering =
+ if parent == nil:
+ raise ValueError.newException("Popup windows require a parent window")
+ new result
+ result.softwarePresentEnabled = true
+ result.globals = globals
+ let screen = globals.defaultScreenX11()
+ result.basicInitWindow(placement.popupSize(), screen)
+
+ if transparent:
+ result.m_transparent = true
+ let root = globals.display.DefaultRootWindow
+
+ var vi: XVisualInfo
+ discard globals.display.XMatchVisualInfo(screen.id, 32, TrueColor, vi.addr)
+
+ let cmap = globals.display.XCreateColormap(root, vi.visual, AllocNone)
+ var swa = XSetWindowAttributes(
+ colormap: cmap,
+ override_redirect: 1,
+ save_under: 1,
+ )
+
+ result.handle = globals.display.XCreateWindow(
+ root, 0, 0, placement.popupSize().x.cuint, placement.popupSize().y.cuint, 0,
+ vi.depth, InputOutput, vi.visual,
+ CwColormap or CWOverrideRedirect or CWSaveUnder,
+ swa.addr
+ )
+ else:
+ var swa = XSetWindowAttributes(
+ override_redirect: 1,
+ save_under: 1,
+ background_pixel: globals.display.BlackPixel(screen.id),
+ border_pixel: 0,
+ )
+ result.handle = globals.display.XCreateWindow(
+ globals.display.DefaultRootWindow, 0, 0, placement.popupSize().x.cuint, placement.popupSize().y.cuint, 0,
+ CopyFromParent, InputOutput, nil,
+ CWOverrideRedirect or CWSaveUnder or CWBackPixel or CWBorderPixel,
+ swa.addr
+ )
+
+ result.setupWindow(fullscreen = false, frameless = true, class = "")
+ result.gc.gc = globals.display.XCreateGC(result.handle, GCForeground or GCBackground, result.gc.gcv.addr)
+ result.initPopupState(parent, placement, grab)
+ let parentPos = globals.absolutePos(parent.handle)
+ let bounds = globals.popupConstraintBounds(parentPos + placement.anchorRectPos)
+ let rect = resolvePopupRect(
+ parentPos,
+ bounds.pos,
+ bounds.size,
+ placement,
+ )
+ if result.m_size != rect.size:
+ result.m_size = rect.size
+ result.pos = rect.pos
+
proc setSoftwarePresentEnabled*(window: WindowX11SoftwareRendering, enabled: bool) =
window.softwarePresentEnabled = enabled
diff --git a/src/siwin/window.nim b/src/siwin/window.nim
index bc7098e..dd61252 100644
--- a/src/siwin/window.nim
+++ b/src/siwin/window.nim
@@ -119,6 +119,27 @@ when not siwin_use_lib:
resizable, fullscreen, frameless, transparent
)
+ proc newPopupWindow*(
+ globals: SiwinGlobals,
+ parent: Window,
+ placement: PopupPlacement,
+ transparent = false,
+ grab = true,
+ ): PopupWindow =
+ when defined(android):
+ raise SiwinPlatformSupportDefect.newException("Popup windows are not supported on Android")
+ elif defined(linux) or defined(bsd):
+ if globals of SiwinGlobalsX11:
+ result = globals.SiwinGlobalsX11.newPopupWindowX11(parent.WindowX11, placement, transparent, grab)
+ elif globals of SiwinGlobalsWayland:
+ result = globals.SiwinGlobalsWayland.newPopupWindowWayland(parent.WindowWayland, placement, transparent, grab)
+ else:
+ raise SiwinPlatformSupportDefect.newException("Unsupported platform")
+ elif defined(windows):
+ result = newPopupWindowWinapi(parent.WindowWinapi, placement, transparent, grab)
+ elif defined(macosx):
+ result = newPopupWindowCocoa(parent.WindowCocoa, placement, transparent, grab)
+
when defined(android):
proc loadExtensions*() =
diff --git a/tests/tpopup_api.nim b/tests/tpopup_api.nim
new file mode 100644
index 0000000..55a66e7
--- /dev/null
+++ b/tests/tpopup_api.nim
@@ -0,0 +1,308 @@
+import unittest
+import std/math
+import std/[os, osproc, strutils]
+import std/strtabs
+import pkg/vmath
+import siwin/[window, platforms]
+
+when defined(macosx):
+ import siwin/platforms/cocoa/window
+
+proc toPoints(distance: int32, scale: float32): int32 =
+ if scale <= 0'f32:
+ return distance
+ round(distance.float64 / scale.float64).int32
+
+proc toPoints(size: IVec2, scale: float32): IVec2 =
+ ivec2(toPoints(size.x, scale), toPoints(size.y, scale))
+
+proc scalePlacementToPoints(placement: PopupPlacement, scale: float32): PopupPlacement =
+ PopupPlacement(
+ anchorRectPos: placement.anchorRectPos.toPoints(scale),
+ anchorRectSize: placement.anchorRectSize.toPoints(scale),
+ size: placement.size.toPoints(scale),
+ anchor: placement.anchor,
+ gravity: placement.gravity,
+ offset: placement.offset.toPoints(scale),
+ constraintAdjustment: placement.constraintAdjustment,
+ reactive: placement.reactive,
+ )
+
+proc stepUntil(window: Window, predicate: proc(): bool, maxSteps = 32) =
+ for _ in 0 ..< maxSteps:
+ if predicate():
+ break
+ window.step()
+
+type PopupProbeResult = object
+ platform: string
+ relPos: IVec2
+ reportedSize: IVec2
+ uiScale: float32
+
+proc maxAbsComponent(v: IVec2): int32 =
+ max(abs(v.x), abs(v.y))
+
+proc parseIvec2(value: string): IVec2 =
+ let parts = value.split(',')
+ if parts.len != 2:
+ raise newException(ValueError, "invalid ivec2: " & value)
+ ivec2(parseInt(parts[0]).int32, parseInt(parts[1]).int32)
+
+proc parsePopupProbeResult(output: string): PopupProbeResult =
+ for line in output.splitLines():
+ if not line.startsWith("POPUP_RESULT "):
+ continue
+
+ for field in line["POPUP_RESULT ".len .. ^1].splitWhitespace():
+ let kv = field.split('=', maxsplit = 1)
+ if kv.len != 2:
+ continue
+ case kv[0]
+ of "platform":
+ result.platform = kv[1]
+ of "relPos":
+ result.relPos = parseIvec2(kv[1])
+ of "reportedSize":
+ result.reportedSize = parseIvec2(kv[1])
+ of "uiScale":
+ result.uiScale = parseFloat(kv[1]).float32
+ else:
+ discard
+
+ if result.platform.len != 0:
+ return
+
+ raise newException(ValueError, "missing POPUP_RESULT line")
+
+proc popupProbePlacement(): PopupPlacement =
+ PopupPlacement(
+ anchorRectPos: ivec2(40, 100),
+ anchorRectSize: ivec2(120, 34),
+ size: ivec2(520, 420),
+ anchor: bottomLeft,
+ gravity: topLeft,
+ offset: ivec2(0, 14),
+ constraintAdjustment: {},
+ reactive: false,
+ )
+
+proc runPopupProbe(platform: Platform) =
+ let globals = newSiwinGlobals(platform)
+ let parent = globals.newSoftwareRenderingWindow(
+ size = ivec2(780, 630),
+ title = "popup probe parent",
+ )
+ parent.firstStep(makeVisible = true)
+
+ var
+ lastParentPos = ivec2(low(int32), low(int32))
+ parentStableSteps = 0
+
+ for _ in 0 ..< 120:
+ parent.step()
+ if parent.pos == lastParentPos:
+ inc parentStableSteps
+ else:
+ parentStableSteps = 0
+ lastParentPos = parent.pos
+ if parentStableSteps >= 8:
+ break
+
+ let placement = popupProbePlacement()
+ let popup = globals.newPopupWindow(parent, placement, grab = true)
+ popup.firstStep(makeVisible = true)
+
+ var
+ lastRelPos = ivec2(low(int32), low(int32))
+ lastReportedSize = ivec2(low(int32), low(int32))
+ stableSteps = 0
+
+ for _ in 0 ..< 180:
+ parent.step()
+ popup.step()
+
+ let relPos = popup.pos - parent.pos
+ let reportedSize = popup.size
+ if relPos == lastRelPos and reportedSize == lastReportedSize:
+ inc stableSteps
+ else:
+ stableSteps = 0
+ lastRelPos = relPos
+ lastReportedSize = reportedSize
+
+ if stableSteps >= 8:
+ break
+
+ echo "POPUP_RESULT platform=", $platform, " relPos=", lastRelPos.x, ",", lastRelPos.y,
+ " reportedSize=", lastReportedSize.x, ",", lastReportedSize.y,
+ " uiScale=", popup.uiScale
+
+ close popup
+ close parent
+ quit(0)
+
+proc runPopupProbeSubprocess(platform: Platform; waylandDebug = false): tuple[output: string, exitCode: int] =
+ var env = newStringTable()
+ for key, value in envPairs():
+ env[key] = value
+ env["SIWIN_POPUP_TEST_HELPER"] = $platform
+ if waylandDebug:
+ env["WAYLAND_DEBUG"] = "1"
+ execCmdEx(getAppFilename().quoteShell & " --popup-runtime-helper", env = env)
+
+when defined(linux) or defined(bsd):
+ if getEnv("SIWIN_POPUP_TEST_HELPER").len != 0 and paramCount() > 0 and paramStr(1) == "--popup-runtime-helper":
+ runPopupProbe(parseEnum[Platform](getEnv("SIWIN_POPUP_TEST_HELPER")))
+
+suite "siwin popup api":
+ test "popup placement resolves size and relative position":
+ let placement = PopupPlacement(
+ anchorRectPos: ivec2(20, 30),
+ anchorRectSize: ivec2(80, 24),
+ size: ivec2(120, 200),
+ anchor: bottomLeft,
+ gravity: topLeft,
+ offset: ivec2(3, 4),
+ constraintAdjustment:
+ {PopupConstraintAdjustment.pcaSlideX, PopupConstraintAdjustment.pcaFlipY},
+ reactive: true,
+ )
+
+ check placement.popupSize() == ivec2(120, 200)
+ check placement.popupRelativePos() == ivec2(23, 58)
+
+ test "popup placement falls back to anchor rect size":
+ let placement = PopupPlacement(
+ anchorRectPos: ivec2(10, 12),
+ anchorRectSize: ivec2(40, 18),
+ anchor: topRight,
+ gravity: bottomRight,
+ offset: ivec2(-2, -3),
+ )
+
+ check placement.popupSize() == ivec2(40, 18)
+ check placement.popupRelativePos() == ivec2(8, -9)
+
+ when defined(macosx):
+ test "cocoa firstStep syncs initial window position":
+ let globals = newSiwinGlobals(Platform.cocoa)
+ let parent = globals.newSoftwareRenderingWindow(
+ size = ivec2(300, 200),
+ title = "popup parent initial pos",
+ )
+ parent.firstStep(makeVisible = true)
+ parent.step()
+
+ check parent.pos == parent.WindowCocoa.framePos()
+
+ close parent
+
+ test "cocoa popup placement uses parent content position":
+ let globals = newSiwinGlobals(Platform.cocoa)
+ let parent = globals.newSoftwareRenderingWindow(
+ size = ivec2(300, 200),
+ title = "popup parent",
+ )
+ parent.pos = ivec2(140, 160)
+ parent.firstStep(makeVisible = false)
+ parent.step()
+
+ let scale = parent.uiScale
+ let initialPlacement = PopupPlacement(
+ anchorRectPos: ivec2(parent.size.x div 3, parent.size.y div 4),
+ anchorRectSize: ivec2(parent.size.x div 5, 48),
+ size: ivec2(240, 180),
+ anchor: bottomRight,
+ gravity: topLeft,
+ offset: ivec2(0, 14),
+ )
+ let popup = globals.newPopupWindow(parent, initialPlacement, grab = true)
+ popup.firstStep(makeVisible = false)
+ popup.step()
+
+ check popup.pos == parent.WindowCocoa.contentPos() + initialPlacement.popupRelativePos().toPoints(scale)
+ check popup.size == initialPlacement.popupSize()
+
+ let updatedPlacement = PopupPlacement(
+ anchorRectPos: ivec2(parent.size.x div 2, parent.size.y div 3),
+ anchorRectSize: ivec2(96, 56),
+ size: ivec2(140, 110),
+ anchor: bottomRight,
+ gravity: topRight,
+ offset: ivec2(-8, 10),
+ )
+ popup.reposition(updatedPlacement)
+ popup.step()
+
+ check popup.pos == parent.WindowCocoa.contentPos() + updatedPlacement.popupRelativePos().toPoints(scale)
+ check popup.size == updatedPlacement.popupSize()
+
+ close popup
+ close parent
+
+ when defined(linux) or defined(bsd):
+ test "wayland and x11 popup final geometry match for popup probe":
+ if Platform.wayland notin availablePlatforms() or Platform.x11 notin availablePlatforms():
+ skip()
+
+ let wayland = runPopupProbeSubprocess(Platform.wayland, waylandDebug = true)
+ require wayland.exitCode == 0
+ check "xdg_popup" in wayland.output
+ check "configure(" in wayland.output
+
+ let x11 = runPopupProbeSubprocess(Platform.x11)
+ require x11.exitCode == 0
+
+ let waylandResult = parsePopupProbeResult(wayland.output)
+ let x11Result = parsePopupProbeResult(x11.output)
+ check maxAbsComponent(waylandResult.relPos - x11Result.relPos) <= 1
+ check maxAbsComponent(waylandResult.reportedSize - x11Result.reportedSize) <= 1
+
+ test "x11 popup window position matches relative placement in unconstrained case":
+ if Platform.x11 notin availablePlatforms():
+ skip()
+
+ let globals = newSiwinGlobals(Platform.x11)
+ let parent = globals.newSoftwareRenderingWindow(
+ size = ivec2(300, 200),
+ title = "popup parent",
+ )
+ parent.firstStep(makeVisible = false)
+ parent.step()
+
+ let placement = PopupPlacement(
+ anchorRectPos: ivec2(40, 48),
+ anchorRectSize: ivec2(96, 32),
+ size: ivec2(140, 110),
+ anchor: bottomLeft,
+ gravity: topLeft,
+ offset: ivec2(0, 14),
+ )
+ let popup = globals.newPopupWindow(parent, placement, grab = true)
+ popup.firstStep(makeVisible = false)
+ popup.stepUntil(proc(): bool = popup.pos == parent.pos + placement.popupRelativePos() and popup.size == placement.popupSize())
+
+ check popup.pos == parent.pos + placement.popupRelativePos()
+ check popup.size == placement.popupSize()
+
+ let updatedPlacement = PopupPlacement(
+ anchorRectPos: ivec2(120, 76),
+ anchorRectSize: ivec2(84, 28),
+ size: ivec2(120, 96),
+ anchor: topRight,
+ gravity: bottomRight,
+ offset: ivec2(-6, -8),
+ )
+ popup.reposition(updatedPlacement)
+ popup.stepUntil(
+ proc(): bool =
+ popup.pos == parent.pos + updatedPlacement.popupRelativePos() and
+ popup.size == updatedPlacement.popupSize()
+ )
+
+ check popup.pos == parent.pos + updatedPlacement.popupRelativePos()
+ check popup.size == updatedPlacement.popupSize()
+
+ close popup
+ close parent