Describe in documentation how to implement scrollable areas properly #1804

Open
opened 2025-03-21 00:05:16 -07:00 by cheery · 6 comments
cheery commented 2025-03-21 00:05:16 -07:00 (Migrated from github.com)

Hi,

I presume all the documentation for this project is here: https://www.yogalayout.dev/docs/about-yoga

It does not tell how to make a scrollable area. Could you take a little bit of time to explain how it's done?

I'm using poga bindings to apply yoga in my project. It's providing C bindings directly, eg. YGNodeStyleSetOverflow would allow me to set the scroll on.

So once I've set the overflow: scroll, how do I actually make it scroll? Eg. Set the scroll offsets and get the scroll limits so I won't let the user scroll over the content.

Thank you for writing yoga. It's been a nice library to use overall.

(I'm also interested about making very large scrollable areas that need to dynamically remove/add content based on where user is scrolling. But the basic recipe would be nice and I could then apply it on the infinite scrolling -problem)

Hi, I presume all the documentation for this project is here: https://www.yogalayout.dev/docs/about-yoga It does not tell how to make a scrollable area. Could you take a little bit of time to explain how it's done? I'm using [poga bindings](https://github.com/dzhsurf/poga/) to apply yoga in my project. It's providing C bindings directly, eg. `YGNodeStyleSetOverflow` would allow me to set the scroll on. So once I've set the overflow: scroll, how do I actually make it scroll? Eg. Set the scroll offsets and get the scroll limits so I won't let the user scroll over the content. Thank you for writing yoga. It's been a nice library to use overall. (I'm also interested about making very large scrollable areas that need to dynamically remove/add content based on where user is scrolling. But the basic recipe would be nice and I could then apply it on the infinite scrolling -problem)
cheery commented 2025-03-21 09:36:37 -07:00 (Migrated from github.com)

I realised it was simpler than I expected. I was not examining the whole palette of options I actually have.

The following recipe assumes you don't have paddings in the scroll area element:

  • Measure the content area from children nodes by merging their rectangles.
  • Get the scroll area width.
  • scroll_x = clamp(scroll_x, -(content_width - scroll_area_width), 0)
  • When drawing/interacting with the content, apply your scroll offset.

When I'm doing it properly, I have less need for infinite scrolling element.

I realised it was simpler than I expected. I was not examining the whole palette of options I actually have. The following recipe assumes you don't have paddings in the scroll area element: * Measure the content area from children nodes by merging their rectangles. * Get the scroll area width. * scroll_x = clamp(scroll_x, -(content_width - scroll_area_width), 0) * When drawing/interacting with the content, apply your scroll offset. When I'm doing it properly, I have less need for infinite scrolling element.
natalia-hultrix commented 2025-04-27 06:15:57 -07:00 (Migrated from github.com)

What do you mean scroll-able areas? If your using OpenGL, Vulkan, Direct3D use viewports and if your using Skia, Direct2D or Cairo then use clipping. A scroll-able area is just a larger area clipped by a smaller area and you match the width of your content to the clipping or viewport container. Then the top of your scrolling content is 0.0 and the bottom of your content is 1.0. The blue container is yoga root and pink is yoga root children, the black is your clipped area or viewport.

Scrollable Area Example

What do you mean scroll-able areas? If your using OpenGL, Vulkan, Direct3D use viewports and if your using Skia, Direct2D or Cairo then use clipping. A scroll-able area is just a larger area clipped by a smaller area and you match the width of your content to the clipping or viewport container. Then the top of your scrolling content is 0.0 and the bottom of your content is 1.0. The blue container is yoga root and pink is yoga root children, the black is your clipped area or viewport. ![Scrollable Area Example](https://www.kirupa.com/web/images/scrollable_area_what_we_see_200.png)
cheery commented 2025-04-27 06:22:48 -07:00 (Migrated from github.com)

Hi Steven, thanks for late reply. With scrollable area I mean for exactly
what you appear to mean and propose to build.

I mostly solved my problems except to one thing: it appears difficult to
make a scrolling area that scrolls both vertically and horizontally.
Particularly measuring areas in such case. I should build a demonstration
for this. Maybe I'll do so when I have motivation for solving it again.

su 27. huhtik. 2025 klo 16.16 steven @.***> kirjoitti:

steven-hultrix left a comment (facebook/yoga#1804)
https://github.com/facebook/yoga/issues/1804#issuecomment-2833454819

What do you mean scroll-able areas? If your using OpenGL, Vulkan, Direct3D
use viewports and if your using Skia, Direct2D or Cairo then use clipping.
A scroll-able area is just a larger area clipped by a smaller area and you
match the width of your content to the container. Then the top of your
scrolling content is 0.0 and the bottom of your content is 1.0.


Reply to this email directly, view it on GitHub
https://github.com/facebook/yoga/issues/1804#issuecomment-2833454819,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/AAJIXPHH63M2KMTBUJ2AVDD23TKCHAVCNFSM6AAAAABZPIGGOWVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDQMZTGQ2TIOBRHE
.
You are receiving this because you authored the thread.Message ID:
@.***>

Hi Steven, thanks for late reply. With scrollable area I mean for exactly what you appear to mean and propose to build. I mostly solved my problems except to one thing: it appears difficult to make a scrolling area that scrolls both vertically and horizontally. Particularly measuring areas in such case. I should build a demonstration for this. Maybe I'll do so when I have motivation for solving it again. su 27. huhtik. 2025 klo 16.16 steven ***@***.***> kirjoitti: > *steven-hultrix* left a comment (facebook/yoga#1804) > <https://github.com/facebook/yoga/issues/1804#issuecomment-2833454819> > > What do you mean scroll-able areas? If your using OpenGL, Vulkan, Direct3D > use viewports and if your using Skia, Direct2D or Cairo then use clipping. > A scroll-able area is just a larger area clipped by a smaller area and you > match the width of your content to the container. Then the top of your > scrolling content is 0.0 and the bottom of your content is 1.0. > > — > Reply to this email directly, view it on GitHub > <https://github.com/facebook/yoga/issues/1804#issuecomment-2833454819>, > or unsubscribe > <https://github.com/notifications/unsubscribe-auth/AAJIXPHH63M2KMTBUJ2AVDD23TKCHAVCNFSM6AAAAABZPIGGOWVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDQMZTGQ2TIOBRHE> > . > You are receiving this because you authored the thread.Message ID: > ***@***.***> >
fredemmott commented 2025-07-07 05:18:03 -07:00 (Migrated from github.com)

I realised it was simpler than I expected

Ditto, I initially hugely overcomplicated this :)

Here's what I ended up doing:

  • put all of the content in one container - InnerContainer - which has YGOverflowVisible (default)
  • put that inside another container - OuterContainer - which has YGOverflowScroll
  • that goes inside my actual ScrollView
  • Set constraints on ScrollView based on InnerContainer's measured dimensions.
    • For example, I want 'as narrow as possible without a horizontal scroll bar', so I set min-width, and then I set max-height to the measured height for that width
    • InnerContainer's width and height are the actual size of your scrollable content
    • OuterContainer's width and height will be the size of your scrollable area, not the content
  • The actual scroll bars are direct children of ScrollView with YGPositionTypeAbsolute
    • For example, the vertical scroll bar may have top, bottom, and right == 0, while the horizontal may have left, right, and bottom = 0
  • Store the scroll offsets, use them when rendering, and while processing cursor/touch events

InnerContainer can be removed by:

  • using the content as InnerContainer, if you require that your scrollable area has a single logical child as its' content
  • iterating the children and calculating the size

On a side note, calculating the minimum width to avoid a horizontal scrollbar is somewhat irritating, because:

Using the workaround suggested at https://github.com/facebook/yoga/issues/1003#issuecomment-642888983 to work around the caching bug, here's my implementation:

Size GetMinimumWidthAndIdealHeight(YGNodeConstRef original) {
  const auto yoga = YGNodeClone(original);
  YGNodeStyleSetOverflow(yoga, YGOverflowVisible);
  YGNodeStyleSetFlexDirection(yoga, YGFlexDirectionRow);
  YGNodeStyleSetWidth(yoga, YGUndefined);
  YGNodeStyleSetHeight(yoga, YGUndefined);
  YGNodeCalculateLayout(yoga, YGUndefined, YGUndefined, YGDirectionLTR);
  float low = 128; // arbitrary
  float high = YGNodeLayoutGetWidth(yoga);
  while ((high - low) > 1) {
    const auto mid = std::ceil((low + high) / 2);
    YGNodeStyleSetWidth(yoga, mid);
    YGNodeCalculateLayout(yoga, YGUndefined, YGUndefined, YGDirectionLTR);
    if (YGNodeLayoutGetHadOverflow(yoga)) {
      if (mid - low < 1) {
        break;
      }
      low = mid;
    } else {
      if (high - mid < 1) {
        break;
      }
      high = mid;
    }
  }
  YGNodeStyleSetWidth(yoga, high);
  YGNodeCalculateLayout(yoga, YGUndefined, YGUndefined, YGDirectionLTR);
  const Size ret {
    YGNodeLayoutGetWidth(yoga),
    YGNodeLayoutGetHeight(yoga),
  };
  YGNodeFree(yoga);
  return ret;
}

As a binary search, it's reasonably fast for initialization or when constraints change, but isn't suitable for calling every frame.

> I realised it was simpler than I expected Ditto, I initially hugely overcomplicated this :) Here's what I ended up doing: - put all of the content in one container - `InnerContainer` - which has `YGOverflowVisible` (default) - put that inside another container - `OuterContainer` - which has `YGOverflowScroll` - that goes inside my actual `ScrollView` - Set constraints on `ScrollView` based on `InnerContainer`'s measured dimensions. - For example, I want 'as narrow as possible without a horizontal scroll bar', so I set `min-width`, and then I set `max-height` to the measured height for that width - `InnerContainer`'s width and height are the actual size of your scrollable content - `OuterContainer`'s width and height will be the size of your scrollable area, not the content - The actual scroll bars are direct children of `ScrollView` with `YGPositionTypeAbsolute` - For example, the vertical scroll bar may have top, bottom, and right == 0, while the horizontal may have left, right, and bottom = 0 - Store the scroll offsets, use them when rendering, and while processing cursor/touch events `InnerContainer` can be removed by: - using the content as `InnerContainer`, if you require that your scrollable area has a single logical child as its' content - iterating the children and calculating the size - in this case, I think what I've described is identical to what you've described - there is a solution addressing padding and margin here: https://github.com/facebook/yoga/issues/1807#issuecomment-2868193550 On a side note, calculating the minimum width to avoid a horizontal scrollbar is somewhat irritating, because: - Yoga does not directly support calculating the required size - #1126, #1298 - Caching bugs lead to layouts not actually being recalculated when you change the layout width - #1003 Using the workaround suggested at https://github.com/facebook/yoga/issues/1003#issuecomment-642888983 to work around the caching bug, here's my implementation: ```c++ Size GetMinimumWidthAndIdealHeight(YGNodeConstRef original) { const auto yoga = YGNodeClone(original); YGNodeStyleSetOverflow(yoga, YGOverflowVisible); YGNodeStyleSetFlexDirection(yoga, YGFlexDirectionRow); YGNodeStyleSetWidth(yoga, YGUndefined); YGNodeStyleSetHeight(yoga, YGUndefined); YGNodeCalculateLayout(yoga, YGUndefined, YGUndefined, YGDirectionLTR); float low = 128; // arbitrary float high = YGNodeLayoutGetWidth(yoga); while ((high - low) > 1) { const auto mid = std::ceil((low + high) / 2); YGNodeStyleSetWidth(yoga, mid); YGNodeCalculateLayout(yoga, YGUndefined, YGUndefined, YGDirectionLTR); if (YGNodeLayoutGetHadOverflow(yoga)) { if (mid - low < 1) { break; } low = mid; } else { if (high - mid < 1) { break; } high = mid; } } YGNodeStyleSetWidth(yoga, high); YGNodeCalculateLayout(yoga, YGUndefined, YGUndefined, YGDirectionLTR); const Size ret { YGNodeLayoutGetWidth(yoga), YGNodeLayoutGetHeight(yoga), }; YGNodeFree(yoga); return ret; } ``` As a binary search, it's reasonably fast for initialization or when constraints change, but isn't suitable for calling every frame.
cheery commented 2025-07-17 16:40:07 -07:00 (Migrated from github.com)

With your recipe I seem to get a neat scrolling container but it appears to scroll only horizontally or vertically. If I set outer_container.flex-direction to row, it scrolls as a row, and vice versa.

scroll view:
    height: 50%
    (clip the scroll view contents)
    outer container:
        (apply scroll shifting to outer container, t * max(0, inner container.size - scroll view.size))
        flex-direction: column #switch to row if you want horizontal scrolling
        overflow: scroll
        inner container:
            overflow: visible
            ...

But to my surprise I got a very passable bidirectional scrolling container by doing this:

scroll view:
    (clip the scroll view contents)
    height: 50% # or flex-shrink: 1
    outer container:
        (apply scroll shifting to outer container, t * max(0, inner container.size - scroll view.size))
        flex-direction: row
        inner container:
            flex-direction: column
            overflow: visible
            ...
Image

It looks like for dual scroll container you won't need scroll overflow setting.

With your recipe I seem to get a neat scrolling container but it appears to scroll only horizontally or vertically. If I set outer_container.flex-direction to row, it scrolls as a row, and vice versa. ``` scroll view: height: 50% (clip the scroll view contents) outer container: (apply scroll shifting to outer container, t * max(0, inner container.size - scroll view.size)) flex-direction: column #switch to row if you want horizontal scrolling overflow: scroll inner container: overflow: visible ... ``` But to my surprise I got a very passable bidirectional scrolling container by doing this: ``` scroll view: (clip the scroll view contents) height: 50% # or flex-shrink: 1 outer container: (apply scroll shifting to outer container, t * max(0, inner container.size - scroll view.size)) flex-direction: row inner container: flex-direction: column overflow: visible ... ``` <img width="808" height="642" alt="Image" src="https://github.com/user-attachments/assets/c28bfc24-c7e8-49e5-9e35-91a5741bb1e5" /> It looks like for dual scroll container you won't need scroll overflow setting.
fredemmott commented 2025-08-17 11:50:39 -07:00 (Migrated from github.com)

I think a lot of the varying behavior and requirements we're seeing is because of Yoga caching bugs, leading to small differences in our usages that should be insignificant actually changing things at a large scale. This feels likely due to Yoga being primarily designed for React Native, which is in turn primarily designed for fixed-size displays like full-screen on phones, rather than resizable windows.

For example, AIUI this code should have no observable effect beyond hurting performance, but sprinkling it in code paths with 'surprising' results often fixes the issue, and lets me keep my scroll handling code simple:

    FUI_ASSERT(YGNodeGetChildCount(yogaRoot) == 1);
    FUI_ASSERT(YGNodeGetChild(yogaRoot, 0) == yogaChild);
    YGNodeSetChildren(yogaRoot, &yogaChild, 1);
I think a lot of the varying behavior and requirements we're seeing is because of Yoga caching bugs, leading to small differences in our usages that *should* be insignificant actually changing things at a large scale. This feels likely due to Yoga being primarily designed for React Native, which is in turn primarily designed for fixed-size displays like full-screen on phones, rather than resizable windows. For example, AIUI this code *should* have no observable effect beyond hurting performance, but sprinkling it in code paths with 'surprising' results often fixes the issue, and lets me keep my scroll handling code simple: ```c++ FUI_ASSERT(YGNodeGetChildCount(yogaRoot) == 1); FUI_ASSERT(YGNodeGetChild(yogaRoot, 0) == yogaChild); YGNodeSetChildren(yogaRoot, &yogaChild, 1); ```
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: DaddyFrosty/yoga#1804
No description provided.