SOLVED Scrollbar problem [Partially solved]

FoxyOfJungle

Kazan Games
I'm trying to make a simple scrollbar work, I managed to do the main thing, which is to move it.

The scrollbar function returns a value between 0 and 1. What I want to do is move the Y of an area according to that 0 and 1.


The video below shows what happens:


The video is a bit long, but I did some tests just to show what happens if I modify something.



This is what I have:
Below is the code that makes drawing the scrollbar and scrolling the area:

GML:
//--- These variables below are the ones that will be used later: ---
// position
var _x1 = vx1 + 2;
var _y1 = vy1 + 2;
var _x2 = vx2 - 10;
var _y2 = vy2 - 2;

var _yyl = _y1 + gui_area_index[av].scrollarea_y;
var _hh = _y2 - _y1;

for (var i = 1; i < array_length(global.Layer_Wp_Array); i+=1)
{
    var _thh = string_height(_nn) + 24;
    var _sep = 4;

    /*-------------------
    Draw the array list here
    --------------------*/

    _yyl += (_thh + _sep);
}

//The height of the complete list can be obtained using the variable "_yyl"



//--- Draw the scrollbar ---

// set the height of the scroll (note that it is the size between the tip of the scroll and the end of the scroll area)
var _scroll_hh = max(1, _yyl - _y2); // limit scrollbar height between "1" and "the size that passes"


// draw scrollbar (return between 0 and 1)
draw_scrollbar(gui_area_index[av], vx2-7, vy1+2, vx2-2, vy2-2, _scroll_hh);


// move area
// "gui_area_index[av].scrollarea_y" goes from 0 and N/A. This is the variable that moves the array list from 0.
gui_area_index[av].scrollarea_y = -gui_area_index[av].scrollbar_pos * _hh;



I caused the scroll size to decrease if the list size passes:



^ Note that the _scroll_hh variable is precisely that value that passes ^





GML:
function draw_scrollbar(area_index, x, y, w, h, scrollh)
{
    /// @func draw_scrollbar(area_index, x, y, w, h, scrollh)
    /// @arg area_index
    /// @arg x
    /// @arg y
    /// @arg w
    /// @arg h
    /// @arg scroll_h
    // vai retornar um valor de 1 a 0

    var _old_color = draw_get_color();
    var _but_col = global.col_list_bg_lighter;
    var aa = area_index;
    var hhr = h - y;
    var scroll_h = hhr - scrollh;

    if (scroll_h <= 32) {scroll_h = 32;}


    // enable drag
    if point_in_rectangle(gui_mouse_x(), gui_mouse_y(), x, y+aa.scrollbar_y, w, y+aa.scrollbar_y+scroll_h)
    {
        _but_col = global.col_text;
  
        if mouse_check_button(mb_left)
        {
            aa.scrollbar_drag = true;
        }
    }

    // scroll
    if (aa.scrollbar_drag)
    {
        // drag
        _but_col = global.col_list_bg_lighter;
        aa.scrollbar_y = gui_mouse_y() + aa.scrollbar_yd;

        if (!mouse_check_button(mb_left))
        {
            aa.scrollbar_drag = false;
        }
    }
    else
    {
        aa.scrollbar_yd = aa.scrollbar_y - gui_mouse_y();
        if gui_area_inside(aa)
        {
            if mouse_wheel_up() aa.scrollbar_y -= 30;
            if mouse_wheel_down() aa.scrollbar_y += 30;
        }
    }

    // calculate height pos
    var _height_pos = y + hhr - y - scroll_h;

    // clamp scroll y
    aa.scrollbar_y = clamp(aa.scrollbar_y, 0, _height_pos);

    // get percentage
    aa.scrollbar_pos = aa.scrollbar_y / _height_pos;

    // draw scrollbar
    draw_set_color(_but_col);
    draw_rectangle(x, y+aa.scrollbar_y, w, y+aa.scrollbar_y+scroll_h, false);
    draw_set_color(_old_color);

    return aa.scrollbar_pos;
}

This script takes information from a struct "area_index", referring to each area, these are the variables within the struct:
GML:
// scrollbars
scrollarea_y = 0;
scrollbar_drag = false;
scrollbar_pos = 0;
scrollbar_yd = 0;
scrollbar_y = 0;



If you need more information I will show you.
I will be very grateful who can help me!

Thank you!
 
Last edited:

FoxyOfJungle

Kazan Games
I made a more simplified example for you to understand, I also uploaded the project, please help:





obj_canvas:

CREATE EVENT:

GML:
// others
global.col_list_bg_lighter = make_color_rgb(100,100,100);
global.col_text = make_color_rgb(200,200,200);


