1
0
Fork 0

Canvas GUI: Basic widget, focus and theming support.

This commit is contained in:
Thomas Geymayer 2013-07-27 11:54:41 +02:00
parent 187883563a
commit a13add166b
21 changed files with 510 additions and 21 deletions

View file

@ -1,3 +1,46 @@
var gui = {
widgets: {},
focused_window: nil
};
var gui_dir = getprop("/sim/fg-root") ~ "/Nasal/canvas/gui/";
var loadGUIFile = func(file) io.load_nasal(gui_dir ~ file, "canvas");
var loadWidget = func(name) loadGUIFile("widgets/" ~ name ~ ".nas");
loadGUIFile("Style.nas");
loadGUIFile("Widget.nas");
loadGUIFile("styles/DefaultStyle.nas");
loadWidget("Button");
var style = DefaultStyle.new("AmbianceClassic");
var WindowButton = {
new: func(parent, name)
{
var m = {
parents: [WindowButton, gui.widgets.Button.new(parent, nil, {"flat": 1})],
_name: name
};
m._focus_policy = m.NoFocus;
m._setRoot( parent.createChild("image", "WindowButton-" ~ name) );
return m;
},
# protected:
_onStateChange: func
{
var file = style._dir_decoration ~ "/" ~ me._name;
file ~= me._window._focused ? "_focused" : "_unfocused";
if( me._active )
file ~= "_pressed";
else if( me._hover )
file ~= "_prelight";
else if( me._window._focused )
file ~= "_normal";
me._root.set("file", file ~ ".png");
}
};
var Window = {
# Constructor
#
@ -7,7 +50,10 @@ var Window = {
var ghost = _newWindowGhost(id);
var m = {
parents: [Window, PropertyElement, ghost],
_node: props.wrapNode(ghost._node_ghost)
_node: props.wrapNode(ghost._node_ghost),
_focused: 0,
_focused_widget: nil,
_widgets: []
};
m.setInt("content-size[0]", size[0]);
@ -15,6 +61,7 @@ var Window = {
# TODO better default position
m.move(0,0);
m.setFocus();
# arg = [child, listener_node, mode, is_child_event]
setlistener(m._node, func m._propCallback(arg[0], arg[2]), 0, 2);
@ -26,6 +73,8 @@ var Window = {
# Destructor
del: func
{
me.clearFocus();
if( me["_canvas"] != nil )
{
var placements = me._canvas.texture.getChildren("placement");
@ -93,6 +142,42 @@ var Window = {
{
return wrapCanvas(me._getCanvasDecoration());
},
addWidget: func(w)
{
append(me._widgets, w);
w._window = me;
if( size(me._widgets) == 2 )
w.setFocus();
w._onStateChange();
return me;
},
#
setFocus: func
{
if( me._focused )
return me;
if( gui.focused_window != nil )
gui.focused_window.clearFocus();
me._focused = 1;
# me.onFocusIn();
me._onStateChange();
gui.focused_window = me;
return me;
},
#
clearFocus: func
{
if( !me._focused )
return me;
me._focused = 0;
# me.onFocusOut();
me._onStateChange();
gui.focused_window = nil;
return me;
},
setPosition: func(x, y)
{
me.setInt("tf/t[0]", x);
@ -114,6 +199,26 @@ var Window = {
# on writing the z-index the window always is moved to the top of all other
# windows with the same z-index.
me.setInt("z-index", me.get("z-index", 0));
me.setFocus();
},
# protected:
_onStateChange: func
{
if( me._getCanvasDecoration() != nil )
{
# Stronger shadow for focused windows
me.getCanvasDecoration()
.set("image[1]/fill", me._focused ? "#000000" : "rgba(0,0,0,0.5)");
var suffix = me._focused ? "" : "-unfocused";
me._title_bar_bg.set("fill", style.getColor("title" ~ suffix));
me._title.set( "fill", style.getColor("title-text" ~ suffix));
me._top_line.set( "stroke", style.getColor("title-highlight" ~ suffix));
}
foreach(var w; me._widgets)
w._onStateChange();
},
# private:
_propCallback: func(child, mode)
@ -188,28 +293,25 @@ var Window = {
var group_deco = canvas_deco.getGroup("decoration");
var title_bar = group_deco.createChild("group", "title_bar");
title_bar
.rect( 0, 0,
me.get("size[0]"),
me.get("size[1]"), #25,
{"border-top-radius": border_radius} )
.setColorFill(0.25,0.24,0.22)
.setStrokeLineWidth(0);
var style_dir = "gui/styles/AmbianceClassic/";
me._title_bar_bg =
title_bar.rect( 0, 0,
me.get("size[0]"),
me.get("size[1]"),
{"border-top-radius": border_radius} );
me._top_line = title_bar.createChild("path", "top-line")
.moveTo(border_radius - 2, 2)
.lineTo(me.get("size[0]") - border_radius + 2, 2);
# close icon
var x = 10;
var y = 3;
var w = 19;
var h = 19;
var ico = title_bar.createChild("image", "icon-close")
.set("file", style_dir ~ "close_focused_normal.png")
.setTranslation(x,y);
ico.addEventListener("click", func me.del());
ico.addEventListener("mouseover", func ico.set("file", style_dir ~ "close_focused_prelight.png"));
ico.addEventListener("mousedown", func ico.set("file", style_dir ~ "close_focused_pressed.png"));
ico.addEventListener("mouseout", func ico.set("file", style_dir ~ "close_focused_normal.png"));
var button_close = WindowButton.new(title_bar, "close")
.move(x, y);
button_close.onClick = func me.del();
me.addWidget(button_close);
# title
me._title = title_bar.createChild("text", "title")
@ -223,10 +325,8 @@ var Window = {
me._node.getNode("title", 1).alias(me._title._node.getPath() ~ "/text");
me.set("title", title);
title_bar.addEventListener("drag", func(e) {
if( !ico.equals(e.target) )
me.move(e.deltaX, e.deltaY);
});
title_bar.addEventListener("drag", func(e) me.move(e.deltaX, e.deltaY));
me._onStateChange();
}
};

View file

@ -0,0 +1,50 @@
gui.Style = {
new: func(name)
{
var root_node = props.globals.getNode("/sim/gui/canvas", 1)
.addChild("style");
var path = getprop("/sim/fg-root") ~ "/gui/styles/" ~ name;
var m = {
parents: [gui.Style],
_path: path,
_node: io.read_properties(path ~ "/style.xml", root_node),
_colors: {}
};
# parse theme colors
var comp_names = ["red", "green", "blue", "alpha"];
var colors = m._node.getChild("colors");
if( colors != nil )
{
foreach(var color; colors.getChildren())
{
var str = "rgba(";
for(var i = 0; i < size(comp_names); i += 1)
{
if( i > 0 )
str ~= ",";
var val = color.getValue(comp_names[i]);
if( val == nil )
val = 1;
if( i < 3 )
str ~= int(val * 255 + 0.5);
else
str ~= int(val * 100) / 100;
}
m._colors[ color.getName() ] = str ~ ")";
}
}
m._dir_decoration =
m._path ~ "/" ~ (m._node.getValue("folders/decoration") or "decoration");
m._dir_widgets =
m._path ~ "/" ~ (m._node.getValue("folders/widgets") or "widgets");
return m;
},
getColor: func(name, def = "#00ffff")
{
return me._colors[name] or def;
}
};

View file

@ -0,0 +1,83 @@
gui.Widget = {
# enum FocusPolicy:
NoFocus: 0,
TabFocus: 1,
ClickFocus: 2,
StrongFocus: 1 + 2,
#
new: func(derived)
{
return {
parents: [derived, gui.Widget],
_focused: 0,
_focus_policy: gui.Widget.NoFocus,
_hover: 0,
_root: nil
};
},
# Move the widget to the given position (relative to its parent)
move: func(x, y)
{
me._root.setTranslation(x, y);
return me;
},
#
setFocus: func
{
if( me._focused )
return me;
if( me._window._focused_widget != nil )
me._window._focused_widget.clearFocus();
me._focused = 1;
me._window._focused_widget = me;
me.onFocusIn();
me._onStateChange();
return me;
},
#
clearFocus: func
{
if( !me._focused )
return me;
me._focused = 0;
me._window._focused_widget = nil;
me.onFocusOut();
me._onStateChange();
return me;
},
onFocusIn: func {},
onFocusOut: func {},
onMouseEnter: func {},
onMouseLeave: func {},
# protected:
_onStateChange: func {},
_setRoot: func(el)
{
me._root = el;
el.addEventListener("mouseenter", func {
me._hover = 1;
me.onMouseEnter();
me._onStateChange();
});
el.addEventListener("mousedown", func {
if( bits.test(me._focus_policy, me.ClickFocus / 2) )
{
me.setFocus();
me._window.setFocus();
}
});
el.addEventListener("mouseleave", func {
me._hover = 0;
me.onMouseLeave();
me._onStateChange();
});
}
};

View file

@ -0,0 +1,87 @@
var DefaultStyle = {
new: func(name)
{
return { parents: [gui.Style.new(name), DefaultStyle] };
},
createWidget: func(parent, type, cfg)
{
var factory = me.widgets[type];
if( factory == nil )
{
debug.warn("DefaultStyle: unknown widget type (" ~ type ~ ")");
return nil;
}
return factory.new(parent, me, cfg);
},
widgets: {}
};
# A button
DefaultStyle.widgets.button = {
padding: [6, 8, 6, 8],
new: func(parent, style, cfg)
{
var button = {
parents: [DefaultStyle.widgets.button],
element: parent.createChild("group", "button"),
size: cfg.get("size", [26, 26]),
_style: style
};
button._bg =
button.element.rect( 3,
3,
button.size[0] - 6,
button.size[1] - 6,
{"border-radius": 5} );
button._border =
button.element.createChild("image", "button")
.set("slice", "10 12") #"7")
.setSize(button.size);
button._label =
button.element.createChild("text")
.setFont("LiberationFonts/LiberationSans-Regular.ttf")
.set("character-size", 14)
.set("alignment", "center-baseline");
return button;
},
setText: func(text)
{
me._label.set("text", text);
},
update: func(active, focused, hover, backdrop)
{
var file = me._style._dir_widgets ~ "/";
if( backdrop )
{
file ~= "backdrop-";
me._label.set("fill", me._style.getColor("backdrop_fg_color"));
}
else
me._label.set("fill", me._style.getColor("fg_color"));
file ~= "button";
if( active )
{
file ~= "-active";
me._label.setTranslation(me.size[0] / 2 + 1, me.size[1] / 2 + 6);
}
else
me._label.setTranslation(me.size[0] / 2, me.size[1] / 2 + 5);
if( focused and !backdrop )
file ~= "-focused";
if( hover and !active )
{
file ~= "-hover";
me._bg.set("fill", me._style.getColor("button_bg_color_hover"));
}
else
me._bg.set("fill", me._style.getColor("button_bg_color"));
me._border.set("file", file ~ ".png");
}
};

View file

@ -0,0 +1,86 @@
var Config = {
new: func(cfg)
{
var m = {
parents: [Config],
_cfg: cfg
};
if( typeof(m._cfg) != "hash" )
m._cfg = {};
return m;
},
get: func(key, default = nil)
{
var val = me._cfg[key];
if( val != nil )
return val;
return default;
}
};
gui.widgets.Button = {
new: func(parent, style, cfg)
{
var cfg = Config.new(cfg);
var m = gui.Widget.new(gui.widgets.Button);
m._focus_policy = m.StrongFocus;
m._active = 0;
m._flat = cfg.get("flat", 0);
if( style != nil and !m._flat )
{
m._button = style.createWidget(parent, "button", cfg);
m._setRoot(m._button.element);
}
return m;
},
setText: func(text)
{
me._button.setText(text);
return me;
},
setActive: func
{
if( me._active )
return me;
me._active = 1;
me._onStateChange();
return me;
},
clearActive: func
{
if( !me._active )
return me;
me._active = 0;
me._onStateChange();
return me;
},
onClick: func {},
# protected:
_onStateChange: func
{
if( me._button != nil )
me._button.update(me._active, me._focused, me._hover, !me._window._focused);
},
_setRoot: func(el)
{
el.addEventListener("mousedown", func me.setActive());
el.addEventListener("mouseup", func me.clearActive());
# Use 'call' to ensure 'me' is not set and can be used in the closure of
# custom callbacks. TODO pass 'me' as argument?
el.addEventListener("click", func call(me.onClick));
el.addEventListener("mouseleave",func me.clearActive());
el.addEventListener("drag", func(e) e.stopPropagation());
call(gui.Widget._setRoot, [el], me);
}
};
return;

View file

Before

Width:  |  Height:  |  Size: 833 B

After

Width:  |  Height:  |  Size: 833 B

View file

Before

Width:  |  Height:  |  Size: 858 B

After

Width:  |  Height:  |  Size: 858 B

View file

Before

Width:  |  Height:  |  Size: 589 B

After

Width:  |  Height:  |  Size: 589 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 882 B

View file

@ -0,0 +1,83 @@
<?xml version="1.0"?>
<PropertyList>
<!--
<folders>
<decoration type="string">decoration</decoration>
<widgets type="string">widgets</widgets>
</folders>
-->
<colors>
<!-- Window decoration colors -->
<title>
<red type="float">0.275</red>
<green type="float">0.271</green>
<blue type="float">0.251</blue>
</title>
<title-unfocused>
<red type="float">0.235</red>
<green type="float">0.231</green>
<blue type="float">0.216</blue>
</title-unfocused>
<title-text>
<red type="float">0.875</red>
<green type="float">0.859</green>
<blue type="float">0.824</blue>
</title-text>
<title-text-unfocused>
<red type="float">0.502</red>
<green type="float">0.490</green>
<blue type="float">0.471</blue>
</title-text-unfocused>
<title-highlight>
<red type="float">0.365</red>
<green type="float">0.361</green>
<blue type="float">0.341</blue>
</title-highlight>
<title-highlight-unfocused>
<red type="float">0.278</red>
<green type="float">0.275</green>
<blue type="float">0.259</blue>
</title-highlight-unfocused>
<!-- default colors for all GUI objects -->
<bg_color>
<red type="float">0.949</red>
<green type="float">0.945</green>
<blue type="float">0.941</blue>
</bg_color>
<fg_color>
<red type="float">0.298</red>
<green type="float">0.298</green>
<blue type="float">0.298</blue>
</fg_color>
<text_color>
<red type="float">0.235</red>
<green type="float">0.235</green>
<blue type="float">0.235</blue>
</text_color>
<backdrop_fg_color>
<red type="float">0.428</red>
<green type="float">0.427</green>
<blue type="float">0.427</blue>
</backdrop_fg_color>
<button_bg_color>
<red type="float">0.949</red>
<green type="float">0.945</green>
<blue type="float">0.941</blue>
</button_bg_color>
<button_bg_color_hover>
<red type="float">0.996</red>
<green type="float">0.992</green>
<blue type="float">0.988</blue>
</button_bg_color_hover>
</colors>
</PropertyList>

Binary file not shown.

After

Width:  |  Height:  |  Size: 909 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 909 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 848 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 848 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 977 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 934 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 858 B