Package: src/packages/gui.fdoc
utitle GUI ============ ========================== key file ============ ========================== __init__.flx share/lib/gui/__init__.flx types.flx share/lib/gui/types.flx events.flx share/lib/gui/events.flx ============ ==========================
key | file |
---|---|
init.flx | share/lib/gui/init.flx |
font.flx | share/lib/gui/font.flx |
color.flx | share/lib/gui/color.flx |
surface.flx | share/lib/gui/surface.flx |
drawable.flx | share/lib/gui/drawable.flx |
drawchain.flx | share/lib/gui/drawchain.flx |
key | file |
---|---|
window.flx | share/lib/gui/window.flx |
window_controller_interface.flx | share/lib/gui/window_controller_interface.flx |
window_controller.flx | share/lib/gui/window_controller.flx |
window_manager.flx | share/lib/gui/window_manager.flx |
key | file |
---|---|
button.flx | share/lib/gui/button.flx |
menu.flx | share/lib/gui/menu.flx |
line_buffer_display_controller_interface.flx | share/lib/gui/line_buffer_display_controller_interface.flx |
line_buffer_display_controller.flx | share/lib/gui/line_buffer_display_controller.flx |
line_buffer_interface.flx | share/lib/gui/line_buffer_interface.flx |
line_buffer_object.flx | share/lib/gui/line_buffer_object.flx |
line_editor.flx | share/lib/gui/line_editor.flx |
linegraph.flx | share/lib/gui/linegraph.flx |
Basics¶
The Felix portable GUI is based on the portable SDL library and the Felix bindings thereof. SDL_ttf and SDL_image are required too for font and image handling.
//[__init__.flx]
include "sdl/SDL2";
include "sdl/SDL_ttf";
include "sdl/SDL_image";
The basic SDL initialisation stuff.
//[__init__.flx]
include "gui/init";
include "gui/types";
include "gui/events";
include "gui/font";
Basic GUI abstractions. Felix uses the Model-View-Controller (MVC) design idea.
The <em>model</em> is representation of the abstract state. independent of the visual interface.
The <em>view</em> provides the operations required to render the abstract state onto a graphical surface.
The <em>controller</em> is responsible for event management and in particular state mutations and scheduling display updates corresponding to them in the view, based on input from the client mouse and keyboard.
//[__init__.flx]
include "gui/color";
include "gui/surface";
include "gui/drawable";
include "gui/drawchain";
include "gui/window";
include "gui/window_controller_interface";
include "gui/window_controller";
include "gui/window_manager";
Widgets¶
And of course now for some Widgets! Felix uses a novel mechanism. It is not like other GUIs. Where other systems use subtyping and callbacks, in Felix the controllers for widgets are active threads of control modelled by Felix fthreads (synchronous fibres).
Widgets, and the window manager communicate using schannels (synchronous channels) instead of using callbacks for message passing. This avoids the catastrophic design failing of other GUI systems in which components are reactive slaves. In Felix, the components are autonomous active actors.
In particular interfaces are primarily based on communication protocols which allow plugins to provide services.
Buttons and menus.
//[__init__.flx]
include "gui/button";
include "gui/menu";
//[__init__.flx]
include "gui/line_buffer_interface";
include "gui/line_buffer_object";
include "gui/line_buffer_display_controller_interface";
include "gui/line_buffer_display_controller";
include "gui/line_editor";
Integrated presentation.¶
Merge all the separate classes into a single class to make it a all a bit easier to use.
//[__init__.flx]
class FlxGui
{
inherit FlxGuiInit;
inherit FlxGuiTypes;
inherit FlxGuiEvents;
inherit FlxGuiFont;
inherit FlxGuiColor;
inherit FlxGuiSurface;
inherit FlxGuiDrawable;
inherit FlxGuiDrawChain;
inherit FlxGuiWindow;
inherit FlxGuiWindowController;
inherit FlxGuiWindowControllerInterface;
inherit FlxGuiWindowManager;
inherit FlxGuiButton;
inherit FlxGuiMenu;
// text field editor
inherit FlxGuiLineBufferInterface;
inherit FlxGuiLineBuffer;
inherit FlxGuiLineBufferDisplayControllerInterface;
inherit FlxGuiLineBufferDisplayController;
inherit FlxGuiLineEditor;
} // class FlxGui
Core types¶
Mostly we just lift them from the sdl library which in turn lifts them from the C SDL2 library.
The result is somewhat messy, especially for messages, since SDL’s emulation of unions in C is a long way from the well presented sum type Felix would use.
//[types.flx]
class FlxGuiTypes
{
typedef font_t = TTF_Font;
typedef colour_t = SDL_Color;
typedef color_t = colour_t; // dang yanks ..
// rectangular shape without origin
typedef box_t = (w:int,h:int);
ctor box_t(w:int,h:int)=>(w=w,h=h);
// point
typedef point_t = SDL_Point;
ctor point_t(x:int,y:int)=>SDL_Point(x,y);
// box with origin for label (margin, baseline)
typedef label_box_t = (box:box_t, label_origin: point_t);
ctor label_box_t (box:box_t, label_origin: point_t)=> (box=box,label_origin=label_origin);
// rectangular shape with top left origin
typedef rect_t = SDL_Rect;
ctor rect_t (x:int, y:int, w:int, h:int) => SDL_Rect (x,y,w,h);
ctor rect_t (xy:point_t, dim:box_t) => SDL_Rect (xy.x,xy.y,dim.w,dim.h);
// label rect
typedef label_rect_t = (xy:point_t, lb: label_box_t);
}
//[events.flx]
class FlxGuiEvents
{
typedef event_t = SDL_Event;
fun _match_ctor_QUIT (e:event_t) => e.type == SDL_QUIT.uint32;
fun _match_ctor_WINDOWEVENT (e:event_t) => e.type == SDL_WINDOWEVENT.uint32;
fun _ctor_arg_WINDOWEVENT (e:event_t) => e.window;
fun _match_ctor_KEYDOWN (e:event_t) => e.type == SDL_KEYDOWN.uint32;
fun _ctor_arg_KEYDOWN (e:event_t) => e.key;
fun _match_ctor_KEYUP (e:event_t) => e.type == SDL_KEYUP.uint32;
fun _ctor_arg_KEYUP(e:event_t) => e.key;
fun _match_ctor_MOUSEMOTION (e:event_t) => e.type == SDL_MOUSEMOTION.uint32;
fun _ctor_arg_MOUSEMOTION (e:event_t) => e.motion;
fun _match_ctor_MOUSEBUTTONDOWN (e:event_t) => e.type == SDL_MOUSEBUTTONDOWN.uint32;
fun _ctor_arg_MOUSEBUTTONDOWN (e:event_t) => e.button;
fun _match_ctor_MOUSEBUTTONUP (e:event_t) => e.type == SDL_MOUSEBUTTONUP.uint32;
fun _ctor_arg_MOUSEBUTTONUP (e:event_t) => e.button;
fun _match_ctor_MOUSEWHEEL (e:event_t) => e.type == SDL_MOUSEWHEEL.uint32;
fun _ctor_arg_MOUSEWHEEL (e:event_t) => e.wheel;
fun _match_ctor_TEXTINPUT (e:event_t) => e.type == SDL_TEXTINPUT.uint32;
fun _ctor_arg_TEXTINPUT (e:event_t) => e.text;
fun _match_ctor_TEXTEDITING (e:event_t) => e.type == SDL_TEXTEDITING.uint32;
fun _ctor_arg_TEXTEDITING (e:event_t) => e.edit;
chip event_source
connector events
pin src : %> event_t
{
var clock = Faio::mk_alarm_clock();
var e : SDL_Event;
// dummy first event
e&.type <- SDL_FIRSTEVENT.uint32;
write$ events.src,e;
proc waitevent()
{
nexte:>
var result = SDL_PollEvent$ &e;
if result == 0 do
Faio::sleep(clock,0.1);
goto nexte;
done
}
waitevent;
while e.type.SDL_EventType != SDL_QUIT do
//println$ "SDL EVENT: " + e.type.SDL_EventType.str + " SDL window #" + e.window.windowID.str;
write$ events.src, e;
waitevent;
done
println$ "[event_source] SDL_QUIT seen!";
write$ events.src, e;
return;
} // chip event_source
proc demo_timer (quit:&bool) (var d:double) ()
{
var delta = 0.1;
var clock = Faio::mk_alarm_clock();
again:>
Faio::sleep(clock,delta);
d -= delta;
if *quit goto doquit;
if d > 0.0 goto again;
quit <- true;
var quitmsg : SDL_Event;
quitmsg&.type <- SDL_QUIT.uint32;
println$ "TIMEOUT";
C_hack::ignore(SDL_PushEvent(&quitmsg));
doquit:>
}
}
Subsystem initialisation.¶
Ensures we have visuals, sound, fonts, and images. Display versions of libraries, both the one from the compiled header files and the binary linked in.
//[init.flx]
class FlxGuiInit
{
proc init()
{
if SDL_Init(SDL_INIT_AUDIO \| SDL_INIT_VIDEO) < 0 do
eprintln$ f"Unable to init SDL: %S\n" #SDL_GetError;
System::exit(1);
done
println$ "SDL_init OK";
if TTF_Init() < 0 do
eprintln$ f"Unable to init TTF: %S\n" #TTF_GetError;
System::exit(1);
done
println$ "TTF_init OK";
if IMG_Init(IMG_INIT_PNG) < 0 do
eprintln$ f"Unable to init IMG with PNG: %S\n" #IMG_GetError;
System::exit(1);
done
println$ "IMG_init OK";
}
proc quit() { SDL_Quit(); }
proc versions ()
{
begin
var compiled = #SDL_Compiled_Version;
var linked = #SDL_Linked_Version;
println$ f"We compiled against SDL version %d.%d.%d ..."
(compiled.major.int, compiled.minor.int, compiled.patch.int);
println$ f"But we are linking against SDL version %d.%d.%d."
(linked.major.int, linked.minor.int, linked.patch.int);
end
begin
var compiled = #TTF_Compiled_Version;
var linked = #TTF_Linked_Version;
println$ f"We compiled against TTF version %d.%d.%d ..."
(compiled.major.int, compiled.minor.int, compiled.patch.int);
println$ f"But we are linking against TTF version %d.%d.%d."
(linked.major.int, linked.minor.int, linked.patch.int);
end
begin
var compiled = #IMG_Compiled_Version;
var linked = #IMG_Linked_Version;
println$ f"We compiled against IMG version %d.%d.%d ..."
(compiled.major.int, compiled.minor.int, compiled.patch.int);
println$ f"But we are linking against IMG version %d.%d.%d."
(linked.major.int, linked.minor.int, linked.patch.int);
end
}
}
Font handling.¶
Felix uses SDL_ttf which in turn uses Freetype to render TrueType fonts with some hinting. Unfortunately in my experience the rending is appalling. The glyphs are barely readable. It is not known if this problem is with SDL_ttf or Freetype. The rending is just barely good enough for GUI tools such as game scenario editors, it wouldn’t be useful in game.
Felix provides three fonts borrowed from Apple to save the user from having to set up a font library Felix knows about.
//[font.flx]
class FlxGuiFont
{
private fun / (s:string, t:string) => Filename::join (s,t);
fun dflt_mono_font() => #Config::std_config.FLX_SHARE_DIR/ "src"/"lib"/"fonts"/ "Courier New.ttf";
fun dflt_sans_serif_font() => #Config::std_config.FLX_SHARE_DIR/ "src"/"lib"/"fonts"/ "Arial.ttf";
fun dflt_serif_font() => #Config::std_config.FLX_SHARE_DIR/ "src"/"lib"/"fonts"/ "Times New Roman.ttf";
gen get_font (font_file:string, ptsize:int) = {
var font = TTF_OpenFont (font_file,ptsize);
if not (TTF_ValidFont font) do
eprintln$ f"Unable to open TTF font %S\n" font_file;
System::exit 1;
done
TTF_SetFontKerning (font,0);
var isfixed = TTF_FontFaceIsFixedWidth (font);
println$ "Opened Font " + font_file +
" Facename: " + TTF_FontFaceFamilyName font +
(if isfixed>0 then " MONOSPACED "+ isfixed.str else " VARIABLE WIDTH");
println$ "Metrics: Height "+font.TTF_FontHeight.str +
", Ascent "+ font.TTF_FontAscent.str +
", Descent "+ font.TTF_FontDescent.str +
", Lineskip"+ font.TTF_FontLineSkip.str
;
TTF_SetFontHinting (font,TTF_HINTING_MONO); // guess...
return font;
}
fun get_lineskip (f: font_t) => TTF_FontLineSkip(f) + 1;
fun get_textsize (f: font_t, s:string) =
{
var w: int; var h: int;
C_hack::ignore$ TTF_SizeText (f,s,&w, &h);
return w,h;
}
// x,y is the origin of the first character
// The bounding box is 2 pixels up from the highest char
// 2 pixies down from the lowest char
// 2 pixies to the left of the first character's orgin
// and 2 pix right from the origin of the last char + the notional advance
// this ONLY works right for a monospaced font!
fun bounding_box (f:font_t, x:int, y:int, s:string) : rect_t =
{
var n = s.len.int;
var w =
#{
var minx:int; var maxx:int; var miny:int; var maxy:int; var advance:int;
C_hack::ignore$ TTF_GlyphMetrics(f,"m".char.ord.uint16,&minx, &maxx, &miny, &maxy, &advance);
return advance;
}
;
var a = f.TTF_FontAscent;
var d = f.TTF_FontDescent;
// the 5 = 4 + 1 is due to what looks like a BUG in SDL or TTF:
// for at least one font, height = ascent - descent + 1
// even though lineskip = ascent - descent
return SDL_Rect (x - 2,y - a - 2, w * n +4, a - d + 5);
}
}
Colours.¶
Felix uses RGBA colour scheme: 8 bits of Red, Blue and Green followed by 8 bits of transparency, where 0 means no colour and full transparency, and 255 means maximum colour and opaque rendering.
//[color.flx]
class FlxGuiColor
{
fun RGB (r:int, g:int, b:int) =>
SDL_Color (r.uint8, g.uint8, b.uint8, 255u8)
;
// create some colours and clear the window
var white = RGB (255,255,255);
var black = RGB (0,0,0);
var lightgrey = RGB (180,180,180);
var grey = RGB (100,100,100);
var darkgrey = RGB (60,60,60);
var red = RGB(255,0,0);
var green = RGB (0,255,0);
var blue = RGB (0,0,255);
var purple = RGB (255,0,255);
var yellow = RGB (255,255,0);
var orange = RGB (100,255,100);
}
Surfaces.¶
A surface is something you can do simple drawing on. It is basically a representation of a rectangular grid of pixels. The pixels may support full RGBA or not, depending on construction. For example we might provide a bitmap which supports only black and white using a 1 bit encoding.
Each window will have a native surface onto which we must render the imagery we wish to appear on the client display device. In general, however, we should be using full RGBA arrays for rendering and then blit those arrays onto hardware dependent surfaces.
SDL only provides a very limited set of operations on surfaces! Complex rendering requires OpenGL. But we do not need that in GUI.
//[surface.flx]
class FlxGuiSurface
{
proc clear(surf:&SDL_Surface) (c: colour_t)
{
var pixelformat : &SDL_PixelFormat = surf*.format;
var bgpixels = SDL_MapRGB(pixelformat,c.r,c.g,c.b);
SDL_ClearClipRect (surf);
C_hack::ignore$ SDL_FillSurface (surf, bgpixels);
}
proc fill (surf:&SDL_Surface) (var r:rect_t, c:colour_t)
{
SDL_ClearClipRect (surf);
var pixelformat : &SDL_PixelFormat = surf*.format;
var bgpixels = SDL_MapRGB(pixelformat,c.r,c.g,c.b);
C_hack::ignore$ SDL_FillRect (surf, &r, bgpixels);
SDL_ClearClipRect (surf);
}
noinline proc draw_line (surf:&SDL_Surface) (c:color_t, x0:int, y0:int, x1:int, y1:int)
{
var r: SDL_Renderer = SDL_CreateSoftwareRenderer surf;
C_hack::ignore$ SDL_SetRenderDrawColor (r, c.r, c.g, c.b, c.a);
C_hack::ignore$ SDL_RenderDrawLine (r, x0, y0, x1, y1);
SDL_DestroyRenderer r;
}
proc write(surf:&SDL_Surface) (x:int, y:int, font:font_t, c: colour_t, s:string)
{
var rendered = TTF_RenderText_Solid (font,s,c);
var rect : SDL_Rect;
var minx:int; var maxx:int; var miny:int; var maxy:int; var advance:int;
C_hack::ignore$ TTF_GlyphMetrics(font,"m".char.ord.uint16,&minx, &maxx, &miny, &maxy, &advance);
rect&.x <- x + (min (minx,0));
rect&.y <- y - maxy;
var nullRect = C_hack::null[SDL_Rect];
var result = SDL_BlitSurface (rendered, nullRect, surf, &rect);
if result != 0 do
eprintln$ "Unable to blit text to surface";
System::exit 1;
done
SDL_FreeSurface rendered;
}
proc blit (surf:&SDL_Surface) (dstx:int, dsty:int, src: &SDL_Surface)
{
var nullRect = C_hack::null[SDL_Rect];
var dstRect = rect_t (dstx, dsty,0,0);
var result = SDL_BlitSurface (src, nullRect, surf, &dstRect);
if result != 0 do
eprintln$ "Unable to blit surface to surface at (" + dstx.str + "," + dsty.str + ")";
//System::exit 1;
done
}
interface surface_t {
get_sdl_surface: 1 -> &SDL_Surface;
get_width : 1 -> int;
get_height: 1 -> int;
clear: colour_t -> 0;
fill: rect_t * colour_t -> 0;
draw_line: colour_t * int * int * int * int -> 0; // x0,y0,x1,y1
write: int * int * font_t * colour_t * string -> 0;
}
// Wrapper around SDL surface
// borrows the SDL_Surface!! Does not own or delete
object surface (surf: &SDL_Surface) implements surface_t =
{
method fun get_sdl_surface () => surf;
method fun get_width () => surf*.w;
method fun get_height() => surf*.h;
method proc clear (c:colour_t) => FlxGuiSurface::clear surf c;
method proc fill (r:rect_t, c:colour_t) => FlxGuiSurface::fill surf (r,c);
method proc draw_line (c:colour_t, x0:int, y0:int, x1:int, y1:int) { FlxGuiSurface::draw_line surf (c,x0,y0,x1,y1); }
method proc write (x:int, y:int, font:font_t, c: colour_t, s:string) { FlxGuiSurface::write surf (x,y,font,c,s); }
}
// Takes possession of the surface
// Frees surface when object is freed by GC
header surface_deleter = """
struct _SDL_SurfaceDeleter {
_SDL_Surface *p;
_SDL_SurfaceDeleter () : p (nullptr) {}
~_SDL_SurfaceDeleter () { SDL_FreeSurface (p); }
};
""";
type surface_holder_t = "surface_deleter" requires surface_deleter;
proc set : &surface_holder_t * &SDL_Surface = "$1->p=$2;";
object owned_surface (surf: &SDL_Surface) implements surface_t =
{
var holder: surface_holder_t;
set (&holder, surf);
// returns a LOAN of the surface only
method fun get_sdl_surface () => surf;
method fun get_width () => surf*.w;
method fun get_height() => surf*.h;
method proc clear (c:colour_t) => FlxGuiSurface::clear surf c;
method proc fill (r:rect_t, c:colour_t) => FlxGuiSurface::fill surf (r,c);
method proc draw_line (c:colour_t, x0:int, y0:int, x1:int, y1:int) { FlxGuiSurface::draw_line surf (c,x0,y0,x1,y1); }
method proc write (x:int, y:int, font:font_t, c: colour_t, s:string) { FlxGuiSurface::write surf (x,y,font,c,s); }
}
}
Drawables¶
Things which can draw on surface planes. A surface provides x,y coordinates, a plane adds a z coordinate. The z coordinate is used to control drawing order: the drawables with lowest z are applied first.
//[drawable.flx]
class FlxGuiDrawable
{
interface drawable_t {
draw: surface_t -> 0;
get_z: 1 -> uint32;
get_tag: 1 -> string;
}
object drawable (tag:string) (z:uint32) (d: surface_t -> 0) implements drawable_t =
{
method fun get_z () => z;
method proc draw (surf:surface_t) => d surf;
method fun get_tag () => tag;
}
// given some routine like draw_line (s:&SDL_surface) (p:parameters)
// this wrapper constructs a drawable by adding a tag name, a Z coordinate
// and binding the parameters.
noinline fun mk_drawable[T] (tag:string) (z:uint32) (d: &SDL_Surface -> T -> 0) (var a:T) : drawable_t =>
drawable tag z (proc (s:surface_t) { d (s.get_sdl_surface()) a; })
;
noinline fun mk_drawable[T] (d: &SDL_Surface -> T -> 0) (var a:T) : drawable_t =>
drawable "notag" 100u32 (proc (s:surface_t) { d (s.get_sdl_surface()) a; })
;
noinline fun mk_drawable[T] (tag:string) (d: &SDL_Surface -> T -> 0) (var a:T) : drawable_t =>
drawable tag 100u32 (proc (s:surface_t) { d (s.get_sdl_surface()) a; })
;
}
Draw Chain¶
A dynamic set of drawables, maintained in Z order. The draw method draws the drawables in the stored Z order. Drawchains are used to schedule and manage the appearance of a window surface for which drawing is demanded asynchronously from the scheduling. This is usual in windowing systems where the window can be hidden, exposed, or require display by events occuring at times different to the events such as mouse clicks triggering state changes.
//[drawchain.flx]
include "gui/__init__";
class FlxGuiDrawChain
{
open FlxGui;
interface drawchain_t {
draw: surface_t -> 0;
remove: string -> 0;
add: drawable_t -> 0;
len: 1 -> size;
get_drawables : 1 -> darray[drawable_t];
}
object drawchain() implements drawchain_t =
{
var drawables = darray[drawable_t] ();
method fun len () => drawables.len;
method fun get_drawables () => drawables;
method proc draw (surf: surface_t)
{
//println$ "----";
for d in drawables do
d.draw surf;
//println$ "Drawn " + d.get_tag() + " " + #(d.get_z).str;
done
}
method proc remove (tag:string)
{
//println$ "remove " + tag;
var i = 0;
while i < drawables.len.int do
if drawables.i.get_tag () == tag do
erase (drawables, i);
else
++i;
done
done
}
method proc add (d:drawable_t)
{
var z = d.get_z ();
var i = 0;
next:>
if i == drawables.len.int do
push_back (drawables, d);
else
if drawables.i.get_z() > z do
insert(drawables, i, d);
else
++i;
goto next;
done
done
}
}
}
Windows¶
We provide a model for a platform dependent top level overlapping window. Windows provide a method to get a surface in the same pixel format as the window. We draw on that then use update operation to synchronise transfer of the surface to the hardware screen.
The provided surface may be the actual window surface in video ram, or it may be a software surface which is blitted to the hardware by system dependent operations.
NOTE: in earlier SDL2 versions there is a catastrophic bug when a window is hidden: the surface becomes invalid. So it is not possible to create the window hidden, initialise it with imagery, and then display it. This means there may be a flicker on window creation as the unpopulated window image is shown then replaced by a populated display.
//[window.flx]
class FlxGuiWindow
{
interface window_t {
get_sdl_window : 1 -> SDL_Window;
get_sdl_surface: 1 -> &SDL_Surface;
get_sdl_window_id : 1 -> uint32;
get_surface: 1 -> surface_t;
add: drawable_t -> 0;
remove: string -> 0;
get_drawchain: 1 -> drawchain_t;
draw: 1 -> 0;
show: 1 -> 0;
hide: 1 -> 0;
raise: 1 -> 0;
prim_update: 1 -> 0;
update: 1 -> 0; // does a draw then prim_update
destroy: 1 -> 0;
}
object window (title:string, xpos:int, ypos:int, width:int,height:int, flag:uint32) implements window_t =
{
var w = SDL_CreateWindow(
title,
xpos,ypos,
width, height,
flag
);
var dc = drawchain ();
method fun get_drawchain () => dc;
method proc add (d:drawable_t) => dc.add d;
method proc remove (tag:string) => dc.remove tag;
method fun get_sdl_window_id () => SDL_GetWindowID w;
method fun get_sdl_window () => w;
method fun get_sdl_surface() => SDL_GetWindowSurface w;
method fun get_surface () : surface_t => surface (SDL_GetWindowSurface w);
method proc show () { SDL_ShowWindow w; }
method proc hide () { SDL_HideWindow w; }
method proc raise () { SDL_RaiseWindow w; }
method proc destroy () { SDL_DestroyWindow w; }
method proc prim_update()
{
var result = SDL_UpdateWindowSurface w;
if result != 0 do
eprintln$ "Unable to update window";
System::exit 1;
done
}
method proc draw ()
{
var surf = surface (SDL_GetWindowSurface w);
dc.draw surf;
}
method proc update () { draw(); prim_update(); }
}
gen create_fixed_window (title:string, x:int, y:int, width:int, height:int) : window_t =>
window (title, x,y,width,height, SDL_WINDOW_SHOWN \| SDL_WINDOW_ALLOW_HIGHDPI)
;
gen create_resizable_window (title:string, x:int, y:int, width:int, height:int) : window_t =>
window (title, x,y,width,height, SDL_WINDOW_RESIZABLE \| SDL_WINDOW_ALLOW_HIGHDPI)
;
}
The Window Controller.¶
In Felix, the window controller is an object which dispatches events read from an input schannel.
The user provides a procedure which can handle the events by reading on an schannel of events. The window controller creates an schannel of events and starts the user procedure as an fthread, passing it the input end of the schannel.
After creation, the window controller object provides a method so the client can fetch the output end of this schannel on which the client writes events. These will then be serviced by the procedure the client provided since the window controller has started it running.
The controller is basically a Felix kind of RAII: on construction an active process is started which can service events.
//[window_controller_interface.flx]
class FlxGuiWindowControllerInterface
{
// ------------------------------------------------------------------
// Window controller is responsible for all the work
// being done on a window. It requires support for
// dispatching events on its event channel.
interface window_controller_interface {
get_window_id : 1 -> uint32;
get_oschannel : 1 -> oschannel[event_t];
destroy_window : 1 -> 0;
display: 1 -> 0;
}
}
//[window_controller.flx]
class FlxGuiWindowController
{
object window_controller
(
w:window_t,
p:(input:ischannel[event_t]) -> 1->0 // chip interface
)
implements window_controller_interface =
{
var imsgs,omsgs = #mk_ioschannel_pair[event_t];
method fun get_window_id () => w.get_sdl_window_id ();
method proc destroy_window () => w.destroy ();
method fun get_oschannel () => omsgs;
method proc display() { w.update(); }
circuit
wire imsgs to p.input
endcircuit
//spawn_fthread (p imsgs);
}
}
The Window Manager.¶
The Window manager is a top level object that is used to fetch process level events such as mouse clicks and dispatch them to the appropriate window event handler.
Note that the Window manager MUST run in the main thread! This is because some system GUI’s maintain separate event queues for each thread (Windows) or may provide a unified queue (X-Windows).
Windows managed by the window manager have two identifying tags: the window ID, maintained by SDL, and the window index, which is the slot number in an array the Felix Window manager uses to store the window controller associated with the window.
The window manager creates the SDL event queue and reads events from the queue. It dispatches them to the appropriate windows based on the SDL window ID if the even has one, or all windows if there isn’t one.
The dispatch, of course, is done by writing the event down the schannel of the window controller associated with the window.
Note carefully that the window manager is the equivalent of a traditional event dispatch loop, and underneath, Felix indeed implements fthreads with schannel I/O using callbacks. However this is transparent to the client programmer! For all intents and purpose the dispatching is done by a background thread to windows each of which is running an active process that listens for events.
//[window_manager.flx]
class FlxGuiWindowManager
{
// Window Manager is responsible for a set of windows,
// and dispatching events specific to a particular
// window to that window.
// ------------------------------------------------------------------
object window_manager () =
{
var windows = darray[window_controller_interface]();
method fun get_n_windows () => windows.len.int;
// add a new window to the controlled set
// return its current index
method gen add_window (w:window_controller_interface) : int =
{
windows += w;
println$ "add_window: index = " + (windows.len.int - 1 ).str + " SDL windows id = " + #(w.get_window_id).str;
return windows.len.int - 1;
}
fun find_window(wid: uint32) : opt[window_controller_interface] =
{
for wobj in windows do
if wid == #(wobj.get_window_id) do
return Some wobj;
done
done
return None[window_controller_interface];
}
fun find_window_index (wid: uint32) : opt[int] =
{
for var i in 0 upto windows.len.int - 1 do
if wid == #(windows.i.get_window_id) return Some i;
done
return None[int];
}
method fun get_window_controller_from_index (i:int) => windows.i;
method proc delete_window (wid: uint32)
{
match find_window_index wid with
| #None => ;
| Some i =>
println$ "delete window found index " + i.str;
windows.i.destroy_window ();
println$ "SDL destroyed";
erase (windows, i);
println$ "Window erased";
endmatch;
}
chip window_event_dispatcher
connector events
pin eventin : %<event_t
pin quit: %>int
{
forever:while true do
var e = read events.eventin;
if e.type.SDL_EventType == SDL_QUIT break forever
dispatch_window_event e;
done
write$ events.quit,1;
}
method fun get_window_event_dispatcher () => window_event_dispatcher;
method proc dispatch_window_event (e:event_t)
{
match SDL_GetWindowID e with
| Some wid =>
match find_window wid with
| Some wobj =>
var omsgs = #(wobj.get_oschannel);
write (omsgs, e);
if e.type.SDL_EventType == SDL_WINDOWEVENT and
e.window.event.SDL_WindowEventID == SDL_WINDOWEVENT_CLOSE
do
#(wobj.get_window_id).delete_window;
println$ "dispatch: window deleted!";
else
wobj.display();
done
| #None => println$ "Can't find window ID = " + str wid;
endmatch;
| #None => println$ "No window for message: Event type " + e.type.SDL_EventType.str;
endmatch;
}
method proc delete_all()
{
println$ "Delete all";
var e : SDL_Event;
e&.type <- SDL_WINDOWEVENT.uint32;
e&.window.event <- SDL_WINDOWEVENT_CLOSE.uint8;
for wobj in windows do
var omsgs = #(wobj.get_oschannel);
e&.window.windowID <- #(wobj.get_window_id);
write (omsgs, e);
done
// note: not bothering to delete the darray :)
}
// the quit channel is deliberately connected to a dummy channel
// (a dummy is used to suppress compiler non-connection warning)
// the WM will suicide when it gets a SDL_QUIT message
method proc start ()
{
var qin,qout = mk_ioschannel_pair[int]();
circuit
connect window_event_dispatcher.eventin, event_source.src
wire qout to window_event_dispatcher.quit
endcircuit
}
// start WM, wait until SDL_QUIT seen
// closes windows before returning
method proc run_until_quit ()
{
var qin,qout = mk_ioschannel_pair[int]();
circuit
connect window_event_dispatcher.eventin, event_source.src
wire qout to window_event_dispatcher.quit
endcircuit
C_hack::ignore(read qin);
// we must have got a quit ..
println$ "QUIT EVENT, deleting all windows";
delete_all();
}
// start WM, wait until SDL_QUIT issued by either
// the user or the timer
// closes windows before returning
method proc run_with_timeout (var timeout: double)
{
var qin,qout = mk_ioschannel_pair[int]();
circuit
connect window_event_dispatcher.eventin, event_source.src
wire qout to window_event_dispatcher.quit
endcircuit
var quit = false;
spawn_fthread$ demo_timer &quit timeout;
C_hack::ignore(read qin);
quit = true;
// we must have got a quit ..
println$ "QUIT EVENT, deleting all windows";
delete_all();
}
}
gen create_SDL_event_source () : ischannel[event_t] =
{
var imsgs, omsgs = mk_ioschannel_pair[event_t]();
circuit
wire omsgs to event_source.src
endcircuit
return imsgs;
}
}
Widgets¶
Simple Click Button¶
//[button.flx]
class FlxGuiButton
{
variant button_state_t =
| Up // ready
| Down // being clicked
| Disabled // inactive
| Mouseover // ready and mouse is over
;
variant button_action_t =
| NoAction
| ClickAction of string
;
interface button_model_t
{
get_state: 1 -> button_state_t;
set_state: button_state_t -> 0;
get_tag: 1 -> string;
}
object ButtonModel
(var tag: string, init_state:button_state_t)
implements button_model_t
=
{
var state = init_state;
method fun get_state() => state;
method proc set_state (s:button_state_t) => state = s;
method fun get_tag () => tag;
}
typedef button_colour_scheme_t =
(
label_colour: colour_t,
bg_colour: colour_t,
top_colour: colour_t,
left_colour: colour_t,
bottom_colour: colour_t,
right_colour: colour_t
);
typedef button_skin_t =
(
up: button_colour_scheme_t,
down: button_colour_scheme_t,
disabled: button_colour_scheme_t,
mouseover: button_colour_scheme_t
);
interface button_display_t {
display: 1 -> 0;
get_client_rect: 1 -> rect_t;
get_label : 1 -> string;
get_tag: 1 -> string;
}
object ButtonDisplay (b:button_model_t)
(
w:window_t, // change to surface later
font:font_t,
label:string,
tag: string, // note: NOT the same as the button's tag!
skin : button_skin_t,
coords: rect_t,
origin: point_t
)
implements button_display_t =
{
// NOTE: the tag must be unique per button-display on each window.
// it is used to *remove* the drawing instructions from the window
// for the previous button state prior to adding new instructions.
// Dont confuse with the label (which might change per display)
// or the button state tag (which is not enough if the same button state
// drives two displays on the same window).
method fun get_tag () => tag;
method fun get_client_rect () => coords;
method fun get_label () => label;
method proc display()
{
var state = b.get_state ();
var scheme = match state with
| #Up => skin.up
| #Down => skin.down
| #Disabled => skin.disabled
| #Mouseover => skin.mouseover
endmatch
;
w.remove tag;
var left_x = coords.x;
var right_x = coords.x + coords.w - 1;
var top_y = coords.y;
var bottom_y = coords.y + coords.h - 1;
var origin_x = origin.x;
var origin_y = origin.y;
// top
w.add$ mk_drawable tag draw_line (scheme.top_colour, left_x - 2,top_y - 2,right_x + 2, top_y - 2) ;
w.add$ mk_drawable tag draw_line (scheme.top_colour, left_x - 1,top_y - 1,right_x + 1, top_y - 1);
// left
w.add$ mk_drawable tag draw_line (scheme.left_colour, left_x - 2,top_y - 2,left_x - 2, bottom_y + 2);
w.add$ mk_drawable tag draw_line (scheme.left_colour, left_x - 1,top_y - 1,left_x - 1, bottom_y + 1);
// right
w.add$ mk_drawable tag draw_line (scheme.right_colour, right_x + 2,top_y - 2,right_x + 2, bottom_y + 2);
w.add$ mk_drawable tag draw_line (scheme.right_colour, right_x + 1,top_y - 1,right_x + 1, bottom_y + 1);
// bottom
w.add$ mk_drawable tag draw_line (scheme.bottom_colour, left_x - 1,bottom_y + 1,right_x + 1, bottom_y + 1);
w.add$ mk_drawable tag draw_line (scheme.bottom_colour, left_x - 2,bottom_y + 2,right_x + 2, bottom_y + 2);
w.add$ mk_drawable tag fill(SDL_Rect (left_x, top_y, right_x - left_x + 1, bottom_y - top_y + 1), scheme.bg_colour);
w.add$ mk_drawable tag FlxGuiSurface::write (origin_x, origin_y, font, scheme.label_colour, label);
} // draw
display();
} //button
chip button_controller
(
bm: button_model_t,
bd: button_display_t
)
connector but
pin ec: %<event_t
pin response: %>button_action_t
{
bd.display();
var run = true;
var e = read but.ec;
while run do
match e with
| MOUSEMOTION mm =>
var x,y = mm.x,mm.y; //int32
if SDL_Point (x.int,y.int) \in bd.get_client_rect () do
//println$ "Motion in client rect of button " + bd.get_label();
match bm.get_state () with
| #Up => bm.set_state Mouseover; bd.display(); // Enter
| _ => ;
endmatch;
else
match bm.get_state () with
| #Mouseover => bm.set_state Up; bd.display(); // Leave
| #Down => bm.set_state Up; bd.display(); // Leave
| _ => ;
endmatch;
done
write$ but.response, NoAction;
| MOUSEBUTTONDOWN mbd =>
x,y = mbd.x,mbd.y; //int32
if SDL_Point (x.int,y.int) \in bd.get_client_rect () do
//println$ "Button down in client rect of button " + bd.get_label();
bm.set_state Down; bd.display();
done
write$ but.response, NoAction;
| MOUSEBUTTONUP mbu =>
x,y = mbu.x,mbu.y; //int32
if SDL_Point (x.int,y.int) \in bd.get_client_rect () do
//println$ "Button up in client rect of button " + bd.get_label();
bm.set_state Mouseover; bd.display();
write$ but.response, ClickAction #(bm.get_tag);
else
bm.set_state Up; bd.display();
write$ but.response, NoAction;
done
| WINDOWEVENT we when we.event == SDL_WINDOWEVENT_LEAVE.uint8 =>
bm.set_state Up; bd.display();
write$ but.response, NoAction;
| _ =>
write$ but.response, NoAction;
endmatch;
e = read but.ec;
done
}
} // class
Tools¶
//[linegraph.flx]
include "gui/__init__";
library GraphTools {
open FlxGui;
interface linegraph_t {
title: string;
func: double -> double;
xmin: double;
xmax: double;
ymin: double;
ymax: double;
client: rect_t;
}
proc linegraph (g:linegraph_t) {
// SDL
FlxGui::init();
// window
var w = create_resizable_window(g.title,
g.client.x,g.client.y,g.client.w,g.client.h
);
w.add$ mk_drawable FlxGui::clear lightgrey;
// font and label
var font_name = dflt_sans_serif_font();
var font : font_t = get_font(font_name, 12);
var bigfont : font_t = get_font(font_name, 14);
var lineskip = get_lineskip font;
// bounding box for graph
var t = 20;
var l = 50;
var b = g.client.h - 90;
var r = g.client.w - 10;
w.add$ mk_drawable FlxGui::write (l+(r - l)/2,10,bigfont,black,g.title);
var c = RGB(0,0,255);
var c2 = RGB(0,0,0);
// top
w.add$ mk_drawable draw_line (c, l - 5,t,r,t);
w.add$ mk_drawable FlxGui::write (l - 40,t,font,black,g.ymax.str);
// bottom
w.add$ mk_drawable draw_line (c, l - 5,b,r,b);
w.add$ mk_drawable FlxGui::write (l - 40,b,font,black,g.ymin.str);
// left
w.add$ mk_drawable draw_line (c, l,t,l,b + 5);
w.add$ mk_drawable FlxGui::write (l,b + 15,font,black,g.xmin.str);
// right
w.add$ mk_drawable draw_line (c, r,t,r,b + 5);
w.add$ mk_drawable FlxGui::write (r - 40,b + 15,font,black,g.xmax.str);
// coordinate transforms
fun i2x (i:int): double => (i - l).double / (r - l).double * (g.xmax - g.xmin) + g.xmin;
fun y2j (y:double) : int => b-((y - g.ymin)/ (g.ymax - g.ymin) * (b - t).double).int;
fun x2i (x:double) : int => ((x - g.xmin) / (g.xmax - g.xmin) * (r - l).double).int + l;
// x axis (y=0)
var jorig = y2j 0.0;
w.add$ mk_drawable FlxGui::write (l - 40,jorig,font,black,"0");
w.add$ mk_drawable draw_line (blue,l,jorig,r,jorig);
// y axis (x=0)
var iorig = x2i 0.0;
w.add$ mk_drawable draw_line (red,iorig,t,iorig,b+5);
w.add$ mk_drawable FlxGui::write (iorig,b+15,font,black,"0");
w.update();
w.show();
var oldi = -2000;
var oldj = 0;
rfor i in l..r do
var x = i2x i;
var y = g.func x;
var j = y2j y;
//println$ g.title+"(" + x.str + ")=" y.str + ", coord(" + i.str + "," + j.str + ")";
var data = c2,oldi,oldj,i,j;
if oldi != -2000 do
w.add$ mk_drawable draw_line data;
done
oldi = i;
oldj= j;
w.update();
sleep(0.01);
done
w.add$ mk_drawable draw_line (c, l,t,r,b);
w.update();
var wm = window_manager();
wm.run_with_timeout 15.0;
FlxGui::quit();
} // lilnegraph
} // GraphTools