// scrollbar
scrollarea_y = 0;
scrollbar_drag = false;
scrollbar_pos = 0;
scrollbar_yd = 0;
scrollbar_y = 0;


// create array list
for (var i = 0; i < 15; ++i)
{
    list_array[i] = "Name_" +string(i);
}



DRAW GUI EVENT:

GML:
// area
var vx1 = 20;
var vy1 = 20;
var vx2 = 280;
var vy2 = display_get_gui_height()-20;

draw_rectangle(vx1, vy1, vx2, vy2, true);





// draw array list
var _x1 = vx1;
var _y1 = vy1;
var _x2 = vx2-10;
var _y2 = vy2;

var _yyl = _y1 + scrollarea_y;
var _hh = _y2 - _y1;

for (var i = 0; i < array_length(list_array); i+=1)
{
    var _nn = list_array[i];
    var _thh = string_height(_nn) + 24;
    var _sep = 4;
 
 
    // rectangle bg
    draw_set_color(c_dkgray);
    draw_rectangle(_x1, _yyl, _x2, _yyl+_thh, false);
 
    // text
    draw_set_color(c_white);
    draw_set_valign(fa_middle);
    draw_text(_x1+32, _yyl+_thh/2, _nn);
    draw_set_valign(fa_top);



    _yyl += (_thh + _sep);
}





// set the height of the scroll (note that it is the size between the tip of the scroll and the end of the scroll area)
//var _scroll_hh = max(1, _yyl - _y2); // limit scrollbar height between "1" and "the size that passes"
var _scroll_hh = 150;



// draw scrollbar (return between 0 and 1)
draw_scrollbar(object_index, vx2-7, vy1+2, vx2-2, vy2-2, _scroll_hh);



// move area
// "gui_area_index[av].scrollarea_y" goes from 0 and N/A. This is the variable that moves the array list from 0.
scrollarea_y = -scrollbar_pos * _hh;

GML:
function draw_scrollbar(area_index, x, y, w, h, scrollh)
{
    /// @func draw_scrollbar(area_index, x, y, w, h, scrollh)
    /// @arg area_index
    /// @arg x
    /// @arg y
    /// @arg w
    /// @arg h
    /// @arg scroll_h
    // vai retornar um valor de 1 a 0

    var _old_color = draw_get_color();
    var _but_col = global.col_list_bg_lighter;
    var aa = area_index;
    var hhr = h - y;
    var scroll_h = hhr - scrollh;

    if (scroll_h <= 32) {scroll_h = 32;}


    // enable drag
    if point_in_rectangle(device_mouse_x_to_gui(0), device_mouse_y_to_gui(0), x, y+aa.scrollbar_y, w, y+aa.scrollbar_y+scroll_h)
    {
        _but_col = global.col_text;
  
        if mouse_check_button(mb_left)
        {
            aa.scrollbar_drag = true;
        }
    }

    // scroll
    if (aa.scrollbar_drag)
    {
        // drag
        _but_col = global.col_list_bg_lighter;
        aa.scrollbar_y = device_mouse_y_to_gui(0) + aa.scrollbar_yd;

        if (!mouse_check_button(mb_left))
        {
            aa.scrollbar_drag = false;
        }
    }
    else
    {
        aa.scrollbar_yd = device_mouse_y_to_gui(0) - aa.scrollbar_y;
        if mouse_wheel_up() aa.scrollbar_y -= 30;
        if mouse_wheel_down() aa.scrollbar_y += 30;
    }

    // calculate height pos
    var _height_pos = y + hhr - scroll_h;

    // clamp scroll y
    aa.scrollbar_y = clamp(aa.scrollbar_y, 0, _height_pos);

    // get percentage
    aa.scrollbar_pos = aa.scrollbar_y / _height_pos;

    // draw scrollbar
    draw_set_color(_but_col);
    draw_rectangle(x, y+aa.scrollbar_y, w, y+aa.scrollbar_y+scroll_h, false);
    draw_set_color(_old_color);

    return aa.scrollbar_pos;
}








Any help is appreciated.
Thank you.
 
Last edited:

TailBit

Member
The only problem I see is that you don't draw the content in the list from the correct starting y position:
GML:
var _yyl = _y1 + (list_height - scrollbar_height)*scrollbar_pos;
Unless scrollarea_y is something like (list_height - scrollbar_height)*scrollbar_pos

But .. just to be sure .. what is the problem?
 

Joe Ellis

