Clone in a terminal:
DropMenu.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 enum MenuDirection {
	DROP_DOWN,
	POP_UP;
}

public class DropMenu : GLib.Object {

	public delegate void Selected (MenuAction self);
	public signal void selected (DropMenu self);
	
	double x = -1;
	double y = -1;
	bool menu_visible = false;

	List <MenuAction> actions = new List <MenuAction> ();

	const int item_height = 25;
	
	MenuDirection direction = MenuDirection.DROP_DOWN;
	
	ImageSurface? icon = null;
	
	public DropMenu (string icon_file = "") {
		if (icon_file != "") {
			icon = Icons.get_icon (@"$icon_file.png");
		}
	}
	
	public void deselect_all () {
		foreach (MenuAction m in actions) {
			m.set_selected (false);
		}
	}
	
	public void set_direction (MenuDirection d) {
		direction = d;
	}
	
	public void close () {
		menu_visible = false;
	}
	
	public MenuAction add_item (string label) {
		MenuAction m = new MenuAction (label);
		add_menu_item (m);
		return m;
	}
	
	public void add_menu_item (MenuAction m) {
		m.parent = this;
		actions.append (m);
	}
		
	public bool is_over_icon (double px, double py) {
		if (x == -1 || y == -1) {
			return false;
		}
		
		return x - 5 < px < x + 12 + 5 && y - 5 < py < y + 12 + 5;
	}

	public bool menu_item_action (double px, double py) {
		MenuAction? action;

		if (menu_visible) {
			action = get_menu_action_at (px, py);
			
			if (action != null) {
				((!) action).action ((!)action);
				selected (this);
				menu_visible = false;
				return true;
			}
		}
		
		return false;
	}
	
	public bool menu_icon_action (double px, double py) {		
		menu_visible = is_over_icon (px, py);
		return menu_visible;
	}
	
	MenuAction? get_menu_action_at (double px, double py) {
		double n = 0;
		double ix, iy;
		
		foreach (MenuAction item in actions) {
			ix = x - 100 + 12;
			
			if (direction == MenuDirection.DROP_DOWN) {
				iy = y + 12 + n * item_height;
			} else {
				iy = y - 24 - n * item_height;
			}
	
			if (ix <= px <= ix + 100 && iy <= py <= iy + item_height) {
				return item;
			}
			
			n++;			
		}

		return null;
	}
	
	public void set_position (double px, double py) {
		x = px;
		y = py;
	}
	
	public void draw_menu (Context cr) {
		double ix, iy;
		int n;
		
		if (likely (!menu_visible)) {
			return;
		}
		
		cr.save ();
		cr.set_source_rgba (122/255.0, 150/255.0, 169/255.0, 1);
		cr.set_line_join (LineJoin.ROUND);
		cr.set_line_width (12);
			
		if (direction == MenuDirection.DROP_DOWN)
			cr.rectangle (x - 100 + 18, y + 18, 88, actions.length () * item_height - 12);
		else
			cr.rectangle (x - 100 + 18, y + 6 - actions.length () * item_height, 88, actions.length () * item_height - 12);
			
		cr.fill_preserve ();
		cr.stroke ();
		cr.restore ();
		
		cr.save ();
		
		n = 0;
		foreach (MenuAction item in actions) {
			
			if (direction == MenuDirection.DROP_DOWN)
				iy = y + 33 + n * item_height - 5;
			else 
				iy = y - 8 - n * item_height;
			
			ix = x - 100 + 20;
			
			item.draw (ix, iy, cr);
			n++;
		}
		
		cr.restore ();
	}
	
	public void draw_icon (Context cr) {
		double alpha = 1;
		ImageSurface i = (!) icon;
		
		if (!menu_visible) {
			alpha = 0;
		}
		
		cr.save ();
		
		cr.set_source_rgba (122/255.0, 150/255.0, 169/255.0, alpha);
		
		cr.rectangle (x, y, 12, 12);
		cr.fill_preserve ();
		cr.stroke ();
		
		if (likely (icon != null && i.status () == Cairo.Status.SUCCESS)) {
			cr.set_source_surface (i, x, y + (12 - i.get_height ()) / 2.0);
			cr.paint ();
		} else {
			warning ("Failed to load icon.");
		}
		
		cr.restore ();
	}
}

}

Contact

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