Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added docs/center.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
80 changes: 80 additions & 0 deletions docs/layout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Layout in Silky

Layout in Silky is a little different because it uses an immediate mode UI. Some layouts just are not possible without custom math since in immediate mode you have to know the position of every widget as you draw it. That is how immediate mode works. This limits what layouts you can do, but it also makes reasoning about them simpler. The layout system is more straightforward and easier to think about because the problem space is smaller and more constrained.

## The pen and stretch pen.

![Pen and stretch pen](pen.png)

Think about how the layout works. Imagine a "pen" that decides where the next widget goes. You create a parent element, and it has padding. Because of that padding, the pen moves inward. Then you add a child element. The child moves the pen depending on whether the parent layout is vertical or horizontal. In a vertical layout, the pen advances by the child's height. In a horizontal layout, it advances by the child's width.

After moving the pen, you also need to account for parent item spacing, which is how much space sits between children. This spacing depends on whether the layout is vertical or horizontal and must be applied accordingly.

There is also a second conceptual pen, the "stretch pen." When you place a child layout inside a parent, it stretches the parent's size. Both the width and height may grow depending on the child's dimensions. At the end, when the parent box is closed and drawn, the parent padding is added again to this accumulated stretch size.

## Stretch and sizing layouts.

Stretch to fit layouts that require knowing the sizes of all children in advance are tricky in immediate mode UIs. Withoug scrafacing perf of frame independence, you cannot look ahead to compute total child sizes before drawing them. Layouts that depend on precomputed child measurements simply do not work here.

![Stretch pen](size.png)

What *is* possible:

* **Fixed size layouts**, because all sizes are known ahead of time.
* **Fill parent layouts**, because the parent's size is already known and children can expand to match it.

When children stretch a parent, they expand its **inner dimensions**. If the inner dimensions exceed the parent's outer dimensions, a scrollbar appears. Scrollbars can be enabled or disabled depending on your needs.

Stretching can happen independently in the X and Y directions. For example:

* Fixed size in X and fill parent in Y
* Fill parent in X and fixed size in Y
* Or any combination of the two

## Stacking direction and anchoring.

Next is the **stacking direction**, which is very flexible. Stacking direction is handled by reversing signs in the layout math. The underlying logic stays the same.

![Stacking direction](stack.png)

There is also the concept of **anchoring**, which determines where stacking begins. You can anchor at a side and stack:

* Anchor to Top
* Stack Left to right
* Stack Right to left
* Anchor to Bottom
* Stack Left to right
* Stack Right to left
* Anchor to Left
* Stack Top to bottom
* Stack Bottom to top
* Anchor to Right
* Stack Top to bottom
* Stack Bottom to top

Most UIs anchor on the left and stack from top to bottom. That is the default and most common layout style. But you could build something like a chat application that anchors at the bottom and stacks upward, since new chat bubbles appear there. You can also create panels that stack controls inward from different edges to organize screen layout.


## Performance considerations.

The layouts are constrained not because they are hard to build, but because of performance. In theory, we could precompute child layouts, throw away the actual widgets, and keep only their sizes. But that would add extra computation. This goes against the core philosophy of Silky, which prioritizes maximum performance.

Yes, we could add precomputation and make the layout system more flexible, but we chose not to on purpose. The goal is to keep the UI as fast as possible. The "holy grail" of immediate mode UI is speed: no extra bookkeeping, no unnecessary memory storage, no redundant calculations.

In immediate mode, drawing the UI is almost like using print statements. You emit the widgets, they get rendered for that frame, and then they are gone. There is no retained tree structure or persistent layout state. This simplicity is what makes immediate mode UIs so fast.

That is the main reason the layouts are constrained: not because of technical limitations, but because of a deliberate design choice in favor of performance.

You might say, "I want this layout or that layout, I want to do this and that." In practice, you usually can.

In Silky, everything supports explicit X, Y, and position settings. That means you can use your own math formulas, often much simpler than trying to solve a generalized layout puzzle, to compute exactly where things should go based on what you know about your UI and your data.