Member
The trick is to draw the scrollbar middle button that you grab with the mouse, at the position of the ratio it's at based on the height of the scrollbar, and the scroll height\length of the draw area based on the current y pos of the view\scroll position.
So:
Code:
middle_block_y = bar_height * (target_area.scroll_y / target_area.scroll_height)
The target area's scroll_y is how many pixels you've scrolled downwards, and it's scroll height is the the height of what it displays if it could be displayed on a whole screen (in pixels). So like 3028.

I don't know how else to explain it until you reply, but I thought I'd post the whole script of my scrollbar, if the code can explain better than I can

GML:
function gui_scrollbar(_x, _y, height, target) {

    var obj = gui_create(_x - 1, _y);
    obj.parent = my_id
    obj.width = 16
    obj.height = height
    obj.target = gui_objects[target]
    obj.target.scrollbar = obj
    obj.up_held = false
    obj.down_held = false
    obj.block_held = false
    obj.block_y = 21
    obj.bar_height = height - 44
    obj.step_event = method(obj, gui_scrollbar_step)
    obj.held_method = method(obj, gui_scrollbar_hold)
    obj.s = surface_create(16, height)
   
    gui_objects[@ ++num_gui_objects] = obj

    with obj
    {gui_scrollbar_refresh()}
}

function gui_scrollbar_step() {

    if target.scroll_height != height
    && mouse_check_button_pressed(mb_left)
    {

    var my = mouse_y - parent.y - y;
    anchor_y = 0

    if my < 15
    {up_held = true}
    else
    {

    if my > height - 15
    {down_held = true}
    else
    {
    block_held = true

    if abs(my - block_y) < 8
    {anchor_y = my - block_y}

    }
    }

    oheld = global.editor_id.held
    global.editor_id.held = my_id
    gui_scrollbar_refresh()
    }
}

function gui_scrollbar_hold() {

    var my = (mouse_y - parent.y - y) - anchor_y;

    if block_held
    {target.scroll_y = ceil(((my - 21) / bar_height) * target.scroll_height)}
    else
    {target.scroll_y += (down_held - up_held) * 3}

    if mouse_check_button_released(mb_left)
    {
    global.editor_id.cursor = cur_normal
    global.editor_id.held = oheld
    up_held = false
    down_held = false
    block_held = false
    }

    target.scroll_y = clamp(target.scroll_y, 0, target.scroll_height)
    gui_scrollbar_refresh()
    with target
    {refresh_event()}
    gui_form_refresh(parent)
}

function gui_scrollbar_refresh() {

    surface_set_target(s)
    view_set_2d(0, 0, width, height)

    if target.scroll_height = height
    {draw_clear_alpha(0, 0)}
    else
    {
    block_y = 21 + floor(bar_height * (target.scroll_y / target.scroll_height))
    draw_clear(make_colour_rgb(225, 225, 225))
    draw_sprite_stretched(spr_pixel, 0, 0, 0, 1, height)
    draw_sprite(spr_arrowbutton, up_held, 1, 0)
    draw_sprite(spr_arrowbutton, 2 + down_held, 1, height - 15)
    draw_sprite(spr_scrollblock_y, 0, 1, block_y)
    }

    surface_reset_target()
}
Also the ratio you're using from 0-1 can still be used, but it'd always be based on the scroll_y divided by the scroll_height, so it might be adding an extra calculation that isn't needed, although once you have this ratio it might make other things easier. Btw let me know if you want more help, cus my scrollbars have been working for about 2 years or so, so I can def get them working for you haha
 
Last edited:

FoxyOfJungle

Kazan Games
The only problem I see is that you don't draw the content in the list from the correct starting y position:
GML:
var _yyl = _y1 + (list_height - scrollbar_height)*scrollbar_pos;
Unless scrollarea_y is something like (list_height - scrollbar_height)*scrollbar_pos

But .. just to be sure .. what is the problem?
Thanks for the reply and for trying to help me.
In fact, I can't draw it in the right position.


Unless scrollarea_y is something like (list_height - scrollbar_height)*scrollbar_pos
It was meant to be. Since I moved the variable to after drawing the array.



This is the code I have after your explanation and unfortunately it didn't work, did I do something wrong?

GML:
//var _scroll_hh = max(1, _yyl - _y2)
var _scroll_hh = 150;
scrollarea_y = -scrollbar_pos * (_hh - _scroll_hh);

// _hh = Relative size from position Y1 to Y2, is the same as the one you use in surface_create() for example.
// At the end of the "for" loop, the "_yyl" variable returns the height of the array.
// I don't know if I put the variables in the right places, but I tested them in different ways and it doesn't works.


