Canonical Grid Positioning (CGP) and Nesting Rules
Why CGP exists
Most UI systems teach placement in pixels first:
- put this box at 120px from the left
- make this width 300px
That works only until the screen changes.
Canonical Grid Positioning (CGP) takes a different approach. It says:
First define the grid of the available space.
Then place the element inside that grid.
This gives a placement method that a human can think in, and a machine can execute on any rectangular device boundary.
The core idea
CGP does not position things in pixels.
It positions things by:
- defining a bounded viewport as a grid of rows and columns
- choosing a starting cell inside that grid
- choosing how many rows and columns the element spans
Pixels are computed later by the renderer.
CGP is not just four numbers
A shorthand description of CGP often uses four numbers:
- start row
- start column
- row span
- column span
That shorthand is incomplete by itself.
Those four numbers only make sense inside a grid context.
So a complete CGP definition is:
- viewport rows
- viewport columns
- start row
- start column
- row span
- column span
In JSON form:
{
"viewport": {
"rows": 8,
"columns": 12
},
"position": {
"startCell": [2, 4],
"span": [3, 5]
}
}
Why viewport rows and columns matter
Without the viewport grid, the same placement numbers mean very different things.
Example:
{
"position": {
"startCell": [1, 1],
"span": [1, 1]
}
}
This is meaningless by itself.
But inside:
2 × 2→ that cell is very large24 × 24→ that cell is very small
So the viewport grid defines the resolution of placement.
That gives the designer a powerful control:
- low-density grid → coarse layout
- high-density grid → fine layout
This is how two small fields can be kept apart, or how large Pulse regions can be planned cleanly, without using pixels directly.
Canonical validity rules
A CGP definition is valid only if:
viewport.rows >= 1viewport.columns >= 1startRow >= 1startCol >= 1rowSpan >= 1colSpan >= 1startRow + rowSpan - 1 <= viewport.rowsstartCol + colSpan - 1 <= viewport.columns
This guarantees the element stays inside its viewport.
Runtime pixel projection
At runtime, let the actual rectangular rendering surface be:
- width =
VW - height =
VH
If the CGP says:
viewport.rows = Rviewport.columns = CstartCell = [SR, SC]span = [RS, CS]
then the renderer computes:
cellWidth = VW / CcellHeight = VH / R
and:
x = (SC - 1) * cellWidthy = (SR - 1) * cellHeightwidth = CS * cellWidthheight = RS * cellHeight
This is the standard GridLookout projection rule.
Human–machine symmetry
CGP supports a simple symmetry:
- a human thinks in bounded relative regions
- a machine calculates the exact pixels for the device
The human thinks:
divide this space into rows and columns,
and place the item here
The machine does:
compute the pixel rectangle for the current device
This is the main reason CGP is useful as a platform-neutral positioning convention.
Pulse-level CGP
In GridLookout, a Pulse is the primary visual unit.
A Pulse may carry a CGP definition that places it in the outer runtime viewport.
Example:
{
"pulsePhrase": "bank_account_details",
"visual": {
"viewport": { "rows": 8, "columns": 12 },
"layer": 1,
"position": {
"startCell": [1, 8],
"span": [3, 5]
}
}
}
This places the Pulse in the outer GridLookout viewport.
Response Array Convention and fields
A Pulse Response may be simple or structured. In the structured case, the Response uses META rows to declare field names, followed by data rows.
Example:
[
["META", "bank_name", "account_number", "account_type"],
["ABC Bank", "12345678", "Savings"]
]
This means the Pulse carries named fields already. That makes it natural to render those fields inside the Pulse.
The Pulse still remains one concern. The fields are parts of that concern.
Field-level CGP
A Pulse may optionally define CGP for fields inside the Pulse.
This means:
- Pulse CGP places the Pulse in the outer viewport
- Field CGP places each field inside the Pulse boundary
Example:
{
"fieldViewport": { "rows": 6, "columns": 6 },
"fieldRender": {
"bank_name": {
"type": "show",
"position": { "startCell": [1, 1], "span": [1, 6] }
},
"account_number": {
"type": "input",
"position": { "startCell": [2, 1], "span": [2, 6] }
},
"account_type": {
"type": "act",
"actKind": "button",
"position": { "startCell": [4, 1], "span": [1, 6] }
}
}
}
Nesting rule
The nesting rule is simple.
Step 1
Resolve the Pulse CGP against the outer runtime viewport.
Step 2
Obtain the Pulse rectangle in pixels.
Step 3
Treat that Pulse rectangle as the runtime viewport for the fields.
Step 4
Resolve each field CGP inside that Pulse-local viewport.
So:
- Pulse CGP is global to the current GridLookout surface
- Field CGP is local to the Pulse
Formal nested projection
Pulse level
Given outer runtime viewport VW × VH:
pulseCellWidth = VW / viewport.columnspulseCellHeight = VH / viewport.rowspulseX = (startCol - 1) * pulseCellWidthpulseY = (startRow - 1) * pulseCellHeightpulseWidth = colSpan * pulseCellWidthpulseHeight = rowSpan * pulseCellHeight
Field level
Given Pulse rectangle pulseWidth × pulseHeight:
fieldCellWidth = pulseWidth / fieldViewport.columnsfieldCellHeight = pulseHeight / fieldViewport.rowsfieldX = pulseX + (fieldStartCol - 1) * fieldCellWidthfieldY = pulseY + (fieldStartRow - 1) * fieldCellHeightfieldWidth = fieldColSpan * fieldCellWidthfieldHeight = fieldRowSpan * fieldCellHeight
Field types
A field may also declare a behavior type.
Recommended core types:
show→ display onlyinput→ user types into itact→ emits an Intention
For act, the rendering style may further specify:
- button
- option
- radio
- toggle
- select
This gives a uniform interaction grammar for Intention Space.
Interaction discipline
A recommended perceptive interaction rule is:
- tap Pulse → Pulse takes the whole available viewport
- tap Field → Field expands within the Pulse boundary, or within a focused Pulse view
This preserves the idea that:
- Pulse is the main unit of attention
- Field is a sub-focus inside the Pulse
Pickup area as prototyping aid
CGP becomes much easier to understand when paired with a Pickup area.
The Pickup area is a prototyping surface where the designer:
- selects a Pulse region from a visible grid
- adds that Pulse to a bounded preview area
- sees how the Pulse is rendered
- sees how fields sit inside the Pulse
- tests tap-to-expand behavior
Pickup is useful because it makes CGP visible before a full application is built.
Important Pickup principle
Pickup is only a prototyping and perceptual aid.
The canonical source of truth remains the CGP definition itself.
Why Pickup helps beginners
Without Pickup, CGP looks abstract.
With Pickup, the learner can see:
- a grid
- a chosen region
- a Pulse in that region
- fields arranged inside it
- interactive types like
inputandact
So Pickup acts as a bridge between abstract placement and rendered UI.
Relationship to GridLookout architecture
GridLookout should treat CGP as its core spatial contract.
At minimum, a GridLookout renderer should:
- accept Pulse-level CGP
- compute pulse pixel rectangles from runtime viewport
- optionally accept field-level CGP
- resolve field-level CGP inside Pulse boundaries
- support layer ordering
- support perceptive interaction such as tap-to-expand
This keeps GridLookout platform-neutral while still being precise.
Constraints for correct use
To preserve Pulse integrity:
- field CGP must remain local to the Pulse
- fields enrich a Pulse; they do not replace Pulse granularity
- if separate concerns emerge, use multiple Pulses grouped in a Signal rather than one overloaded Pulse
This keeps the rendering model aligned with the Response Array Convention.
Minimal teaching sequence
For beginners, CGP can be taught in three steps:
1. Define the viewport grid
How many rows and columns divide the space?
2. Define the placement
Where does the element start, and how many cells does it span?
3. Repeat inside the Pulse
Use the same method again for fields inside the Pulse.
That is the whole idea of CGP and nested CGP.
Summary
Canonical Grid Positioning (CGP) is a bounded, grid-relative positioning convention in which an element is defined by:
- viewport row count
- viewport column count
- starting cell
- cell span
Nested CGP applies the same rule recursively inside enclosing elements, especially for Pulse Fields inside a Pulse.
This gives GridLookout:
- a pixel-independent placement grammar
- a human-teachable spatial language
- a machine-executable rendering rule
- a consistent path from Pulse data to perceptive UI
Next practical steps
A useful next round of testing is:
- multiple Pulses in one GridLookout surface
- multiple field types inside a Pulse
actfields rendered as button, radio, option, select- field expansion inside focused Pulse view
- layer interactions across multiple Pulses