var DefaultStyle = { new: func(name, name_icon_theme) { return { parents: [ gui.Style.new(name, name_icon_theme), DefaultStyle ] }; }, createWidget: func(parent, type, cfg) { var factory = me.widgets[type]; if( factory == nil ) { debug.warn("DefaultStyle: unknown widget type (" ~ type ~ ")"); return nil; } var w = { parents: [factory], _style: me }; call(factory.new, [parent, cfg], w); return w; }, widgets: {} }; # A button DefaultStyle.widgets.button = { new: func(parent, cfg) { me._root = parent.createChild("group", "button"); me._bg = me._root.createChild("path"); me._border = me._root.createChild("image", "button") .set("slice", "10 12"); #"7") me._label = me._root.createChild("text") .set("font", "LiberationFonts/LiberationSans-Regular.ttf") .set("character-size", 14) .set("alignment", "center-baseline"); }, setSize: func(model, w, h) { me._bg.reset() .rect(3, 3, w - 6, h - 6, {"border-radius": 5}); me._border.setSize(w, h); }, setText: func(model, text) { me._label.setText(text); var min_width = math.max(80, me._label.maxWidth() + 16); model.setLayoutMinimumSize([min_width, 16]); model.setLayoutSizeHint([min_width, 28]); return me; }, update: func(model) { var backdrop = !model._windowFocus(); var (w, h) = model._size; var file = me._style._dir_widgets ~ "/"; # TODO unify color names with image names var bg_color_name = "button_bg_color"; if( backdrop ) bg_color_name = "button_backdrop_bg_color"; else if( !model._enabled ) bg_color_name = "button_bg_color_insensitive"; else if( model._down ) bg_color_name = "button_bg_color_down"; else if( model._hover ) bg_color_name = "button_bg_color_hover"; me._bg.set("fill", me._style.getColor(bg_color_name)); 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( model._down ) { file ~= "-active"; me._label.setTranslation(w / 2 + 1, h / 2 + 6); } else me._label.setTranslation(w / 2, h / 2 + 5); if( model._enabled ) { if( model._focused and !backdrop ) file ~= "-focused"; if( model._hover and !model._down ) file ~= "-hover"; } else file ~= "-disabled"; me._border.set("src", file ~ ".png"); } }; # A checkbox DefaultStyle.widgets.checkbox = { new: func(parent, cfg) { me._label_position = cfg.get("label-position", "right"); me._root = parent.createChild("group", "checkbox"); me._icon = me._root.createChild("image", "checkbox-icon") .setSize(18, 18); me._label = me._root.createChild("text") .set("font", "LiberationFonts/LiberationSans-Regular.ttf") .set("character-size", 14) .set("alignment", "left-center"); }, setSize: func(model, w, h) { if (me._label_position == "left") { me._label.setTranslation(3, int((h / 2) + 1)); me._icon.setTranslation(w - 24, int((h - 18) / 2)); } else { me._icon.setTranslation(3, int((h - 18) / 2)); me._label.setTranslation(24, int(h / 2) + 1); } return me; }, setText: func(model, text) { me._label.setText(text); var min_width = me._label.maxWidth() + 3 + 24; model.setLayoutMinimumSize([min_width, 18]); model.setLayoutSizeHint([min_width, 24]); return me; }, update: func(model) { var backdrop = !model._windowFocus(); var (w, h) = model._size; 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 ~= "check"; if( model._down ) file ~= "-selected"; else file ~= "-unselected"; if( model._enabled ) { if( model._hover ) file ~= "-hover"; } else file ~= "-disabled"; me._icon.set("src", file ~ ".png"); } }; # A label DefaultStyle.widgets.label = { new: func(parent, cfg) { me._root = parent.createChild("group", "label"); }, setSize: func(model, w, h) { if( me['_bg'] != nil ) me._bg.reset().rect(0, 0, w, h); if( me['_img'] != nil ) me._img.set("size[0]", w) .set("size[1]", h); if( me['_text'] != nil ) { # TODO different alignment me._text.setTranslation(2, 2 + h / 2); me._text.set( "max-width", model._cfg.get("wordWrap", 0) ? (w - 4) : 0 ); } return me; }, setText: func(model, text) { if ( !isstr(text) or size(text) == 0 ) { model.setHeightForWidthFunc(nil); return me._deleteElement('text'); } me._createElement("text", "text") .setText(text); var hfw_func = nil; var min_width = me._text.maxWidth() + 4; var width_hint = min_width; if( model._cfg.get("wordWrap", 0) ) { var m = me; hfw_func = func(w) m.heightForWidth(w); min_width = math.min(32, min_width); # prefer approximately quadratic text blocks if( width_hint > 24 ) width_hint = int(math.sqrt(width_hint * 24)); } model.setHeightForWidthFunc(hfw_func); model.setLayoutMinimumSize([min_width, 14]); model.setLayoutSizeHint([width_hint, 24]); return me.update(model); }, setImage: func(model, img) { if( img == nil or size(img) == 0 ) return me._deleteElement('img'); me._createElement("img", "image") .set("src", img) .set("preserveAspectRatio", "xMidYMid slice"); return me; }, # @param bg CSS color or 'none' setBackground: func(model, bg) { if( bg == nil or bg == "none" ) return me._deleteElement("bg"); me._createElement("bg", "path") .set("fill", bg); me.setSize(model, model._size[0], model._size[1]); return me; }, heightForWidth: func(w) { if( me['_text'] == nil ) return -1; return math.max(14, me._text.heightForWidth(w - 4)); }, update: func(model) { if( me['_text'] != nil ) { var color_name = model._windowFocus() ? "fg_color" : "backdrop_fg_color"; me._text.set("fill", me._style.getColor(color_name)); } }, # protected: _createElement: func(name, type) { var mem = '_' ~ name; if( me[ mem ] == nil ) { me[ mem ] = me._root.createChild(type, "label-" ~ name); if( type == "text" ) { me[ mem ].set("font", "LiberationFonts/LiberationSans-Regular.ttf") .set("character-size", 14) .set("alignment", "left-center"); } } return me[ mem ]; }, _deleteElement: func(name) { name = '_' ~ name; if( me[ name ] != nil ) { me[ name ].del(); me[ name ] = nil; } return me; } }; # A one line text input field DefaultStyle.widgets["line-edit"] = { new: func(parent, cfg) { me._hpadding = cfg.get("hpadding", 8); me._root = parent.createChild("group", "line-edit"); me._border = me._root.createChild("image", "border") .set("slice", "10 12"); #"7") me._text = me._root.createChild("text", "input") .set("font", "LiberationFonts/LiberationSans-Regular.ttf") .set("character-size", 14) .set("alignment", "left-baseline") .set("clip-frame", Element.PARENT); me._cursor = me._root.createChild("path", "cursor") .set("stroke", "#333") .set("stroke-width", 1) .moveTo(me._hpadding, 5) .vert(10); me._hscroll = 0; }, setSize: func(model, w, h) { me._border.setSize(w, h); me._text.set( "clip", "rect(0, " ~ (w - me._hpadding) ~ ", " ~ h ~ ", " ~ me._hpadding ~ ")" ); me._cursor.setDouble("coord[2]", h - 10); return me.update(model); }, setText: func(model, text) { me._text.setText(text); model._onStateChange(); }, update: func(model) { var backdrop = !model._windowFocus(); var file = me._style._dir_widgets ~ "/"; if( backdrop ) file ~= "backdrop-"; file ~= "entry"; if( !model._enabled ) file ~= "-disabled"; else if( model._focused and !backdrop ) file ~= "-focused"; me._border.set("src", file ~ ".png"); var color_name = backdrop ? "backdrop_fg_color" : "fg_color"; me._text.set("fill", me._style.getColor(color_name)); me._cursor.setVisible(model._enabled and model._focused and !backdrop); var width = model._size[0] - 2 * me._hpadding; var cursor_pos = me._text.getCursorPos(0, model._cursor)[0]; var text_width = me._text.getCursorPos(0, me._text.lineLength(0))[0]; if( text_width <= width ) # fit -> align left (TODO handle different alignment) me._hscroll = 0; else if( me._hscroll + cursor_pos > width ) # does not fit, cursor to the right me._hscroll = width - cursor_pos; else if( me._hscroll + cursor_pos < 0 ) # does not fit, cursor to the left me._hscroll = -cursor_pos; else if( me._hscroll + text_width < width ) # does not fit, limit scroll to align with right side me._hscroll = width - text_width; var text_pos = me._hscroll + me._hpadding; me._text .setTranslation(text_pos, model._size[1] / 2 + 5) .update(); me._cursor .setDouble("coord[0]", text_pos + cursor_pos) .update(); } }; # ScrollArea DefaultStyle.widgets["scroll-area"] = { new: func(parent, cfg) { me._root = parent.createChild("group", "scroll-area"); me._bg = me._root.createChild("path", "background") .set("fill", "#e0e0e0"); me.content = me._root.createChild("group", "scroll-content") .set("clip-frame", Element.PARENT); me.vert = me._newScroll(me._root, "vert"); me.horiz = me._newScroll(me._root, "horiz"); }, setColorBackground: func { if( size(arg) == 1 ) var arg = arg[0]; me._bg.setColorFill(arg); }, update: func(model) { me.horiz.reset(); if( model._max_scroll[0] > 1 ) # only show scroll bar if horizontally scrollable me.horiz.moveTo( model._scroller_offset[0] + model._scroller_pos[0], model._size[1] - 2 ) .horiz(model._scroller_size[0]); me.vert.reset(); if( model._max_scroll[1] > 1 ) # only show scroll bar if vertically scrollable me.vert.moveTo( model._size[0] - 2, model._scroller_offset[1] + model._scroller_pos[1] ) .vert(model._scroller_size[1]); me._bg.reset() .rect(0, 0, model._size[0], model._size[1]); me.content.set( "clip", "rect(0, " ~ model._size[0] ~ ", " ~ model._size[1] ~ ", 0)" ); }, # private: _newScroll: func(el, orient) { return el.createChild("path", "scroll-" ~ orient) .set("stroke", "#f07845") .set("stroke-width", 4); }, # Calculate size and limits of scroller # # @param model # @param dir 0 for horizontal, 1 for vertical # @return [scroller_size, min_pos, max_pos] _updateScrollMetrics: func(model, dir) { if( model._content_size[dir] <= model._size[dir] ) return; model._scroller_size[dir] = math.max( 12, model._size[dir] * (model._size[dir] / model._content_size[dir]) ); model._scroller_offset[dir] = 0; model._scroller_delta[dir] = model._size[dir] - model._scroller_size[dir]; } }; DefaultStyle.widgets["tab-widget"] = { tabBarHeight: 30, new: func(parent, cfg) { me._root = parent.createChild("group", "tab-widget"); me.bg = me._root.createChild("path", "background") .set("fill", "#e0e0e0"); me.tabBar = me._root.createChild("group", "tab-widget-tabbar"); me.content = me._root.createChild("group", "tab-widget-content"); }, update: func(model) { me.bg.set("fill", me._style.getColor("bg_color")); me.tabBar.update(); me.content.update(); }, setSize: func(model, w, h) { me.bg.reset().rect(0, w, h, 0); me.tabBar.set("clip", sprintf("rect(0, %d, %d, 0)", w, me.tabBarHeight)); me.content.setTranslation(0, me.tabBarHeight); me.content.set("clip", sprintf("rect(0, %d, %d, 0)", w, h)); }, }; # Tab button for the tab widget DefaultStyle.widgets["tab-widget-tab-button"] = { new: func(parent, cfg) { me._root = parent.createChild("group", "tab-widget-tab-button"); me._bg = me._root.createChild("path") .set("fill", me._style.getColor("tab_widget_tab_button_bg_focused")) .set("stroke", me._style.getColor("tab_widget_tab_button_border")) .set("stroke-width", 1); me._selected_indicator = me._root.createChild("path") .set("stroke", me._style.getColor("tab_widget_tab_button_selected_indicator")) .set("stroke-width", 3); me._label = me._root.createChild("text") .set("font", "LiberationFonts/LiberationSans-Regular.ttf") .set("character-size", 14) .set("alignment", "center-baseline"); }, setSize: func(model, w, h) { me._bg.reset().rect(3, 0, w - 6, h); me._selected_indicator.reset().moveTo(3, h).horiz(w - 6); }, setText: func(model, text) { me._label.setText(text); var min_width = math.max(80, me._label.maxWidth() + 16); model.setLayoutMinimumSize([min_width, 30]); model.setLayoutSizeHint([min_width, 30]); return me; }, update: func(model) { var backdrop = !model._windowFocus(); var (w, h) = model._size; me._label.setTranslation(w / 2, h / 2 + 5); var bg_color_name = "tab_widget_tab_button_bg_focused"; if (backdrop) { bg_color_name = "tab_widget_tab_button_bg_unfocused"; } else if (model._selected) { bg_color_name = "tab_widget_tab_button_bg_selected"; } else if (model._hover) { bg_color_name = "tab_widget_tab_button_bg_hovered"; } me._bg.set("fill", me._style.getColor(bg_color_name)); me._selected_indicator.setVisible(model._selected); if (backdrop) { me._label.set("fill", me._style.getColor("backdrop_fg_color")); } else { me._label.set("fill", me._style.getColor("fg_color")); } } }; # A horizontal or vertical rule line # possibly with a text label embedded DefaultStyle.widgets.rule = { new: func(parent, cfg) { me._root = parent.createChild("group", "rule"); var bgColor = me._style.getColor("fg_color"); var shadowColor = me._style.getColor("fg_color_shadow"); me._bg = me._root.createChild("path") .set("fill", bgColor) .set("stroke", bgColor) .set("stroke-width", 1); me._shadow = me._root.createChild("path") .set("fill", shadowColor) .set("stroke", shadowColor) .set("stroke-width", 1); me._isVertical = cfg.get("isVertical"); }, setSize: func(model, w, h) { var firstWidth = w; var firstHeight = h; var hh = h * 0.5; if( me['_text'] != nil ) { firstHeight = firstWidth = 20; # TODO make this settable in Style.xml # TODO handle eliding for translations? me._text.setTranslation(firstWidth + 3, hh); var maxW = model._cfg.get("maxTextWidth", -1); if (maxW > 0) { me._text.set("max-width", maxW); } # assume horizontal for now, with a label var bg2Left = maxW > 0 ? maxW : me._text.maxWidth() + 22; var bg2Width = w - bg2Left; me._bg2.reset().rect(bg2Left, hh - 1, bg2Width, 2); me._shadow2.reset().moveTo(bg2Left + 1, hh).horiz(bg2Width - 1); } if (me._isVertical ) { me._bg.reset().rect(0, 0, 2, firstHeight); me._shadow.reset().moveTo(1, 1).vert(firstHeight - 1); } else { me._bg.reset().rect(0, hh - 1, firstWidth, 2); me._shadow.reset().moveTo(1, hh).horiz(firstWidth - 1); } return me; }, setText: func(model, text) { if( text == nil or size(text) == 0 ) { # force a resize? me._deleteElement('bg2'); me._deleteElement('shadow2'); return me._deleteElement('text'); } if (me._isVertical) { logprint(LOG_DEVALERT, "Text label not supported for vertical rules, yet"); return; } me._createElement("text", "text") .setText(text); var width_hint = me._text.maxWidth() + 40; var bgColor = me._style.getColor("fg_color"); var shadowColor = me._style.getColor("fg_color_shadow"); me._bg2 = me._root.createChild("path") .set("fill", bgColor) .set("stroke", bgColor) .set("stroke-width", 1); me._shadow2 = me._root.createChild("path") .set("fill", shadowColor) .set("stroke", shadowColor) .set("stroke-width", 1); model.setLayoutMinimumSize([40, 14]); # TODO mark as expanding? model.setLayoutSizeHint([width_hint, 24]); return me.update(model); }, update: func(model) { # different color if disabled? if( me['_text'] != nil ) { var color_name = model._windowFocus() ? "fg_color" : "backdrop_fg_color"; me._text.set("fill", me._style.getColor(color_name)); } }, # protected: _createElement: func(name, type) { var mem = '_' ~ name; if( me[ mem ] == nil ) { me[ mem ] = me._root.createChild(type, "rule-" ~ name); if( type == "text" ) { me[ mem ].set("font", "LiberationFonts/LiberationSans-Regular.ttf") .set("character-size", 14) .set("alignment", "left-center"); } } return me[ mem ]; }, _deleteElement: func(name) { name = '_' ~ name; if( me[ name ] != nil ) { me[ name ].del(); me[ name ] = nil; } return me; } }; # a frame (sometimes called a group box), with optional label # and enable/disable checkbox DefaultStyle.widgets.frame = { new: func(parent, cfg) { me._root = parent.createChild("group", "frame-box"); me._createElement("bg", "image") .set("slice", "8 8"); me.content = me._root.createChild("group", "frame-content"); # handle label + checkable flag }, update: func(model) { var file = me._style._dir_widgets ~ "/"; file ~= "frame"; # if( !model._enabled ) # file ~= "-disabled"; me._bg.set("src", file ~ ".png"); me._bg.setSize(model._size[0], model._size[1]); }, # protected: _createElement: func(name, type) { var mem = '_' ~ name; if( me[ mem ] == nil ) { me[ mem ] = me._root.createChild(type, "frame-" ~ name); if( type == "text" ) { me[ mem ].set("font", "LiberationFonts/LiberationSans-Regular.ttf") .set("character-size", 14) .set("alignment", "left-center"); } } return me[ mem ]; }, _deleteElement: func(name) { name = '_' ~ name; if( me[ name ] != nil ) { me[ name ].del(); me[ name ] = nil; } return me; } }; # a horizontal or vertical slider, for selecting / # dragging over a numerical range DefaultStyle.widgets.slider = { new: func(parent, cfg) { me._root = parent.createChild("group", "slider"); me._createElement("bg", "image") .set("slice", "2 6"); me._createElement("fill", "image") .set("slice", "2 6"); me._fillHeight = me._fill.imageSize()[1]; me._createElement("thumb", "image"); me._thumbSize = me._thumb.imageSize(); me._ticks = 0; me._ticksPath = nil; }, setNormValue: func(model, normValue) { var (w, h) = model._size; var halfThumbWidth = me._thumbSize[0] * 0.5; var availWidthPos = w - me._thumbSize[0]; var thumbX = math.round(availWidthPos * normValue); var thumbY = (h - me._thumbSize[1]) * 0.5; me._thumb.setTranslation(thumbX - halfThumbWidth, thumbY); me._fill.setSize(thumbX, me._fillHeight); }, update: func(model) { var direction = "horizontal"; # set background state var file = me._style._dir_widgets ~ "/"; file ~= "scale-" ~ direction ~ "-trough"; if( !model._enabled ) file ~= "-disabled"; me._bg.set("src", file ~ ".png"); # fill state var file = me._style._dir_widgets ~ "/"; file ~= "scale-" ~ direction ~ "-fill"; if( !model._enabled ) { file ~= "-disabled"; } else { } me._fill.set("src", file ~ ".png"); # set thumb state file = me._style._dir_widgets ~ "/"; file ~= "slider-" ~ direction; if( !model._enabled ) { file ~= "-disabled"; } else { if (model._down) file ~= "-focused"; if (model._hover) file ~= "-hover"; } me._thumb.set("src", file ~ ".png"); # update the position as well, since other stuff # may have changed me.setNormValue(model, model._normValue()); }, setSize: func(model, w, h) { var fillTop = (h - me._fillHeight) * 0.5; me._bg.setTranslation(0, fillTop); me._fill.setTranslation(0, fillTop); me._bg.setSize(w, me._fillHeight); me.setNormValue(model, model._normValue()); }, updateRanges: func(minValue, maxValue, numTicks = 0) { if (me._ticks != numTicks) { # update tick marks if (numTicks == 0) { me._ticks = 0; me._deleteElement('ticksPath'); } else { me._createElement('ticksPath', 'path'); me._ticks = numTicks; # set style # loop adding ticks } } }, # protected: _createElement: func(name, type) { var mem = '_' ~ name; if( me[ mem ] == nil ) { me[ mem ] = me._root.createChild(type, "slider-" ~ name); if( type == "text" ) { me[ mem ].set("font", "LiberationFonts/LiberationSans-Regular.ttf") .set("character-size", 14) .set("alignment", "left-center"); } } return me[ mem ]; }, _deleteElement: func(name) { name = '_' ~ name; if( me[ name ] != nil ) { me[ name ].del(); me[ name ] = nil; } return me; } }