// draw scrollbar (return between 0 and 1)
draw_scrollbar(object_index, vx2-7, vy1+2, vx2-2, vy2-2, _scroll_hh);



Basically what I want to do is make the scrollbar look like this:




Note that the Y position of the list of items goes all the way to the end and does not pass. Like this:



Thank you!


----------------------------------------------------------------------------------------------

@Joe Ellis
I saw that you just posted something, I will check what you wrote and post another reply, thanks!
 

FoxyOfJungle

Kazan Games
I found out what the problem was... What happens is that the variable was calculating the height of the array incorrectly, maybe it happened because of the loop, I don't know, but I created another variable to define the height and it worked:

GML:
// draw array list
var _x1 = vx1;
var _y1 = vy1;
var _x2 = vx2-10;
var _y2 = vy2;

var _yyl = _y1 + scrollarea_y;
var _hh = _y2 - _y1;
var _sh = 0;

for (var i = 0; i < array_length(list_array); i+=1)
{
    var _nn = list_array[i];
    var _thh = string_height(_nn) + 24;
    var _sep = 4;
   
   
    // rectangle bg
    draw_set_color(c_dkgray);
    draw_rectangle(_x1, _yyl, _x2, _yyl+_thh, false);
   
    // text
    draw_set_color(c_white);
    draw_set_valign(fa_middle);
    draw_text(_x1+32, _yyl+_thh/2, _nn);
    draw_set_valign(fa_top);


    _yyl += (_thh + _sep);
    _sh += (_thh + _sep);
}


//var _scroll_hh = max(1, _sh - _y2)
var _scroll_hh = 150;

scrollarea_y = -scrollbar_pos * (_sh - _hh);



// draw scrollbar (return between 0 and 1)
draw_scrollbar(object_index, vx2-7, vy1+2, vx2-2, vy2-2, _scroll_hh);

I added _sh to check the height. So your formula was correct @TailBit ! Thank you so much!
Thank you for your help @Joe Ellis !


Solved! 🙂



Something that seemed simple but I killed myself to do it 😅
 

Joe Ellis

Member
Simple things like this do make you almost kill yourself with the effort to do it. That simple that it's like a mind f**k, but once you get it working, you never have to touch it again. That's computers for you. One thing to take from this is to remember that the solution is often a lot simpler than your human brain is thinking 😆 You need to learn how to think or make your brain work like a computer, when you've done that let me know and I can start doing it haha
 

TailBit

Member
If all the elements have the same height, then you could skip right to the first visible element and not draw the others at all, but yeah, if you need the different height for each then that is the way to go .. buuut you could still skip the drawing part it the bottom of it wasn't inside the list box, and also end it when the y gets below the list area:

GML:
var _nn;
var _thh = string_height(_nn) + 24;
var _sep = 4;

_sh += _nn*(_thh + _sep);

for (var i = scrollarea_y div (_thh+_sep); i < array_length(list_array); i+=1)
{
    _nn = list_array[i];
    _yy1 = _y1 + scrollarea_y + i*(_thh+sep)

    // rectangle bg
    draw_set_color(c_dkgray);
    draw_rectangle(_x1, _yyl, _x2, _yyl+_thh, false);

    // text
    draw_set_color(c_white);
    draw_set_valign(fa_middle);
    draw_text(_x1+32, _yyl+_thh/2, _nn);
    draw_set_valign(fa_top);

}
I would also define most of them before the loop, as all but _thh would have the same value anyway.

EDIT: scrollarea_y div (_thh+_sep) not _yy1 div (_thh+_sep), Joe got this one correct x3
EDIT2: the list_array have to be there too
 
Last edited:

Joe Ellis

Member
Yeah I make my things skip drawing till the first visible one, you're not really saving a huge lot of time, but it's still worth doing cus maybe if you had a list that's 400 things long it'd stop it from lagging.

You do that with a simple floor((scroll_y / scroll_height) * slot_height), then set the i\start index to that
 
Last edited:

FoxyOfJungle

Kazan Games
I just transferred everything to the program and it's perfect now:




I also put a smooth to get better. 🙂


The final code boiled down to this:
GML:
// draw scrollbar
draw_scrollbar(gui_area_index[av], vx2-7, vy1+2, vx2-2, vy2-2, _sh-_sp, _hh, 0.2);
//draw_scrollbar(area_index, x1, y1, x2, y2, scroll_height, area_height, scroll_smooth)
Thanks again! 😄
 
Last edited:

FoxyOfJungle

Kazan Games
If all the elements have the same height, then you could skip right to the first visible element and not draw the others at all, but yeah, if you need the different height for each then that is the way to go .. buuut you could still skip the drawing part it the bottom of it wasn't inside the list box, and also end it when the y gets below the list area:

