Animating a ScrolledWindow

The other day I worked on improving the auto-scroll in Fractal (a super cool GTK+ Matrix Client). While doing this I discovered some nice features in GTK+.

Scrolling the view automatically (e.g. by pressing a button) is quite easy, you just need to set the value of vadjustment or hadjustment and then the view jumps to the new value.
This code moves the view to the bottom (yes, it’s Rust 😎):

if let Some(adj) = view.get_vadjustment() {
  adj.set_value(adj.get_upper() - adj.get_page_size());

View contains our ScrolledWindow and upper – page_size is the bottom of our view.

Awesome! We are done here!

But wait! What if we want to have a fancy animation, without the sudden jump? That’s exactly what we wanted in Fractal.

Let’s look at how we can add the smooth animation between the starting point and the end point.

The idea behind this is to update the position of the ScrolledWindow gradually instead of all at once. GTK provides us with a pulse to animate the state change, called tick. So we need to increase the value of the adjustment at each “tick”. A tick is fired every time GTK draws a new frame, so we don’t need to think about keeping in sync with the frame rate. For attaching a callback to the tick we use the function add_tick_callback(). We need also the frame clock, the start “tick”. We get the the clock with get_frame_clock(). This allows us to define the length of the animation, because the end of the transition will be start_clock + animation_duration. Now all we need is to calculate the position of the view at each “tick”.

This code snippet is inspired by the GTK+ source code. It moves a ScrolledWindow to the end of the view and could be adapted to scroll automatically (with an animation) to any point:

fn scroll_down(view: gtk::ScrolledWindow, animate: bool) {
    if let Some(adj) = view.get_vadjustment() {
        if animate {
            if let Some(clock) = view.get_frame_clock() {
                let duration = 200;
                let start = adj.get_value();
                let end = adj.get_upper() -
                let start_time = clock.get_frame_time();
                let end_time = start_time + 1000 * duration;
                view.add_tick_callback(move |_view, clock| {
                    let now = clock.get_frame_time();
                    if now < end_time &&
                       adj.get_value() != end {
                        let mut t = (now - start_time)
                                     as f64 /
                                     (end_time - start_time)
                                     as f64;
                        t = ease_out_cubic(t);
                        adj.set_value(start + 
                                      t * 
                                      (end - start));
                        return glib::Continue(true);
                        adj.set_value (end);
                        return glib::Continue(false);
        else {
            adj.set_value(adj.get_upper() -

Instead of a linear animation I used a cubic easing function (the code was found inside the GTK+ source)

/* From clutter-easing.c, based on Robert Penner's
 * infamous easing equations, MIT license.
fn ease_out_cubic (t: f64) -> f64 {
  let p = t - 1f64;
  return p * p * p + 1f64;

This code is from Fractal. The complete commit can be found here. I really hope that this post saves somebody from reading the GTK+ source code like I had to do, to figure out how to animate a simple ScrolledWindow 🙂

3 thoughts on “Animating a ScrolledWindow”

  1. Or you could use GTK+’s built-in animations by using the ::scroll-child signal (code in C, as I’m not familiar with Rust):

    gboolean ret;
    g_signal_emit_by_name (scrolled_window, “scroll-child”,

    1. I finally had some time to check it out. This is Florians line in rust:
      scroll.emit("scroll-child", &[&gtk::ScrollType::End, &false]);

      The only problem with it is just that you can’t scroll to any position, and can’t control the animation, like speed and ease funtion.

Leave a Reply

Your email address will not be published. Required fields are marked *