Clone in a terminal:
TableLayout.vala
/*
	Copyright (C) 2014 2015 Johan Mattsson

	This library is free software; you can redistribute it and/or modify 
	it under the terms of the GNU Lesser General Public License as 
	published by the Free Software Foundation; either version 3 of the 
	License, or (at your option) any later version.

	This library is distributed in the hope that it will be useful, but 
	WITHOUT ANY WARRANTY; without even the implied warranty of 
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 
	Lesser General Public License for more details.
*/

using Cairo;
using Math;

namespace BirdFont {

public class TableLayout : FontDisplay {
	
	public double scroll = 0;
	public double content_height = 1;
	public WidgetAllocation allocation = new WidgetAllocation ();
	public Gee.ArrayList<Widget> widgets = new Gee.ArrayList<Widget> ();
	public Gee.ArrayList<Widget> focus_ring = new Gee.ArrayList<Widget> ();
	public int focus_index = 0;
	bool ignore_input = true;
		
	public Widget? keyboard_focus = null;
	
	public TableLayout () {
	}

	public override void draw (WidgetAllocation allocation, Context cr) {
		this.allocation = allocation;
		
		layout ();
		
		// background
		cr.save ();
		cr.rectangle (0, 0, allocation.width, allocation.height);
		cr.set_line_width (0);
		
		Theme.color (cr, "Default Background");

		cr.fill ();
		cr.stroke ();
		cr.restore ();

		foreach (Widget w in widgets) {
			if (w.is_on_screen ()) {			
				if (w is Text) {
					cr.save ();
					Theme.color (cr, "Foreground 1");
					w.draw (cr);
					cr.restore ();
				} else {
					w.draw (cr);
				}
			}
		}
	}	

	void layout () {
		double y = -scroll;
		
		foreach (Widget w in widgets) {
			w.widget_x = 17;
			w.widget_y = y;
			w.allocation = allocation;
			
			if (w is TextArea) {
				((TextArea) w).layout ();
			}
			
			y += w.get_height () + w.margin_bottom;
		}
		
		content_height = y + scroll;
		update_scrollbar ();
	}

	public void scroll_event (double p) {
		scroll += p;
		layout ();
		GlyphCanvas.redraw ();
	}

	public override void key_press (uint keyval) {
		Widget focus;

		if (keyval == Key.SHIFT_TAB) {
			focus_previous ();
		} else if (keyval == Key.TAB) {
			focus_next ();
		} else if (keyboard_focus != null) {
			focus = (!) keyboard_focus;
			focus.key_press (keyval);
		}
		
		GlyphCanvas.redraw ();
	}
	
	void focus_previous () {
		focus_index--;
		
		if (focus_index < 0) {
			focus_index = 0;
		}
		
		set_focus (focus_ring.get (focus_index));
	}
	
	void focus_next () {
		focus_index++;
		
		if (focus_index >= focus_ring.size) {
			focus_index = focus_ring.size - 1;
		}
		
		set_focus (focus_ring.get (focus_index));
	}
	
	public override void button_press (uint button, double x, double y) {
		Widget t;
		Widget old;
		CheckBox c;
				
		if (ignore_input) {
			printd (@"Ignoring extra clicks.");
			return;
		}
		
		foreach (Widget w in widgets) {
			if (w.is_over (x, y)) {
				if (w is TextArea) {
					t = (TextArea) w;
					if (keyboard_focus != null && (!) keyboard_focus != t) {
						old = (!) keyboard_focus;
						old.focus (false);
					}
					
					set_focus (t);
					t.button_press (button, x, y);
				} else if (w is CheckBox) {
					c = (CheckBox) w;
					c.set_checked (!c.checked);
				} else {
					w.button_press (button, x, y);
				}
			}
		}

		GlyphCanvas.redraw ();
	}
	
	public void set_focus (Widget w) {
		Widget old;
		
		if (keyboard_focus != null && (!) keyboard_focus != w) {
			old = (!) keyboard_focus;
			old.focus (false);
		}
		
		keyboard_focus = w;
		w.focus (true);
		
		focus_index = focus_ring.index_of (w);

		if (!(0 <= focus_index < focus_ring.size)) {
			focus_index = 0;
		}

		update_scrollbar ();
		GlyphCanvas.redraw ();
	}
	
	public override void button_release (int button, double x, double y) {
		Widget t;

		if (ignore_input) {
			printd (@"Ignoring extra clicks.");
			return;
		}
		
		if (keyboard_focus != null) {
			t = (!) keyboard_focus;
			set_focus (t);
			t.button_release (button, x, y);
		}

		foreach (Widget w in widgets) {
			if (w.is_over (x, y)) {
				w.button_release (button, x, y);
			}
		}
					
		GlyphCanvas.redraw ();
	}

	public override void motion_notify (double x, double y) {
		Widget t;
		
		if (keyboard_focus != null) {
			t = (!) keyboard_focus;
			if (t.motion (x, y)) {
				GlyphCanvas.redraw ();
			}
		}
	}

	public override string get_label () {
		return t_("Name and Description");
	}

	public override string get_name () {
		return "Description";
	}

	public override bool has_scrollbar () {
		return true;
	}
	
	public override void scroll_wheel (double x, double y, double pixeldelta, double dy) {
		scroll -= dy;
		
		if (scroll + allocation.height >=  content_height) {
			scroll = content_height - allocation.height;
			
			if (scroll < 0) {
				scroll = 0;
			}
		}
		
		if (scroll < 0) {
			scroll = 0;
		}		

		update_scrollbar ();
		GlyphCanvas.redraw ();
	}
	
	public override void selected_canvas () {
		ignore_input = true;
		
		TimeoutSource input_delay = new TimeoutSource (250);
		input_delay.set_callback(() => {
			ignore_input = false;
			return false;
		});
		input_delay.attach (null);
		
		KeyBindings.set_require_modifier (true);
		update_scrollbar ();
		GlyphCanvas.redraw ();
	}
	
	public override void update_scrollbar () {
		double h = content_height - allocation.height;
		MainWindow.set_scrollbar_size (allocation.height / content_height);
		MainWindow.set_scrollbar_position (scroll /  h);
	}

	public override void scroll_to (double percent) {
		double h = content_height - allocation.height;
		scroll = percent * h;
		GlyphCanvas.redraw ();
	}

	public override bool needs_modifier () {
		return true;
	}
	
	public override void double_click (uint button, double x, double y) {
		Widget t;
				
		if (ignore_input) {
			printd (@"Ignoring extra clicks.");
			return;
		}
		
		if (keyboard_focus != null) {
			t = (!) keyboard_focus;
			set_focus (t);
			t.double_click (button, x, y);
		}

		foreach (Widget w in widgets) {
			if (w.is_over (x, y)) {
				w.double_click (button, x, y);
			}
		}
					
		GlyphCanvas.redraw ();
	}
}

}

Contact

Email: johan.mattsson.m@gmail.com
Mastodon: typo.social/@birdfont
GitHub: johanmattssonm
LinkedIn: johan-mattsson-2ab324112