Handling only user input events

I’m wondering how to separate events generated by user activity from events generated by the program itself updating its components. For example, a user can press the buttons or drag the thumb on a scroll component, but the scroll component can also be updated by the program itself when some data changes. In the first case, the program should take the user’s input into account, and in the second one the event should be ignored, but in both cases the scroll component will dispatch a change event.

Since the event is dispatched only later when the component is validated, it is not possible to temporarily disable events when e.g. updating a component due to the program’s internal activity. What would be the right way to handle such situations?

Hi Kris,

This is an interesting issue, i wonder if a “userGenerated” flag on the event itself makes any sense here? Personally, when i design applications i try to make it so it doesnt matter where the event comes from - i wonder if you have a specific example?

Ive certainly hit cases where ive gone “ah, yeah, thats not user input” but generally ive realised that my logic isnt “right” and im relying on something i shouldnt (at least in the cases im remembering).

A solid example would be really nice here - another issue to consider would be native backends, there is certainly a limitation to “user generated” and “generated” in that regard (not so much with composite backends where haxeui has “total” control), but i have defo had some headscratchers around this area in the past - usually getting around them by just structuring things a little differently.

Id defo be interested in examples / suggestions though.

Cheers,
Ian

Hi Ian,

Thank you for the quick response! I’m indeed wondering if I’m approaching this issue in the right way and I also don’t know how this (seemingly quite universal) problem is generally handled by UI libraries. As I see it, the main issue is that the UI must both reflect the internal state of the program, as well as manipulate that state based on user input, and both situations result in the variables of the UI component itself being changed, which in turn dispatches a change event.

Perhaps a scroll view would be a practical example of this? There is the view area that shows a part of some larger set of information (e.g. rows in a table), and then there is a Scroll component on the side. When the user pushes the up and down buttons or drags the thumb, the information visible in the component should change (e.g. the subset of rows that is shown), and I guess this should be done as response to the onChange event?

However, when the information is changed due to reasons other than user input (e.g. the table could receive new rows which are slowly generated by some internal process), the Scroll also needs to be updated, for example the max value needs to change if new items are added to the table.

If the component has an onChange handler, it will respond to user input, but it will also respond to the latter case of being updated due to an internal state change. For example, on receiving a new row, the component updates its table and its scroll bar’s max value, then the scroll bar dispatches a change event whose handler again updates the table, but after this the position should no longer change, I guess.

It just feels wrong to trigger such a bidirectional user interface event chain in response to an internal state change, and I’m wondering if some more complex use case could result in a longer back-and-forth loop? In any case, now that I’m writing this example, I also realize that I don’t actually know how the default HaxeUI ScrollView (and the ListView and TableView derived from it) handles this situation?

Best Regards,
Kris

I came up with a more concrete example, based on how one might implement a video player component. As in most video players, a horizontal slider is used here both for displaying the current playback position, as well as for allowing the user to move this position. The problem occurs when the program internally updates the slider to reflect the playback position, which in turn triggers a CHANGE event, which in turn again moves the playback position.

interface IVideo
{
	public var position(get,never):Int; // Returns the current position from the video playback thread

	public function play():Void; // Starts the playback (from the current position) in a background thread
	public function pause():Void; // Stops the playback in the background thread
	public function seek(progress:Int):Void; // Moves the video playback position
	
	public function addPlaybackListener(callback:Int->Void):Void; // The listeners are informed of the current position of the playback thread every x milliseconds while the video is playing
}

class VideoPlayer
{
	var video:IVideo;
	var slider:haxe.ui.components.HorizontalSlider;

	public function new()
	{
		video = new Video();
		video.addPlaybackListener(onVideoPlayback);

		slider = new HorizontalSlider();
		slider.onChange = onSliderChange;
	}
	
	function onSliderChange(event:haxe.ui.events.UIEvent)
	{
		/**
		* The purpose of this handler is to move the playback position when the user
		* moves the slider. However, this handler will also receive events triggered 
		* by setting slider.pos in onVideoPlayback.
		* If the event has been triggered by onVideoPlayback, this handler will 
		* incorrectly pause the video and move it to a by now earlier position.
		* It is not possible to detect here whether the UIEvent results from setting
		* slider.pos in onVideoPlayback or from actual user input.
		*/
		
		// When the user moves the slider, pause the playback and move the video position
		video.pause();
		video.seek(Math.round(slider.pos));
	}

