SOLVED Circular Dependency Between Scrollfield and Scrollbar

TheMagician

Member
I'm coding a UI module where I'm running into a problem with a circular dependency between the scrollfield and the scrollbar.

The scrollfield struct has a method set_y() with which you can set the y-offset of the content of the scrollfield.

The scrollbar struct has a method set_value() with which you can set the slider's offset in percent (0 to 1) along the scrollbar.

When the y-offset of the scrollfield is changed via set_y() (e.g. by scrolling the mouse wheel while hovering over the scrollfield) I call the scrollbar's set_value() method from within the scrollfield's set_y() method to update the scrollbar accordingly.

When the value of the scrollbar is changed via set_value() (e.g. by using the mouse to drag the slider up and down) I call the scrollfield's set_y() method from within the scrollbar's set_value() method to update the scrollfield offset accordingly.

This creates an endless loop where the two methods keep calling each other as soon as I either call set_y() on the scrollfield or set_value() on the scrollbar.

What would be an (elegant) solution to this problem?

Of course I could say that you just always have to call both functions separately, one after the other, if you want to update the scrollfield or the scrollbar. However, it feels to me like the scrollfield should automatically take care of its scrollbar and vice-versa - even if you only change one of the two.
 

Alice

Toolmaker of Bucuresti
Forum Staff
Moderator
How about two external methods set_y() and set_value(), and two internal methods adjust_y() and adjust_value()?

set_y changes the scrollfield and scrollbar positions to match the given scrollfield y.
set_value changes the scrollfield and scrollbar positions to match the given scrollbar value.
adjust_y just sets the y of the scrollfield, and that's it.
adjust_value just sets the value of the scrollbar, and that's it.

And then, set_y and set_value would be like:
GML:
var target_y = ...; // determine scrollfield y based on the provided y/value
with (scrollfield) { adjust_y(target_y); }
var target_value = ...; // determine scrollbar value based on the provided y/value
with (scrollbar) { update_value(target_value); }
Another alternative would be having scrollbar position directly bound to scrollfield y, and set_value on scrollbar would actually change the scrollfield.y directly - you don't need to synchronise values if there's only one!
 

NightFrost

Member
This is the way I'm building mine (about three quarters ready so I still might run into problems with implementation). I have scrollbar item (a struct) that knows the size and positions of both scrollarea and the bar, and has a content ID array. If a scrollbar drag is performed on the bar or it is clicked to make it move, a new scroll value gets calculated. Otherwise, if mousewheel operation is detected and mouse hovers over the scrollarea, a new scroll value gets calculated. If scroll value has changed, content list is looped through and a method on the target is called to adjust its position. The content can be both instances and structs, as they are called the same way: with dot notation. The scrollbar doesn't really care, all it does is call a method on an ID value. Instances and methods can call a attach() method on the scrollbar to add their ID to content array.

The scrollarea is a surface. When instances and structs attached to scrollarea do a draw, they target the surface. This is because I don't want to mess around with partial draws when something is scrolled half off the visible region. Drawing to surface solves this. Draw order doesn't matter as scroll content is never supposed to overlap. Also, if scrollable content has some hover and click actions (like other UI elements would) they first check the mouse is within bounds of scrollarea, otherwise click is ignored. This means you cannot click UI elements beyond visible scrollarea.

This design has one aspect I don't wholly like: the scrollarea content must be aware that it is on a scrollarea, and have extra code to handle that (repositioning, draws and the partial click regions). I would like to make it completely transparent, meaning no extra code on the content and scrollbar would handle everything, but I don't see a way to do that. The draws I might be able to solve by making things invisible as part of attach() call, then have scrollbar call their draws to surface target. But I'm not sure if invisible instance's draw can be called at all. Gating click event handling to only inside the scrollarea seems impossible without some code on the items themselves.

I guess I could redefine the scrollarea as another camera view, but I like that idea even less.
 
Last edited:

TheMagician

Member
How about two external methods set_y() and set_value(), and two internal methods adjust_y() and adjust_value()?
That sounds like a good idea. Will try and implement that. Thank you very much!


This is the way I'm building mine...
Actually my system is almost identical. I also have the slider as part of a superordinate scrollbar struct. The only reason I'm not using the scrollbar as the controlling element right now is that in my system it is theoretically allowed to have a scrollfield without an attached scrollbar (which might not be the best design decision, I know). But thank you for sharing your method. It is definitely something I will consider! I could still have a "virtual" scrollbar attached that checks whether there is an actual scrollbar and then updates it accordingly.

It also seems like we have very similar systems of drawing and click detection. However, I've decided that my individual elements don't do their own click detection or their own drawing but instead the big update loop and draw loop of the UI struct does the click detection and drawing. That way, I only need one extra flag in the elements which stores whether or not the element is on a scrollfield. When I go through the list of UI elements and an element is flagged as part of a scrollfield then I do all the necessary calculations to detect the proper click (e.g. subtracting the x/y offset of the scrollfield, making sure the mouse is inside the scrollfield when an element is only partially drawn, etc.).

The only other bit you need to do is that when initializing an element which is on a scrollfield you have to subtract the scrollfield's x/y position from the element's position so that it is positioned relative to 0/0 which is important when drawing the surface. That way, I don't even need different draw methods for the elements. The only difference is whether you call the draw method from the normal draw context or from within a surface draw call.
 

NightFrost

Member
However, I've decided that my individual elements don't do their own click detection or their own drawing but instead the big update loop and draw loop of the UI struct does the click detection and drawing.
Yeah, I didn't want to go into unnecessary detail but that's how my system operates as well. While the element structs house the actual hover check method by inheriting from the base element struct (and override if a more complex than basic box detection is needed, ie a slider) the actual looping is handled by a UI controller object. The elements have a boolean flag that gets set when they attach themselves to scrollarea, just like you do, and then check it to branch their code to behave in correct manner.

It is this flagging and branching I would like to get rid of, as I have a feeling in my gut that adding behavioral exceptions is not a "correct" way to build this, as it builds up more and more the more exceptions one adds. I feel that the behavior should be the same, and the context in which they work (normal or scrollarea) should be the one to frame their behavior correctly. This would allow for easy extension just by defining a new context in one place only instead of adding code to all elements.

I get odd hangups like that from time to time when I ponder what is good mechanical structure to tackle a problem.
 

TheMagician

Member
I feel you. To be honest, I have been working on this UI system on and off for over 2 years and have rewritten it several times. The latest rewrite was triggered by the GML updates in 2.3 and I feel it is the cleanest approach to date.

You're right, it is annoying to add new flags to stuff that you only need for specific contexts - like I have one to check if a button is part of a list entry or whether something is part of a modal window. I've gotten rid of some by using a system of parenting where you just add a parent element to each element and use that to determine its context but I haven't gotten rid of all these flag, yet.

Well, I guess that's why there are dedicated UI programmers in the real world :p
 
Top