For example, you often know how many elements are in a list, and you can figure out their height from the data. With a little upfront math and some simple heuristics, you can compute the sizes and positions of even fairly complex layouts without building an elaborate layout system or doing extra passes.

Then, when it is time to draw, you just plug in the computed numbers for X, Y, width, and height.

In my experience, this works better than a complicated layout system. The code makes it obvious where everything goes, and it is usually much faster.

I feel that explicit math formulas work better because they clearly express intent. In more complicated layout systems, the final layout can feel accidental, as if many separate pieces of code happen to combine and produce the result. It can seem like the layout emerges from a web of interactions rather than from a clear, deliberate decision.

With the formula based approach, you write exactly what you want. You understand your data, you compute the layout directly, and then you apply it. The result is much more intentional. The positioning logic is explicit, readable, and grounded in the actual structure of your UI rather than being the byproduct of a complex layout engine.
Binary file added docs/pen.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
152 changes: 152 additions & 0 deletions docs/sementic_testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
## Semantic testing for Silky UI

This document explains the idea behind semantic UI testing in Silky, why it helps writing tests and AI assisted development.

## The testing problem

Normal tests and AI tools are not great at verifying GUI output when they must rely on pixels.

- Pixel based checks are slow and expensive.
- GUI automation usually needs a full window, GPU, and frame rendering loop.
- Most AI verification tools are stronger with text than with images.
- For immediate mode GUI, dumping every frame is noisy because frames update constantly.

In short: we need a fast, text first way to inspect and test UI behavior.

## Core idea ui semantic mode

Compile and run Silky apps in a semantic testing mode that captures what the UI logically renders instead of what it rasterizes.

- Capture widgets as a semantic tree (kind, name, text, state, rect, children).
- Tests can walk the tree, assert on logical state, and interact with the UI.
- Export that tree as stable text snapshots.
- Compare snapshots with golden files in tests.
- Provide query and interaction helpers for tests and AI tools.
- Emit diffs only when the semantic output changes.

This gives us browser style inspectability for native GUI, without browser overhead.

## Why this helps live coding and AI verification

- The output is nodes and text, so AI can read, reason, and compare quickly.
- No image scraping or OCR is required.
- Tests can assert logical intent: "button exists", "display text changed", "checkbox is checked".
- Iteration is faster because semantic capture avoids real rendering cost.
- Diffs are smaller than full frame dumps and are easier to review.

## Current implementation in Silky

The current implementation already provides the core semantic capture pipeline.

### Compile flag

- Tests are compiled with `-d:silkyTesting`. This gives you mock window, rendering, and frame pumping.
- Test files assert this flag to avoid accidental non testing runs.
- Tests can also use diffs for golden file testing.

### Semantic model

`src/silky/semantic.nim` defines:

- `SemanticNode` for widget kind, name, text, rect, state, parent and children.
- `SemanticCapture` for per frame tree capture, stack management, frame number, and previous snapshot support.
- Snapshot export with `toText()` and `toSnapshot()`.
- Search helpers:
- `findByPath()`
- `findByText()`
- `findByName()`
- `findAllByText()`
- Simple text diff with `diff(old, new)`.

### Frame integration

In semantic mode:

- `beginUi()` resets semantic capture each frame.
- `beginWidget()` pushes a semantic node.
- `setWidgetState()` records interactive state.
- `setWidgetRect()` updates geometry.
- `endWidget()` pops the node.
- `semanticSnapshot()` returns snapshot text for tests.

### Example app and tests

The calculator example shows end to end usage:

- `examples/calculator/calculator.nim` annotates widgets with semantic metadata, including display name and button text.
- `examples/calculator/tests/test.nim` drives the UI using semantic helpers like `clickButton`, queries nodes by text/name, and asserts display output.

## Snapshot format

The snapshot is a readable tree, for example:

```text
frame: 3
Calculator:
type: SubWindow
children:
display:
type: Display
text: 7+3
1:
type: Button
text: =
```

