Initial iris repository structure
Some checks failed
CI / build (push) Failing after 36s

WASM reactive UI framework for Go:
- reactive/ - Signal[T], Effect, Runtime
- ui/ - Button, Text, Input, View, Canvas, SVG components
- navigation/ - Router, guards, history management
- auth/ - OIDC client for WASM applications
- host/ - Static file server

Extracted from arcadia as open-source component.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-01-08 19:23:49 +01:00
commit 00d98879d3
36 changed files with 4181 additions and 0 deletions

144
ui/canvas_geometry.go Normal file
View File

@@ -0,0 +1,144 @@
//go:build js && wasm
package ui
import (
"sort"
"git.flowmade.one/flowmade-one/iris/internal/element"
)
// snapToGrid snaps a position to the grid if enabled
func snapToGrid(pos Point, gridSize int) Point {
if gridSize <= 0 {
return pos
}
return Point{
X: float64(int(pos.X/float64(gridSize)+0.5) * gridSize),
Y: float64(int(pos.Y/float64(gridSize)+0.5) * gridSize),
}
}
// findItemUnderPoint finds which canvas item is under the given screen coordinates
// Returns the item with highest ZIndex that contains the point
func findItemUnderPoint(state *CanvasState, viewport element.Element, clientX, clientY float64) string {
// Get viewport bounds
rect := viewport.Call("getBoundingClientRect")
viewportLeft := rect.Get("left").Float()
viewportTop := rect.Get("top").Float()
// Convert to canvas coordinates
x := clientX - viewportLeft
y := clientY - viewportTop
canvasPos := state.ScreenToCanvas(Point{X: x, Y: y})
// Sort items by ZIndex descending (higher values first = top items first)
items := state.Items.Get()
sortedItems := make([]CanvasItem, len(items))
copy(sortedItems, items)
sort.Slice(sortedItems, func(i, j int) bool {
return sortedItems[i].ZIndex > sortedItems[j].ZIndex
})
// Check items in z-order (top to bottom)
for _, item := range sortedItems {
if canvasPos.X >= item.Position.X && canvasPos.X <= item.Position.X+item.Size.X &&
canvasPos.Y >= item.Position.Y && canvasPos.Y <= item.Position.Y+item.Size.Y {
return item.ID
}
}
return ""
}
// isNearEdge checks if a click position is near the edge of an element
func isNearEdge(clickX, clickY, elemWidth, elemHeight float64) bool {
edgeThreshold := DefaultCanvasDefaults.EdgeThreshold
return clickX < edgeThreshold ||
clickX > elemWidth-edgeThreshold ||
clickY < edgeThreshold ||
clickY > elemHeight-edgeThreshold
}
// calculateEdgePoint calculates the edge point for a connection based on click position
func calculateEdgePoint(item CanvasItem, clickX, clickY, elemWidth, elemHeight float64) Point {
// Calculate center of element
centerX := item.Position.X + item.Size.X/2
centerY := item.Position.Y + item.Size.Y/2
// Determine which edge is closest
distLeft := clickX
distRight := elemWidth - clickX
distTop := clickY
distBottom := elemHeight - clickY
minDist := distLeft
edgeX := item.Position.X
edgeY := centerY
if distRight < minDist {
minDist = distRight
edgeX = item.Position.X + item.Size.X
edgeY = centerY
}
if distTop < minDist {
minDist = distTop
edgeX = centerX
edgeY = item.Position.Y
}
if distBottom < minDist {
edgeX = centerX
edgeY = item.Position.Y + item.Size.Y
}
return Point{X: edgeX, Y: edgeY}
}
// calculateConnectionEdgePoint calculates where a line from center to target
// should intersect with the element's bounding box
func calculateConnectionEdgePoint(center, target, size Point) Point {
dx := target.X - center.X
dy := target.Y - center.Y
// Handle edge case of overlapping centers
if dx == 0 && dy == 0 {
return center
}
// Calculate intersection with bounding box
halfW := size.X / 2
halfH := size.Y / 2
// Check which edge we intersect
if dx == 0 {
// Vertical line
if dy > 0 {
return Point{X: center.X, Y: center.Y + halfH}
}
return Point{X: center.X, Y: center.Y - halfH}
}
slope := dy / dx
// Check horizontal edges
if abs(slope) <= halfH/halfW {
// Intersects left or right edge
if dx > 0 {
return Point{X: center.X + halfW, Y: center.Y + slope*halfW}
}
return Point{X: center.X - halfW, Y: center.Y - slope*halfW}
}
// Intersects top or bottom edge
if dy > 0 {
return Point{X: center.X + halfH/slope, Y: center.Y + halfH}
}
return Point{X: center.X - halfH/slope, Y: center.Y - halfH}
}
// abs returns the absolute value of x
func abs(x float64) float64 {
if x < 0 {
return -x
}
return x
}