Clone in a terminal:
Context.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 {

/** Kerning context. */
public class ContextDisplay : FontDisplay {

	List <Word> row;
	int active_handle = -1;
	int selected_handle = -1;
	
	double begin_handle_x = 0;
	double begin_handle_y = 0;
	
	double last_handle_x = 0;
	
	public ContextDisplay () {
		Word w = new Word ();
		row = new List <Word> ();
		row.append (w);
	}

	public override string get_name () {
		return "Kerning";
	}
	
	public override void draw (Allocation allocation, Context cr) {
		Glyph? g;
		Glyph glyph;
		double x, y, w, kern, alpha;
		double x2;
		int i, wi;
		string prev;
		
		i = 0;
		
		// bg color
		cr.save ();
		cr.set_source_rgba (1, 1, 1, 1);
		cr.rectangle (0, 0, allocation.width, allocation.height);
		cr.fill ();
		cr.restore ();
	
		alpha = 1;
		y = 100;
		x = 20;
		w = 0;
		prev = "";
		
		foreach (Word word in row) {
			wi = 0;
			
			foreach (string s in word.glyph) {				
				g = (!) Supplement.get_current_font ().get_glyph (s);	
				
				if (s == "") {
					continue;
				}
				
				kern = (prev != "") ? get_kerning (prev, s) : 0;
				
				// draw glyph				
				if (g == null) {
					w = 50;
					alpha = 1;
				} else {
					alpha = 0;
					glyph = (!) g;
					Svg.draw_svg_path (cr, glyph.get_svg_data (), x + kern, y);
					w = glyph.get_width ();
				}

				// handle
				if (active_handle == i) {
					//x2 = x + kern;
					x2 = x + kern / 2.0;
					
					cr.save ();
					cr.set_source_rgba (153/255.0, 153/255.0, 173/255.0, 1);
					cr.move_to (x2 - 5, y + 20);
					cr.line_to (x2 + 0, y + 20 - 5);
					cr.line_to (x2 + 5, y + 20);
					cr.fill ();
					
					cr.set_font_size (10);
					cr.show_text (s);
					cr.restore ();
				}	
				
				x += w + kern;
	
				// caption
				if (g == null || ((!)g).is_empty ()) {
					cr.save ();
					cr.set_source_rgba (153/255.0, 153/255.0, 153/255.0, alpha);
					cr.move_to (x - w / 2.0 - 5, y + 20);
					cr.set_font_size (10);
					cr.show_text (s);
					cr.restore ();
				}
							
				prev = s;
				
				wi++;
				i++;
			}
			
			if (y > allocation.height && i > 10) {
				row.remove (word);
			}
						
			y += MainWindow.get_current_glyph ().get_height () + 20;
			x = 20;			
		}
	}
	
	private void set_kerning (int handle, double val) {
		string a, b;
		Font font;
		int wi = 0;
		double kern;
		
		font = Supplement.get_current_font ();

		font.touch ();

		a = "";
		b = "";

		foreach (Word word in row) {
			foreach (string s in word.glyph) {
				
				if (s == "") {
					continue;
				}
				
				b = s;
				
				if (handle == wi) {
					kern = font.get_kerning (a, b) + val;
					font.set_kerning (a, b, kern);
				}
				
				wi++;
				
				a = b;
			}
		}
		
		
	}
	
	private double get_kerning (string a, string b) {
		Font font = Supplement.get_current_font ();
		return font.get_kerning (a, b);
	}
	
	public override void selected_canvas () {
		Glyph g;
		Word w;
		unowned List<Word>? r;
		unowned List<string>? rw;
		StringBuilder s = new StringBuilder ();
		bool append_char = false;
		
		KeyBindings.singleton.set_require_modifier (true);
		
		g = MainWindow.get_current_glyph ();
		s.append_unichar (g.get_unichar ());

		r = row;
		return_if_fail (r != null);

		if (row.length () == 0) {
			append_char = true;
		}
		
		if (row.length () > 0) {
			rw = row.first ().data.glyph;
			return_if_fail (rw != null);
			append_char = (row.first ().data.glyph.data.index_of (s.str) == -1);
		}
		
		if (append_char) {
			w = new Word ();
			row.append (w);
			w.glyph.prepend (s.str);
		}					
	}
	
	public override void key_press (uint keyval) {
		unichar c = (unichar) keyval;
		Font f = Supplement.get_current_font ();
		string name;
		
		if (KeyBindings.singleton.modifier == NONE || KeyBindings.singleton.modifier == SHIFT) {
					
			if (keyval == Key.BACK_SPACE && row.length () > 0) {	
				row.first ().data.glyph.remove_link (row.first ().data.glyph.last ());
			}
			
			if (row.length () == 0 || c == Key.ENTER) {
				row.prepend (new Word ());
			}
			
			if (!is_modifier_key (c) && c.validate ()) {
				name = f.get_name_for_character (c);
				row.first ().data.glyph.append (name);
			}
		}
		
		MainWindow.get_glyph_canvas ().redraw ();
	}
	
	public override void motion_notify (double ex, double ey) {
		double k, y;
		
		if (selected_handle == -1) {
			set_active_handle (ex, ey);
		} else {
			y = 1;
			
			if (Math.fabs (ey - begin_handle_y) > 20) {
				y = ((Math.fabs (ey - begin_handle_y) / 100) + 1);
			}
			
			k = (ex - last_handle_x) / y; // y-axis is variable precision
			set_kerning (selected_handle, k);
			MainWindow.get_glyph_canvas ().redraw ();
		}
		
		last_handle_x = ex;
	}
	
	public void set_active_handle (double ex, double ey) {
		double y = 100;
		double x = 20;
		double w = 0;
		double d, kern;
		double min = double.MAX;
		int i = 0;
		int row_index = 0;
		int col_index = 0;
		Glyph? g;
		Glyph glyph = new Glyph.no_lines ("");
		
		string prev = "";
		string gl_name = "";
		
		foreach (Word word in row) {
			col_index = 0;
			foreach (string s in word.glyph) {
				
				// draw glyph
				g = (!) Supplement.get_current_font ().get_glyph (s);	
				kern = (prev != "") ? get_kerning (prev, s) : 0;
				
				if (g == null) {
					w = 50;	
				} else {
					glyph = (!) g;
					w = glyph.get_width ();
					
				}
				
				d = Math.pow (x + kern - ex, 2) + Math.pow (y - ey, 2);
				
				if (d < min) {
					min = d;
					gl_name = glyph.get_name ();
					
					if (active_handle != i - row_index) {
						active_handle = i - row_index;
						MainWindow.get_glyph_canvas ().redraw ();
					}

					if (col_index == word.glyph.length () || col_index == 0) {
						active_handle = -1;
					}
				}
				
				prev = s;
				x += w + kern;
				i++;
				col_index++;
			}
			
			row_index++;
			y += MainWindow.get_current_glyph ().get_height () + 20;
			x = 20;
		}
	}
	
	public override void button_release (int button, double ex, double ey) {
		set_active_handle (ex, ey);
		selected_handle = -1;
	}
	
	public override void button_press (uint button, double ex, double ey) {
		set_active_handle (ex, ey);
		selected_handle = active_handle;
		begin_handle_x = ex;
		begin_handle_y = ey;
		last_handle_x = ex;
	}

}

class Word {
	public List <string> glyph;
	
	public Word () {
		glyph = new List <string> ();
		glyph.append ("");
	}
}

}

Contact

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