This is a simple tree text format, but controlled by Silky so it stays stable for testing.

## Test workflow

Recommended flow for semantic UI tests:

### Unit test

1. Build and run app/test with `-d:silkyTesting`.
2. Pump a frame and capture semantic snapshot.
3. Query nodes and assert logical state.
4. Trigger interaction, click or type text.
5. Assert on new state.

### Golden file test

1. Build and run app/test with `-d:silkyTesting`.
2. Pump a frame and capture semantic snapshot.
3. Keep doing some complex actions and diffs snapshots.
4. Compare against golden output.

This supports both direct assertions and golden master testing.

## Diff strategy

Silky currently has line by line snapshot diffing.

- If no semantic change occurs, diff is empty.
- If semantic output changes, tests can print only changed lines.
- This avoids frame by frame spam in immediate mode.

Future improvement: keep and publish diffs automatically only when changed from previous frame.

## Interaction strategy

Current tests already click controls by button text.

Next steps can add:

- Click by semantic path.
- Click by index under a container.
- Text lookup with disambiguation when duplicate labels exist.
- Script style actions (`click`, `type`, `assertText`, `expectVisible`).

## Performance expectations

Semantic mode should be much faster than full GUI automation because:

- No real drawing pipeline is needed.
- No GPU rasterization is required.
- Data is captured in memory as plain structures and text.

This makes it suitable for CI, local rapid iteration, and AI driven verification loops.

## Summary

Semantic testing gives Silky a practical path for reliable UI verification without pixel testing. It fits immediate mode UI, works well with AI tools, and is already partially implemented with semantic capture, querying, diffing, and real example coverage in calculator tests.
Binary file added docs/size.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/stack.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions examples/basicwindow/basicwindow.nim
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ window.onFrame = proc() =
# Draw tiled test texture as the background.
for x in 0 ..< 16:
for y in 0 ..< 10:
sk.at = vec2(x.float32 * 256, y.float32 * 256)
sk.layout.at = vec2(x.float32 * 256, y.float32 * 256)
image("testTexture", rgbx(30, 30, 30, 255))

subWindow("A SubWindow", showWindow, vec2(100, 100), vec2(400, 700)):
Expand Down Expand Up @@ -95,11 +95,11 @@ window.onFrame = proc() =
if not showWindow:
if window.buttonPressed[MouseLeft]:
showWindow = true
sk.at = vec2(100, 100)
sk.layout.at = vec2(100, 100)
text("Click anywhere to show the window")

let ms = sk.avgFrameTime * 1000
sk.at = sk.pos + vec2(sk.size.x - 250, 20)
sk.layout.at = sk.pos + vec2(sk.size.x - 250, 20)
text(&"frame time: {ms:>7.3f}ms")

sk.endUi()
Expand Down
44 changes: 22 additions & 22 deletions examples/calculator/calculator.nim
Original file line number Diff line number Diff line change
Expand Up @@ -118,16 +118,16 @@ template calcLabel(displayText: string) =
## Displays a right-aligned label in a dark background box.
let
labelSize = vec2(sk.size.x - 24, 60)
labelRect = rect(sk.at, labelSize)
labelRect = rect(sk.layout.at, labelSize)

sk.beginWidget("Display", name = "display", text = displayText, rect = labelRect)
sk.drawRect(sk.at, labelSize, rgbx(50, 50, 50, 255))
sk.drawRect(sk.layout.at, labelSize, rgbx(50, 50, 50, 255))

let oldStyle = sk.textStyle
sk.textStyle = "H1"
let labelTextSize = sk.getTextSize(sk.textStyle, displayText)
let textX = sk.at.x + labelSize.x - labelTextSize.x - 10
discard sk.drawText(sk.textStyle, displayText, vec2(textX, sk.at.y + 14), rgbx(255, 255, 255, 255))
let textX = sk.layout.at.x + labelSize.x - labelTextSize.x - 10
discard sk.drawText(sk.textStyle, displayText, vec2(textX, sk.layout.at.y + 14), rgbx(255, 255, 255, 255))
sk.textStyle = oldStyle
sk.endWidget()