GML:
var _nn = list_array[i];
var _thh = string_height(_nn) + 24;
var _sep = 4;

_sh += _nn*(_thh + _sep);

for (var i = scrollarea_y div (_thh+_sep); i < array_length(list_array); i+=1)
{
    _yy1 = _y1 + scrollarea_y + i*(_thh+sep)

    // rectangle bg
    draw_set_color(c_dkgray);
    draw_rectangle(_x1, _yyl, _x2, _yyl+_thh, false);

    // text
    draw_set_color(c_white);
    draw_set_valign(fa_middle);
    draw_text(_x1+32, _yyl+_thh/2, _nn);
    draw_set_valign(fa_top);

}
I would also define most of them before the loop, as all but _thh would have the same value anyway.

EDIT: scrollarea_y div (_thh+_sep) not _yy1 div (_thh+_sep), Joe got this one correct x3
One more thing, I am a little confused with this code, will I have to use two loops for this? Because in the first line it calls the "i" in the array, but there is nothing before.
var _nn = list_array[i];


How could I do this to limit the items visible in the following situation?
The code is similar to the previous one, but it uses instances. This is currently not the complete code. I removed unnecessary things like buttons.

GML:
// area size [not relevant in this situation]
var _x1 = vx1 + 2;
var _y1 = vy1 + 2;
var _x2 = vx2 - 10;
var _y2 = vy2 - 2;



// draw instances
var _yyl = _y1 + gui_area_index[av].scrollarea_y;
var _hh = _y2 - _y1;
var _sh = 0;
var _sp = 4;

for (var i = 0; i < instance_number(obj_gwp_parent); i+=1)
{
    // position for layer list
    var _id = instance_find(obj_gwp_parent, i);
    var _nn = _id.gwp_name;
    var _thh = string_height(_nn) + 6;
    var _button_col = c_white;
    var _button_txt_col = c_white;


    // rectangle bg
    draw_set_color(_button_col);
    draw_rectangle(_x1, _yyl, _x2, _yyl+_thh, false);


    // instance name
    draw_set_color(_button_txt_col);
    draw_set_valign(fa_middle);
    draw_text(_x1+32, _yyl+_thh/2, _nn);
    draw_set_valign(fa_top);


    _yyl += (_thh + _sp);
    _sh += (_thh + _sp);
}


// draw scrollbar
draw_scrollbar_v(gui_area_index[av], vx2-7, vy1+2, vx2-2, vy2-2, _sh-_sp, _hh, 0.2);

Thank you.
 
Last edited:

TailBit

Member
Making it jump to the first one that should be drawn is easiest if the height of each of them is the same, which it isn't because of the string_height

otherwise you will have to check all of them if they are in the draw area, then do the draw part of it, it would still have to go through all the first loops
GML:
    if(_y1>_yyl+_thh){
        // rectangle bg
        draw_set_color(_button_col);
        draw_rectangle(_x1, _yyl, _x2, _yyl+_thh, false);


        // instance name
        draw_set_color(_button_txt_col);
        draw_set_valign(fa_middle);
        draw_text(_x1+32, _yyl+_thh/2, _nn);
        draw_set_valign(fa_top);
    }
Instead of just finding the i from a set size:
GML:
var _boxh = string_height("|") + 6 + _sp; // "|" is the tallest character?

var i = gui_area_index[av].scrollarea_y div _boxh
_yyl += i * _boxh;

// it feels so wrong to just write 0 there, but I wanted to use the I to increase _yy1 early
// otherwise you would have to set it to the correct value in the loop instead of increasing it
for (0; i < instance_number(obj_gwp_parent); i+=1)
{
    // position for layer list
    var _id = instance_find(obj_gwp_parent, i);
    var _nn = _id.gwp_name;
    var _button_col = c_white;
    var _button_txt_col = c_white;


    // rectangle bg
    draw_set_color(_button_col);
    draw_rectangle(_x1, _yyl, _x2, _yyl+_boxh, false);


    // instance name
    draw_set_color(_button_txt_col);
    draw_set_valign(fa_middle);
    draw_text(_x1+32, _yyl+_boxh/2, _nn);
    draw_set_valign(fa_top);


    _yyl += _boxh;
}
_sh = _boxh * instance_number(obj_gwp_parent);
BUT ..there is a way to use the variable height, but then you need to keep track of the position and index of the first element that is visible .. and when moving the bar you calculate the crollarea_y difference and then scan forward or backward to see if the start_pos and start_index need to change .. but I tend to avoid that :p
 
Top