Clone in a terminal:
Component.vala
/*
	Copyright (C) 2016 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 B;
using SvgBird;
using Gee;
using Cairo;

namespace Bird {

public abstract class Component : GLib.Object {
	public double width { get; protected set; }
	public double height { get; protected set; }
	
	public double padded_width {
		get {
			return width + get_padding_left () + get_padding_right ();
		}
	}
	
	public double padded_height { 
		get {
			return height + get_padding_top () + get_padding_bottom ();
		}
	}

	public double padded_x {
		get {
			return x + get_padding_left ();
		}
	}
	
	public double padded_y { 
		get {
			return y + get_padding_top ();
		}
	}
	
	/** Vertical placement for this component relative to the parent container. */
	public double x { get; protected set; }
	
	/** Horizontal placement for this component relative to the parent container. */
	public double y { get; protected set; }

	/** The parts this component is made of. */
	protected ArrayList<Component> components = new ArrayList<Component> ();

	XmlElement component_tag;
	
	/** Style sheet and other SVG definitions. */
	Defs defs = new Defs ();
	protected SvgStyle style = new SvgStyle ();

	public string? css_class = null;
	public string? id = null;

	Overflow overflow = Overflow.VISIBLE;

	public Component.empty (double width, double height) {
		this.width = width;
		this.height = height;
	}

	public Component.load (string file_name, double width, double height) {
		this.width = width;
		this.height = height;
		load_file (file_name, null);
		layout (width, height);
	}

	/** Parse component ui file but don't do layout. */
	internal Component.load_component (string file_name, Component parent) {
		load_file (file_name, parent);
	}
	
	public Component (XmlElement component_tag, Defs defs) {
		this.defs = defs;
		this.component_tag = component_tag;
		set_identification ();
		parse (component_tag);
	}

	public Component.embedded (XmlElement component_tag, Defs defs) {
		this.defs = defs;
		this.component_tag = component_tag;
		set_identification ();
	}

	void set_identification () {
		foreach (Attribute attribute in component_tag.get_attributes ()) {
			string attribute_name = attribute.get_name ();
			
			if (attribute_name == "id") {
				id = attribute.get_content ();
			} else if (attribute_name == "class") {
				css_class = attribute.get_content ();
			}
		}
	}

	private void inherit_styles_sheet (Defs defs) {
		this.defs = defs;
	}

	protected void parse_style (XmlElement style_tag) {
		defs = defs.shallow_copy ();
		StyleSheet style_sheet = StyleSheet.parse (defs, style_tag);
		defs.style_sheet.merge (style_sheet);
	}
	
	protected void parse_svg (XmlElement svg_tag) {
		foreach (Attribute attribute in svg_tag.get_attributes ()) {
			string attribute_name = attribute.get_name ();
			
			if (attribute_name == "file") {
				string file_name = attribute.get_content ();
				SvgComponent svg = new SvgComponent (svg_tag, defs, file_name);
				add_component (svg);	
			}
		}
	}

	protected void add_component (Component component) {
		components.add (component);
		component.set_style_properties (this);
	}

	protected void set_style_properties (Component parent) {
		inherit_styles_sheet (parent.defs);
		style = SvgStyle.parse (parent.defs, parent.style, component_tag);		
		set_overflow_property_from_css ();
		set_identification ();
	}
	
	protected void set_overflow_property_from_css () {
		string? css_overflow = style.get_css_property ("overflow");
		
		if (css_overflow != null) {
			string overflow_property = (!) css_overflow;
			
			if (overflow_property == "hidden") {
				overflow = Overflow.HIDDEN;
			} else if (overflow_property == "visible") {
				overflow = Overflow.VISIBLE;
			}
		}
	}

	protected void parse_layout (XmlElement layout_tag) {
		string type = "";
		
		foreach (Attribute attribute in layout_tag.get_attributes ()) {
			string attribute_name = attribute.get_name ();
			
			if (attribute_name == "type") {
				type = attribute.get_content ();				
			}
		}
		
		if (type == "hbox") {
			HBox hbox = new HBox (layout_tag, defs);
			add_component (hbox);
		} else if (type == "vbox") {
			VBox vbox = new VBox (layout_tag, defs);
			add_component (vbox);
		} else {
			warning (type + " layout of type is supported in this verison.");
		}
	}
	
	protected void parse_component (XmlElement component_tag) {
		foreach (Attribute attribute in component_tag.get_attributes ()) {
			string attribute_name = attribute.get_name ();
			
			if (attribute_name == "file") {
				string file_name = attribute.get_content ();
				UI ui = new UI.load_component (file_name, this);
				
				foreach (Component component in ui.components) {
					add_component (component);
					
					SvgStyle copied_style;
					copied_style = SvgStyle.parse (defs, style, component_tag);		
					copied_style.apply (component.style);
					component.style = copied_style;
				}
			}
		}
	}
	
	protected void parse (XmlElement component_tag) {
		foreach (XmlElement tag in component_tag) {
			string tag_name = tag.get_name ();
			
			if (tag_name == "ui") {
				parse (tag);
			} else if (tag_name == "layout") {
				parse_layout (tag);
			} else if (tag_name == "component") {
				parse_component (tag);
			} else if (tag_name == "svg") {
				parse_svg (tag);
			} else if (tag_name == "style") {
				parse_style (tag);
			} else {
				unused_tag (tag_name);
			}
		}
	}

	internal void unused_attribute (string attribute) {
		warning ("The attribute " + attribute + " is not known in this version.");
	}
	
	internal void unused_tag (string tag_name) {
		warning ("The tag " + tag_name + " is not known in this version.");
	}

	public void load_file (string file_name, Component? parent) {
		if (file_name.has_suffix (".ui")) {
			load_layout (file_name, parent);
		} else if (file_name.has_suffix (".svg")) {
			SvgComponent svg = new SvgComponent.for_file (file_name);
			add_component (svg);	
		} else {
			warning (file_name + " is not a ui file or svg file.");
		}
	}

	public void load_layout (string file_name, Component? parent) {
		string? path = find_file (file_name);
		
		if (path == null) {
			warning (file_name + " not found.");
			return;
		}
		
		string xml_data;
		File layout_file = File.new_for_path ((!) path); 
		
		try {
			FileUtils.get_contents((!) layout_file.get_path (), out xml_data);
		} catch (GLib.Error error) {
			warning (error.message);
			return;
		}

		XmlTree xml_parser = new XmlTree (xml_data);		
		XmlElement tag = xml_parser.get_root ();
		
		if (parent == null) {
			component_tag = tag;
			defs = new Defs ();
		} else {
			Component p = (!) parent;
			component_tag = p.component_tag;
			defs = p.defs;
		}
		
		parse (tag);
	}
	
	public static string find_file (string file_name) {
		File file = File.new_for_path ("birdui/" + file_name);
		
		if (file.query_exists ()) {
			return (!) file.get_path ();
		}
		
		return file_name;
	}
	
	public virtual void get_min_size (out double min_width, out double min_height) {
		min_width = width;
		min_height = height;
		
		if (unlikely (components.size > 1)) {
			warning ("A component has several parts but no layout has been set.");
		}
		
		foreach (Component component in components) {
			component.get_min_size (out min_width, out min_height);
		}

		min_width += get_padding_left ();
		min_width += get_padding_right ();
		min_height += get_padding_top ();
		min_height += get_padding_bottom ();
	}
	
	public virtual void layout (double parent_width, double parent_height) {
		warning ("A component without a layout was added to the view.");

		double w, h;
		get_min_size (out w, out h);

		width = w;
		height = h;
		
		foreach (Component component in components) {
			component.layout (parent_width, parent_height);
		}
	}
	
	public void clip (Context cairo) {
		if (overflow == Overflow.HIDDEN) {
			cairo.rectangle (0, 0, width, height);
			cairo.clip ();
		}
	}
	
	public abstract void draw (Context cairo);
	
	public virtual void motion_notify_event (double x, double y) {	
	}
	
	public virtual void button_press_event (uint button, double x, double y) {	
	}
	
	public virtual string to_string () {
		return @"$(component_tag.get_name ())";
	}
	
	public void print_tree () {
		print_tree_level (0);
	}

	protected void print_tree_level (int indent) {
		for (int i = 0; i < indent; i++) {
			print ("\t");
		}
		
		print (@"$(to_string ()) width: $padded_width, height: $padded_height x $x y $y $(style)\n");
		
		foreach (Component component in components) {
			component.print_tree_level (indent + 1);
		}
	}
	
	protected double get_padding_bottom () {
		return SvgFile.parse_number (style.get_css_property ("padding-bottom"));
	}

	protected double get_padding_top () {
		return SvgFile.parse_number (style.get_css_property ("padding-top"));
	}

	protected double get_padding_left () {
		return SvgFile.parse_number (style.get_css_property ("padding-left"));
	}

	protected double get_padding_right () {
		return SvgFile.parse_number (style.get_css_property ("padding-right"));
	}

	protected void limit_size (ref double width, ref double height) {
		string? min_width = style.get_css_property ("min-width");
		if (min_width != null) {
			double w = SvgFile.parse_number (min_width);
			if (width < w) {
				width = w;
			}
		}

		string? min_height = style.get_css_property ("min-height");
		if (min_height != null) {
			double h = SvgFile.parse_number (min_height);
			
			if (height < h) {
				height = h;
			}
		}
		
		string? max_width = style.get_css_property ("max-width");
		if (max_width != null) {
			double w = SvgFile.parse_number (max_width);
			
			if (width > w) {
				width = w;
			}
		}

		string? max_height = style.get_css_property ("max-height");
		if (max_height != null) {
			double h = SvgFile.parse_number (max_height);
			
			if (height > h) {
				height = h;
			}
		}
		
		if (width < 0) {
			width = 0;
		}

		if (height < 0) {
			height = 0;
		}
	}
}

}

Contact

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