Expand All @@ -136,7 +136,7 @@ template calcLabel(displayText: string) =
template calcButton(label: string, body: untyped) =
let
btnSize = vec2(60, 50)
startPos = sk.at
startPos = sk.layout.at
btnRect = rect(startPos, btnSize)

sk.beginWidget("Button", text = label, rect = btnRect)
Expand All @@ -160,9 +160,9 @@ template calcButton(label: string, body: untyped) =

sk.endWidget()

sk.at.x += btnSize.x + 10
sk.stretchAt.x = max(sk.stretchAt.x, sk.at.x + 10)
sk.stretchAt.y = max(sk.stretchAt.y, sk.at.y + 50 + 10)
sk.layout.at.x += btnSize.x + 10
sk.layout.stretchMax.x = max(sk.layout.stretchMax.x, sk.layout.at.x + 10)
sk.layout.stretchMax.y = max(sk.layout.stretchMax.y, sk.layout.at.y + 50 + 10)

window.onFrame = proc() =

Expand All @@ -171,7 +171,7 @@ window.onFrame = proc() =
# Draw tiled test texture as the background.
for x in 0 ..< 16:
for y in 0 ..< 10:
sk.at = vec2(x.float32 * 256, y.float32 * 256)
sk.layout.at = vec2(x.float32 * 256, y.float32 * 256)
image("testTexture", rgbx(30, 30, 30, 255))

subWindow("Calculator", showWindow, vec2(10, 10), vec2(340, 480)):
Expand All @@ -187,7 +187,7 @@ window.onFrame = proc() =
# Draw the calculator display.
calcLabel(displayText)

let rowX = sk.at.x
let rowX = sk.layout.at.x

# Row 1: C, +/- (±), %, ÷.
calcButton("C"):
Expand All @@ -208,8 +208,8 @@ window.onFrame = proc() =
calcButton("÷"):
if inOperator(): symbols[^1].operator = "÷"

sk.at.x = rowX
sk.at.y += 60
sk.layout.at.x = rowX
sk.layout.at.y += 60

# Row 2: 7, 8, 9, ×.
calcButton("7"):
Expand All @@ -224,8 +224,8 @@ window.onFrame = proc() =
calcButton("×"):
if inOperator(): symbols[^1].operator = "×"

sk.at.x = rowX
sk.at.y += 60
sk.layout.at.x = rowX
sk.layout.at.y += 60

# Row 3: 4, 5, 6, -.
calcButton("4"):
Expand All @@ -246,8 +246,8 @@ window.onFrame = proc() =
if symbols.len > 0 and symbols[^1].number == "":
symbols[^1].number = "-"

sk.at.x = rowX
sk.at.y += 60
sk.layout.at.x = rowX
sk.layout.at.y += 60

# Row 4: 1, 2, 3, +.
calcButton("1"):
Expand All @@ -262,8 +262,8 @@ window.onFrame = proc() =
calcButton("+"):
if inOperator(): symbols[^1].operator = "+"

sk.at.x = rowX
sk.at.y += 60
sk.layout.at.x = rowX
sk.layout.at.y += 60

calcButton("0"):
inNumber()
Expand All @@ -277,16 +277,16 @@ window.onFrame = proc() =
calcButton("="):
compute()

sk.at.x = rowX
sk.at.y += 60
sk.layout.at.x = rowX
sk.layout.at.y += 60

if not showWindow:
if window.buttonPressed[MouseLeft]:
showWindow = true
sk.at = vec2(100, 100)
sk.layout.at = vec2(100, 100)

let ms = sk.avgFrameTime * 1000
sk.at = sk.pos + vec2(sk.size.x - 250, 20)
sk.layout.at = sk.pos + vec2(sk.size.x - 250, 20)
text(&"frame time: {ms:>7.3f}ms")

sk.endUi()
Expand Down
Loading
Loading