Clone in a terminal:
TabBar.vala
/*
    Copyright (C) 2012 Johan Mattsson

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

    This program 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 General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

using Cairo;

namespace Supplement {

public class TabBar : GLib.Object {
	
	int width = 0;
	int height = 0;
	
	public List<Tab> tabs = new List<Tab> ();

	static const int NO_TAB = -1;
	static const int NEXT_TAB = -2;
	static const int PREVIOUS_TAB = -3;

	int first_tab = 0;
	int selected = 0;
	int over = NO_TAB;
	int over_close = NO_TAB;
	
	public signal void signal_tab_selected (Tab selected_tab);

	Tab? previous_tab = null;
	Tab? current_tab = null;

	ImageSurface? tab1_left = null;
	ImageSurface? tab1_right = null;

	ImageSurface? tab2_left = null;
	ImageSurface? tab2_right = null;

	ImageSurface? tab3_left = null;
	ImageSurface? tab3_right = null;
	
	ImageSurface? bar_background = null;
	ImageSurface next_tab;
	ImageSurface to_previous_tab;
			
	public TabBar () {
		tab1_left = Icons.get_icon ("tab1_left.png");
		tab1_right = Icons.get_icon ("tab1_right.png");

		tab2_left = Icons.get_icon ("tab2_left.png");
		tab2_right = Icons.get_icon ("tab2_right.png");

		tab3_left = Icons.get_icon ("tab3_left.png");
		tab3_right = Icons.get_icon ("tab3_right.png");
		
		bar_background = Icons.get_icon ("tabbar_background.png");
		
		next_tab = (!) Icons.get_icon ("next_tab.png");
		to_previous_tab = (!) Icons.get_icon ("previous_tab.png");
		
		return_if_fail (!is_null (next_tab));
	}
	
	public void motion (double x, double y) {
		TooltipArea status = MainWindow.get_tool_tip ();
		is_over_close (x, y, out over, out over_close);
	}
	
	private bool is_over_next (double x) {
		return x > width - 19 && has_scroll ();
	}

	private bool is_over_previous (double x) {
		return x < 19  && has_scroll ();
	}

	private void is_over_close (double x, double y, out int over, out int over_close) {
		int i = 0;
		double offset = 19;
		
		if (x < 19 && has_scroll ()) {
			over_close = NO_TAB;
			over = PREVIOUS_TAB;
			return;
		}
		
		foreach (Tab t in tabs) {
			
			if (i < first_tab) {
				i++;
				continue;
			}
			
			if (offset + t.get_width () + 3 > width && has_scroll ()) {
				over_close = NO_TAB;
				over = NEXT_TAB;
				return;
			}
			
			if (offset < x < offset + t.get_width ()) {
				over = i;
				
				if (8 < y < 20 && x > offset + t.get_width () - 16) {
					over_close =  i;
				} else {
					over_close =  NO_TAB;
				}
				
				return;
			}
			
			offset += t.get_width () + 3;
			i++;
		}

		over_close = NO_TAB;		
		over = NO_TAB;
	}	
	
	
	/** Select tab for a glyph by charcode or name.
	 * @return true if the tab was found
	 */
	public bool select_char (string s) {
		int i = 0;
		foreach (Tab t in tabs) {
			if (t.get_label () == s) {
				select_tab (i);
				return true;
			}
			i++;
		}
		
		return false;
	}

	public bool select_tab_name (string s) {
		return select_char (s);
	}

	public void select_overview () {
		select_tab_name ("Overview");
	}

	private void select_previous_tab () {
		Tab t;
		bool open;
		
		if (previous_tab == null) {
			return;
		}
		
		t = (!) previous_tab;
		open = selected_open_tab (t);
		
		if (!open) {
			select_tab ((int) tabs.length () - 1);
		}
	}
		
	public void close_display (FontDisplay f) {
		int i = -1;
		foreach (var t in tabs) {
			++i;
			
			if (t.get_display () == f) {
				close_tab (i) ;
				return;
			}
		}
		
		return_if_fail (i != -1);
	} 

	public bool close_tab (int index, bool background_tab = false) {	
		unowned List<Tab?>? lt;
		Tab t;
		
		if (!(0 <= index < tabs.length ())) {
			return false;
		}
		
		lt = tabs.nth(index);
		
		if (lt == null || ((!) lt).data == null) {
			return false;
		}

		if (first_tab > 0) {
			first_tab--;
		}

		t = (!) ((!) lt).data;

		if (t.has_close_button ()) {
			tabs.delete_link (tabs.nth(index));
			
			if (!background_tab) {
				select_previous_tab ();
			}
			
			return true;
		}
		
		select_tab (index);
		return false;
	}
	
	public bool close_by_name (string name, bool background_tab = false) {
		int i = 0;
		
		foreach (var t in tabs) {
			if (t.get_label () == name) {
				return close_tab (i, background_tab);
			}
			
			i++;
		}
		
		return false;
	}
	
	public void close_background_tab_by_name (string name) {
		close_by_name (name, true);
	}
	
	/** Select a tab and return true if it is open. */
	public bool selected_open_tab (Tab t) {
		int i = 0;
		foreach (var n in tabs) {
			if (n == t) {
				select_tab (i);
				return true;
			}
			
			i++;
		}
		
		return false;
	}

	public Tab? get_tab (string name) {
		foreach (var n in tabs) {
			if (n.get_label () == name) {
				return n;
			}
		}
		
		return null;
	}

	public bool selected_open_tab_by_name (string t) {
		int i = 0;
		foreach (var n in tabs) {
			if (n.get_label () == t) {
				select_tab (i);
				return true;
			}
			
			i++;
		}
		
		return false;
	}
		
	public Tab get_selected_tab () {
		return tabs.nth (get_selected ()).data;
	}
	
	public uint get_length () {
		return tabs.length ();
	}

	public int get_selected () {
		return selected;
	}
	
	public void select_tab (int index) {
		Tab t;

		if (index == NEXT_TAB) {
			selected++;
			
			if (selected >=  tabs.length ()) {
				selected = (int) tabs.length () - 1;
			}
			
			scroll_to_tab (selected);
			return;
		}
		
		if (index == PREVIOUS_TAB) {

			if (selected > 0) {
				selected--;
			}
			
			scroll_to_tab (selected);
			return;
		}
		
		if (!(0 <= index < tabs.length ())) {
			return;
		}

		selected = index;
		
		unowned List<Tab?>? lt = tabs.nth(index);
		
		return_if_fail(lt != null);
		t = (!) ((!) lt).data;
		
		previous_tab = current_tab;
		current_tab = t;

		scroll_to_tab (selected);
	}
	
	private bool has_scroll () {
		int i = 0;
		double offset = 19;
		
		if (first_tab > 0) {
			return true;
		}
		
		foreach (Tab t in tabs) {	
			if (i < first_tab) {
				i++;
				continue;
			}
			
			if (offset + t.get_width () + 3 > width - 19) {
				return true;
			}

			offset += t.get_width () + 3;
			i++;
		}
		
		return false;		
	}
	
	private void signal_selected (int index) {
		unowned List<Tab?>? lt;
		Tab t;
		
		lt = tabs.nth(index);
		return_if_fail(lt != null);		
		t = (!) ((!) lt).data;
		
		signal_tab_selected (t);		
	}
	
	private void scroll_to_tab (int index) {
		double offset = 19;
		int i = 0;
		
		if (index < first_tab) {
			first_tab = index;
			signal_selected (index);
			return;
		}
		
		foreach (Tab t in tabs) {
			
			if (i < first_tab) {
				i++;
				continue;
			}
			
			// out of view
			if (offset + t.get_width () + 3 > width - 19) {
				first_tab++;
				scroll_to_tab (index);
				return;
			}

			// in view
			if (i == index) {
				signal_selected (index);
				return;
			}

			offset += t.get_width () + 3;
			i++;
		}
		
		warning ("");
	}
	
	public void select_tab_click (double x, double y, int width, int height) {
		int over, close;
		
		this.width = width;
		this.height = height;
		
		is_over_close (x, y, out over, out close);

		if (over_close >= 0 && over == selected) {
			close_tab (over_close);
		} else {
			select_tab (over);
		}
	}
	
	public void add_tab (FontDisplay display_item, double tab_width = -1, bool always_open = false) {
		int s = (tabs.length () == 0) ? 0 : selected + 1;
		
		if (tab_width < 0) {
			//cr.text_extents (display_item.get_name (), out te); // this is not a good estimation, pango might solve it
			//tab_width = te.width + 30;
			
			tab_width = 9 * display_item.get_name ().char_count ();
			tab_width += 30;
		}
				
		tabs.insert (new Tab (display_item, tab_width, always_open), s);
		select_tab (s);
	}
	
	/** Returns true if the new item was added to the bar. */
	public bool add_unique_tab (FontDisplay display_item, double tab_width = -1, bool always_open = false) {
		bool i = select_tab_name (display_item.get_name ());

		if (!i) {
			add_tab (display_item, tab_width, always_open);
			return true;
		}
		
		return false;
	}
	
	public void draw (Context cr, int width, int height) {
		this.width = width;
		this.height = height;
		
		cr.save ();
		cr.rectangle (0, 0, width, height);
		cr.set_line_width (0);
		cr.set_source_rgba (183/255.0, 200/255.0, 223/255.0, 1);
		cr.fill_preserve ();
		cr.stroke ();
		cr.restore ();

		for (int j = 0; j < width; j++) {
			cr.set_source_surface ((!) bar_background, j, 0);
			cr.paint ();
		}

		if (has_scroll ()) {
			// left arrow
			cr.set_source_surface (to_previous_tab, 3, (height - to_previous_tab.get_height ()) / 2.0);
			cr.paint ();

			// right arrow
			cr.set_source_surface (next_tab, width - 19, (height - next_tab.get_height ()) / 2.0);
			cr.paint ();
		}
		
		draw_tabs (cr);
		
	}
	
	// this can be removed when tabs are not referenced by name
	string translate (string s) {
		switch (s) {
			case "Overview":
				return _("Overview");
				break;
			case "Kerning":
				return _("Kerning");
				break;
			case "Menu":
				return _("Menu");
				break;
			default:
				return s;
				break;
		}
	}
	
	private void draw_tabs (Context cr) {
		double close_opacity;
		double offset = 19;
		int i = 0;
		
		foreach (Tab t in tabs) {
			
			if (i < first_tab) {
				i++;
				continue;
			}

			cr.save ();
			cr.translate (offset, 0);
						
			if (offset + t.get_width () + next_tab.get_width () + 3 > width) {
				break;
			}
		
			// background
			if (i == selected) {
				for (int j = 0; j < t.get_width (); j++) {
					cr.set_source_surface ((!) tab3_right, j, 2);
					cr.paint ();
				}							
			} else if (i == over) {
				for (int j = 0; j < t.get_width (); j++) {
					cr.set_source_surface ((!) tab2_right, j, 2);
					cr.paint ();
				}				
			} else {
				for (int j = 0; j < t.get_width (); j++) {
					cr.set_source_surface ((!) tab1_right, j, 2);
					cr.paint ();
				}
			}
			
			// close
			if (t.has_close_button ()) {
				cr.set_line_width (1);
				
				close_opacity = (over_close == i) ? 1 : 0.2; 
				cr.set_source_rgba (0, 0, 0, close_opacity);
				
				cr.move_to (t.get_width () - 5, 11);
				cr.line_to (t.get_width () - 10, 16);

				cr.move_to (t.get_width () - 10, 11);
				cr.line_to (t.get_width () - 5, 16);
				
				cr.stroke ();	
			}

			cr.set_source_rgba (0, 0, 0, 1);
			cr.set_font_size (14);
			cr.move_to (8, 18);
			cr.show_text (translate (t.get_label ()));
			cr.stroke ();
			
			// edges
			if (tab1_left != null  && tab1_right != null && tab2_left != null  && tab2_right != null && tab3_left != null  && tab3_right != null) {
				if (i == selected) {
					cr.set_source_surface ((!) tab3_left, 0, 2);
					cr.paint ();
					
					cr.set_source_surface ((!) tab3_right, t.get_width () - 2, 2);
					cr.paint ();
				} else if (i == over) {
					cr.set_source_surface ((!) tab2_left, 0, 2);
					cr.paint ();

					cr.set_source_surface ((!) tab2_right, t.get_width () - 2, 2);
					cr.paint ();
				} else {
					cr.set_source_surface ((!) tab1_left, 0, 2);
					cr.paint ();

					cr.set_source_surface ((!) tab1_right, t.get_width () - 2, 2);
					cr.paint ();
				}
			}

			cr.restore ();
			
			offset += t.get_width () + 3;
			i++;
		}
	}
}

}

Contact

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