	function onVideoPlayback(position:Int)
	{
		/**
		* Setting slider.pos here will dispatch (later in the slider component's
		* validateData function) an unnecessary CHANGE event which will be handled
		* by onSliderChange.
		*/
		
		// Update the UI slider to reflect the progressing video playback
		slider.pos = position;
	}
}

If my understanding of this problem is correct, I think there could be two ways of solving the event feedback loop issue. The first would be to have a way of modifying a HaxeUI component’s variables without triggering a change event. However, this would mean that anyone else listening to a component’s events could no longer assume that all component state changes result in an event.

The other option as you suggested would be to add a “userGenerated” flag to events, or alternatively dispatch entirely separate events. The flag option would be simple and consistent, but could lead to unnecessary event churn in cases like the video player where the position is constantly updated. Separate events for user input would avoid this (if there are no listeners no event needs to be dispatched), but would make the event interface more complicated.

Anyway, these are just my thoughts on the issue, and I’m still not sure if I’m looking at it the right way, but I think the video player example shows a situation where this could be a real problem.

So i was thinking about this and wasnt sure if would be an issue or not, so thought i would make a fake app just to make sure - the reason why it isnt an issue i think is because haxeui only dispatches events if something has changed. So if the slider pos and the video playback are synced as they should be, then there shouldnt be any “fighting” over events. Heres my (very ugly / basic) test app:

var fakeVideo = new FakeVideoPlayback();

var slider = mainView.findComponent("seek", Slider);
slider.max = fakeVideo.maxPos;

var label = mainView.findComponent("pos", Label);

fakeVideo.onCurrentPosChanged = function() {
    slider.pos = fakeVideo.currentPos;
    label.text = fakeVideo.currentPos + "/" + fakeVideo.maxPos;
}
slider.onChange = function(e) {
    fakeVideo.currentPos = Math.round(slider.pos);
}

mainView.findComponent("play", Button).onClick = function(e) {
    fakeVideo.play();
}
import haxe.ui.util.Timer;

class FakeVideoPlayback {
    private var _timer:Timer = null;
    
    public var maxPos:Float = 1234;
    
    public var onCurrentPosChanged:Void->Void = null;
    
    public function new() {
    }    
    
    private var _currentPos:Float = 0;
    public var currentPos(get, set):Float;
    private function get_currentPos():Float {
        return _currentPos;
    }
    private function set_currentPos(value:Float):Float {
        if (_currentPos == value) {
            return value;
        }
        
        _currentPos = value;
        if (onCurrentPosChanged != null) {
            onCurrentPosChanged();
        }
        
        return value;
    }
    
    public function play() {
        if (_timer != null) {
            stop();
        }
        
        _timer = new Timer(100, onTimer);
    }
    
    public function stop() {
        _timer.stop();
        _timer = null;
        currentPos = 0;
    }
    
    private function onTimer() {
        currentPos++;
        if (currentPos > maxPos) {
            stop();
        }
    }
}

fake_video

Cheers,
Ian

1 Like

Thank you for the detailed and testable implementation! The test program certainly looks smooth and free of event fighting. If there would be a conflict between events, I assume it could only happen if the internal state is changed again before the slider’s change event is dispatched. In the video example, I think this could happen if _timer has an update interval smaller than the validation timer. In that case FakeVideoPlayback._currentPos could change again before the CHANGE event from validateData would reach onCurrentPosChanged, in which case the ‘if (_currentPos == value)’ check would not be enough to stop event fighting.

If I understood the HaxeUI event model correctly, the CHANGE event gets dispatched in validateData, which is always called by a timer? Since the CallLater made by ValidationManager runs without a delay (and on the ENTER_FRAME event on OpenFL), I think the change event will always be dispatched before there is time to update _currentPos again, so the values will stay in sync.

In any case, I think the issue I had with the scroll bar can be solved best by simply checking whether the internal value has changed from the component’s value before updating it. Thank you again for the quick response and advice!

1 Like