Skip to content

Conversation

@aymanbagabas
Copy link
Member

@aymanbagabas aymanbagabas commented Nov 20, 2025

This change redefines what a Canvas and Layer are within the lipgloss
package. The Canvas is now a simple screen buffer that can compose
layers and drawable types.

A layer now doesn't do any computation and only represents a tree node. Using a compositor flattens a list of layers, and is able to draw them or render them into a string.

Copy link
Member

@andreynering andreynering left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image

@lrstanley
Copy link

I have some concerns with the layer functionality, not necessarily specific to this PR (as it was present with the previous version in v2-exp as well). There are a couple of things on layers that I feel like are too easy to accidentally misconfigure, primarily around recursive child layers.

  • Setting dimensional information at function execution time vs at render time. e.g., layer.X(val) directly mutates child layers at function call time, but this means that if that child layer is mutated after the fact, the changes in the X() call are lost. It may be worth storing that info only on the layer that is being changed, then either doing the actual recursive child corrections to coordinates in Render, or a separate step of some kind. This also looks like it may apply to AddLayers() as well, where, depending on when it's executed, it may produce the wrong offset information.
    • Would it be worth, rather than mutating the child, to store a reference to the parent in the child when it's added to a parent, so if the parent exists, it can recurse up the tree to get the information it needs?
  • What is the impact of calling something like X(val), but it only affects direct children, vs children of children, and so on?
  • functions like GetLayer(id) only check 1 level deep. Should it be recursive for all children?
  • What is the expected outcome when a child 2 levels deep has a higher z-index than something else? It's currently rendered in order of z-index at the top level, but it seems like undefined (or unclear) behavior when children have different Z levels (in addition to the fact that someone can add a layer, then change the Z value, and there is no warning/recommendation against this currently).

@aymanbagabas
Copy link
Member Author

I have some concerns with the layer functionality, not necessarily specific to this PR (as it was present with the previous version in v2-exp as well). There are a couple of things on layers that I feel like are too easy to accidentally misconfigure, primarily around recursive child layers.

  • Setting dimensional information at function execution time vs at render time. e.g., layer.X(val) directly mutates child layers at function call time, but this means that if that child layer is mutated after the fact, the changes in the X() call are lost. It may be worth storing that info only on the layer that is being changed, then either doing the actual recursive child corrections to coordinates in Render, or a separate step of some kind. This also looks like it may apply to AddLayers() as well, where, depending on when it's executed, it may produce the wrong offset information.

    • Would it be worth, rather than mutating the child, to store a reference to the parent in the child when it's added to a parent, so if the parent exists, it can recurse up the tree to get the information it needs?
  • What is the impact of calling something like X(val), but it only affects direct children, vs children of children, and so on?

  • functions like GetLayer(id) only check 1 level deep. Should it be recursive for all children?

  • What is the expected outcome when a child 2 levels deep has a higher z-index than something else? It's currently rendered in order of z-index at the top level, but it seems like undefined (or unclear) behavior when children have different Z levels (in addition to the fact that someone can add a layer, then change the Z value, and there is no warning/recommendation against this currently).

Good points you raise here. I've made some changes, with the help of Crush, to work around these concerns. Let me know if you see any other issues 🙂

@lrstanley
Copy link

lrstanley commented Nov 22, 2025

Good points you raise here. I've made some changes, with the help of Crush, to work around these concerns. Let me know if you see any other issues 🙂

I mentioned this in Discord as well, but an alternative, and I think a bit cleaner approach could be used. See my version of addressing those concerns here.

Some addl. notes on my proposed tweaks:

  • I also store the width/height pre-computed, otherwise it will get computed more than 2+ times, depending on what the user is doing, and those functions are very compute heavy
  • return a pointer on Compose so you can chain it with .Render()
  • add MaxZ() which is needed to be able to wrap other layers, otherwise you can't easily enforce a situation where you want something to always be on top of something else, given you can't access the children from the parent.
  • add String(), which prints out a debug-friendly version of all of the layers, indented based on depth

I'm still working on a layout wrapper PoC to create more complex layouts, so I will keep poking things to see where any gaps might be/may make some follow up PRs.

@aymanbagabas aymanbagabas changed the title v2: refactor: new Canvas and Layer API v2: refactor: new Canvas, Compositor, and Layer API Nov 24, 2025
This change redefines what a Canvas and Layer are within the lipgloss
package. The Canvas is now a simple screen buffer that can compose
layers and drawable types.

The layer implementation is now responsible for holding metadata about
its position and z-index, as well as managing child layers. This
separation of concerns simplifies the Canvas and makes it more flexible
for composing various drawable types.

The id is now mandatory when creating a Layer, ensuring each layer can
be uniquely identified.
Layer x, y, z coordinates are now relative to parent instead of absolute.
Positions are calculated dynamically during draw/hit/bounds operations
without mutating children. This provides cleaner hierarchy semantics and
prevents unintended state changes when repositioning layers.

- Add absolutePosition() to calculate absolute coords on-demand
- Add drawWithOffset() for recursive drawing with parent context
- Add hitWithOffset() for recursive hit testing with proper z-sorting
- Add boundsWithOffset() for recursive bounds calculation
- Simplify X(), Y(), Z() setters to only update own coordinates
- Simplify AddLayers() to not mutate children
- Make GetLayer() fully recursive to search entire tree

💘 Generated with Crush

Assisted-by: Claude Sonnet 4.5 via Crush <[email protected]>
All layers are now drawn in global z-index order regardless of hierarchy
depth. Previously, parents always drew before children, making z-ordering
only meaningful among siblings. Now a deeply nested child with z=100 will
correctly draw on top of a top-level layer with z=50.

- Add flatLayer type to hold calculated absolute positions
- Add flattenLayers() to recursively collect all layers with absolute coords
- Refactor Draw() to flatten hierarchy, sort by absolute z, then draw
- Remove drawWithOffset() in favor of flatten-sort-draw approach

💘 Generated with Crush

Assisted-by: Claude Sonnet 4.5 via Crush <[email protected]>
Copy link

@lrstanley lrstanley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some minor tweaks that may be worth looking at:

  1. remove absolute Z functionality, as I don't think this will work how people expect it would.
    • This was causing the following to be rendered behind other lower z layers: https://cdn.liam.sh/share/2025/11/layers.txt (last layer in this go-spew output should be on top)
    • see also: https://cdn.liam.sh/share/2025/11/explorer_GQ10NjdU0j.mp4
    • maybe there is still a usecase for absolute z that I can't think of, so maybe it should be configurable on the compositor?
      • E.g. (c *Compositor) UseAbsoluteZ(bool). Alternatively, on *Layer, we could also do (l *Layer) CompoundingZ(int), which sets both a z field, in addition to an compoundingZ field, and if that is present, it's always added onto children?
  2. Allow lipgloss.Canvas to be reused, for users who would like higher perf.

Patch for the above: https://cdn.liam.sh/share/2025/11/misc-v2-canvas-1.patch

$ curl -sL https://cdn.liam.sh/share/2025/11/misc-v2-canvas-1.patch | git apply

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants