No description
Find a file
2025-05-28 15:46:01 -06:00
.github/workflows Move to selectable UiScalar type (#6) 2025-02-08 11:35:30 +02:00
src Improvements 0.13.6 various (#24) 2025-04-28 16:09:34 +03:00
tests Improvements 0.13.6 various (#24) 2025-04-28 16:09:34 +03:00
.gitignore update atlas lock 2025-03-22 16:31:25 -06:00
atlas.lock update atlas lock 2025-03-22 16:31:25 -06:00
config.nims Improvements 0.13.0 css variables (#23) 2025-04-05 07:33:30 +03:00
CSS Grid Layout Module Level 2 Specification.md Improvements 0.11.0 refactor to cssgrid spec using claude (#20) 2025-04-02 00:18:53 +03:00
cssgrid.nimble remove pixie dep 2025-05-28 15:46:01 -06:00
LICENSE Create LICENSE 2024-12-20 04:08:01 -07:00
README.md update readme 2025-02-15 08:49:16 -07:00

CSS Grid

Implementation of CSS Grid for usage in GUI's and TUI's. It currently covers the basics of the CSS grid and includes support for fractions, percents, auto-flow, and auto-insert. The intent is to be fairly (but not 100%) compatible implementation of CSS Grid.

The code is written with minimal expectations for input and outputs so it could be used with various UI frameworks. Currently it assums float32's as the scalar basis for ease of implementation but could be expanded to support integer scalars as well.

Contributions, issues, and PR's are welcome.

API Example

The API can be used directly to setup a CSS Grid. A mini-DSL macro is provided which mimics the CSS syntax. Though it's intented to be used to implement user facing API that matches the UI framework.

Here's an example of using the macro to parse CSS style grid syntax:

  test "compute others":
    var gt: GridTemplate

    parseGridTemplateColumns gt, ["first"] 40'ux \
      ["second", "line2"] 50'ux \
      ["line3"] auto \
      ["col4-start"] 50'ux \
      ["five"] 40'ux ["end"]
    parseGridTemplateRows gt, ["row1-start"] 25'pp \
      ["row1-end"] 100'ux \
      ["third-line"] auto ["last-line"]

    gt.gaps[dcol] = 10.UiScalar
    gt.gaps[drow] = 10.UiScalar

Layout Example

See tplots.nim for a complete example using Pixie to layout a series of rectangles:

Using auto-flow: row:

Layout Row

Using auto-flow: column:

Layout Row

Using justify and alignments:

Align and Justify Row

Basic Usage

type
  TestNode* = ref object
    ## a container that fullfills the GridNode concept
    name: string
    box: UiBox
    bmin, bmax: UiSize
    gridItem: GridItem
    cxSize: array[GridDir, Constraint]  # For width/height
    cxOffset: array[GridDir, Constraint] # For x/y positions
    cxMin: array[GridDir, Constraint]  # For width/height
    cxMax: array[GridDir, Constraint] # For x/y positions
    gridTemplate: GridTemplate
    children: seq[TestNode]
    parent*: TestNode

template getParentBoxOrWindows*(node: TestNode): UiBox =
  ## this needs to be implemented for the GridNode type
  if node.parent.isNil:
    node.frame.windowSize
  else:
    node.parent.box

proc newTestNode(name: string, x, y, w, h: float32): TestNode =
  result = TestNode(
    name: name, box: uiBox(x, y, w, h), children: @[],
    frame: Frame(windowSize: uiBox(0, 0, 800, 600))
  )

proc addChild(parent, child: TestNode) =
  parent.children.add(child)
  child.parent = parent

test "Grid with basic constrained children":
  let parent = newTestNode("parent", 0, 0, 400, 300)
  let child1 = newTestNode("child1", 10, 10, 100, 100)
  let child2 = newTestNode("child2", 10, 120, 100, 100)
  
  parent.addChild(child1)
  parent.addChild(child2)
  
  # Set fixed-parent constraint
  parent.cxSize[dcol] = csFixed(400)  # set fixed parent
  parent.cxSize[drow] = csFixed(300)  # set fixed parent

  # Set percentage-based constraints for children
  child1.cxSize[dcol] = csPerc(50)  # 50% of parent
  child1.cxSize[drow] = csPerc(30)  # 30% of parent
  
  child2.cxSize[dcol] = csPerc(70)  # 70% of parent
  child2.cxSize[drow] = csPerc(40)  # 40% of parent
  
  computeLayout(parent)
  
  check child1.box.w == 200  # 50% of 400
  check child1.box.h == 90   # 30% of 300
  check child2.box.w == 280  # 70% of 400
  check child2.box.h == 120  # 40% of 300

Here's another example using Pixie to generate an image of the grid layout. This layout also sets HTML / CSS Grid style alignment and justification.

From tplots.nim:

  test "grid alignment and justification":
    # grid-template-columns: [first] 40px [line2] 50px [line3] auto [col4-start] 50px [five] 40px [end];
    # parseGridTemplateColumns gridTemplate, 60'ux 60'ux 60'ux 60'ux 60'ux
    let cnt = 8
    var gridTemplate = newGridTemplate()
    gridTemplate.autoFlow = grRow

    parseGridTemplateColumns gridTemplate, 1'fr 1'fr 1'fr 1'fr 1'fr 
    parseGridTemplateRows gridTemplate, 50'ux 50'ux
    gridTemplate.justifyItems = CxStretch

    var nodes = newSeq[GridNode](cnt)

    var parent = GridNode(gridTemplate: gridTemplate)
    assert parent is GridNode
    parent.cxSize = [300'ux, 100'ux]
    parent.frame = Frame(windowSize: uiBox(0, 0, 400, 100))

    # item a
    var itema = newGridItem()
    itema.column = 1 // 2
    itema.row = 1 // 3
    nodes[0] = GridNode(name: "a", gridItem: itema, frame: parent.frame)

    # ==== item e ====
    var iteme = newGridItem()
    iteme.column = 5 // 6
    iteme.row = 1 // 3
    nodes[1] = GridNode(name: "e", gridItem: iteme, frame: parent.frame)

    # ==== item b's ====
    for i in 2 ..< nodes.len():
      let gi = newGridItem()
      nodes[i] = GridNode(name: "b" & $(i-2), gridItem: gi, frame: parent.frame)
      nodes[i].cxSize = [33'ux, 33'ux]
      nodes[i].parent = parent
      nodes[i].gridItem.justify = some(CxCenter)
      nodes[i].gridItem.align = some(CxCenter)
      if i == 5:
        nodes[i].gridItem.justify = some(CxStart)
      if i == 6:
        nodes[i].gridItem.align = some(CxStart)
      if i == 7:
        nodes[i].gridItem.align = some(CxEnd)

    # ==== process grid ====
    parent.children = nodes
    parent.computeLayout()

    printGrid(gridTemplate, cmTerminal)
    printLayout(parent, cmTerminal)
    saveImage(gridTemplate, parent.box, nodes, "grid-align-and-justify")

Basic Layouts

CSS Grid now handles basic HTML style layouts. These are also integrated with the CSS Grid so you can specify things like content-min and have the grid layout understand it!

From tbasiclayout.nim:

suite "Basic CSS Layout Tests":
  test "Fixed size constraints":
    let node = newTestNode("test", 0, 0, 100, 100)
    node.cxSize[dcol] = 200'ux
    node.cxSize[drow] = 150'ux
    
    computeLayout(node)
    check node.box.w == 200
    check node.box.h == 150

  test "Percentage constraints":
    let parent = newTestNode("parent", 0, 0, 400, 300)
    let child = newTestNode("child", 0, 0, 100, 100)
    child.parent = parent
    parent.children.add(child)
    
    child.cxSize[dcol] = 50'pp # 50% of parent width
    child.cxSize[drow] = 25'pp # 25% of parent height
    
    computeLayout(parent)
    check child.box.w == 200 # 50% of 400
    check child.box.h == 75  # 25% of 300

  test "Auto constraints":
    let parent = newTestNode("parent", 0, 0, 400, 300)
    let child = newTestNode("child", 10, 10, 100, 100)
    child.parent = parent
    parent.children.add(child)
    
    child.cxSize[dcol] = cx"auto"
    child.cxSize[drow] = csAuto()
    
    computeLayout(parent)
    # Auto should fill available space (parent size - offset)
    check child.box.w == 390 # 400 - 10
    check child.box.h == 290 # 300 - 10

  test "Min/Max constraints":
    let node = newTestNode("test", 0, 0, 100, 100)
    # Test min constraint
    node.cxSize[dcol] = min(150'ux, 200'ux)
    computeLayout(node)
    check node.box.w == 150
    # Test max constraint
    node.cxSize[drow] = max(150'ux, 200'ux)
    computeLayout(node)
    check node.box.h == 200

  test "Complex nested constraints":
    let parent = newTestNode("parent", 0, 0, 400, 300)
    let child1 = newTestNode("child1", 10, 10, 100, 100)
    let child2 = newTestNode("child2", 10, 120, 100, 100)
    parent.children = @[child1, child2]
    child1.parent = parent
    child2.parent = parent
    
    # Child1: 50% of parent width, min 100px
    child1.cxSize[dcol] = max(50'pp, 100'ux) # same as csMax(csPerc(50), csFixed(100))
    
    # Child2: 25% of parent width + 50px
    child2.cxSize[dcol] = 25'pp + 50'ux # same as csAdd(csPerc(25), csFixed(50))
    
    computeLayout(parent)
    check child1.box.w == 200 # max(50% of 400, 100)
    check child2.box.w == 150 # (25% of 400) + 50