diff --git a/docs/assets/examples/stack.png b/docs/assets/examples/stack.png
new file mode 100644
index 0000000..2cd81b9
Binary files /dev/null and b/docs/assets/examples/stack.png differ
diff --git a/examples/README.md b/examples/README.md
index d8f5cb0..66d407f 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -195,6 +195,10 @@ The `widgets` directory contains examples for how to use different widgets.
Search Entry |
|
+
+ Stack |
+ |
+
Text View |
|
diff --git a/examples/widgets/stack.nim b/examples/widgets/stack.nim
new file mode 100644
index 0000000..25a8113
--- /dev/null
+++ b/examples/widgets/stack.nim
@@ -0,0 +1,110 @@
+# MIT License
+#
+# Copyright (c) 2022 Can Joshua Lehmann
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+import std/[sequtils]
+import owlkettle, owlkettle/[dataentries, playground, adw]
+
+type DummyPage = tuple[
+ name: string,
+ title: string,
+ text: string,
+ visible: bool,
+ useUnderline: bool,
+ needsAttention: bool,
+ iconName: string
+]
+
+let stackPages: seq[DummyPage] = (1..2).mapIt((
+ "Widget " & $it,
+ "Title _" & $it,
+ "I am stack page " & $it,
+ true,
+ true,
+ false,
+ ""
+))
+
+viewable App:
+ hhomogenous: bool = true
+ interpolateSize: bool = true
+ transitionDuration: uint = 500
+ transitionType: StackTransitionType = StackTransitionSlideUp
+ vhomogenous: bool = true
+ visibleChildName: string = "Widget 1"
+ pages: seq[DummyPage] = stackPages
+ sensitive: bool = true
+ tooltip: string = ""
+ sizeRequest: tuple[x, y: int] = (-1, -1)
+
+var counter = 0
+method view(app: AppState): Widget =
+ echo "View run #", counter
+ counter.inc
+ let stack = gui:
+ Stack():
+ hhomogenous = app.hhomogenous
+ interpolateSize = app.interpolateSize
+ transitionDuration = app.transitionDuration
+ transitionType = app.transitionType
+ vhomogenous = app.vhomogenous
+ visibleChildName = app.visibleChildName
+ sensitive = app.sensitive
+ tooltip = app.tooltip
+ sizeRequest = app.sizeRequest
+
+ for page in app.pages:
+ StackPage():
+ name = page.name
+ title = page.title
+ iconName = page.iconName
+ visible = page.visible
+ useUnderline = page.useUnderline
+ needsAttention = page.needsAttention
+
+ Box():
+ Label(text = page.text)
+
+ result = gui:
+ Window():
+ title = "Stack Example"
+ defaultSize = (800, 400)
+ HeaderBar() {.addTitlebar.}:
+ insert(app.toAutoFormMenu(sizeRequest = (700, 600))) {.addRight.}
+
+ for page in app.pages:
+ Button(text = page.name) {.addRight.}:
+ style = [ButtonFlat]
+ proc clicked() =
+ app.visibleChildName = page.name
+
+ Box(orient = OrientY):
+ Label(text = app.pages[0].repr) {.expand: false.}
+ Label(text = app.pages[1].repr) {.expand: false.}
+
+ StackSidebar():
+ insert(stack)
+
+ insert(stack)
+
+
+
+adw.brew(gui(App()))
diff --git a/owlkettle/bindings/gtk.nim b/owlkettle/bindings/gtk.nim
index 24f2e78..c728124 100644
--- a/owlkettle/bindings/gtk.nim
+++ b/owlkettle/bindings/gtk.nim
@@ -130,6 +130,31 @@ type
GTK_LEVEL_BAR_MODE_CONTINUOUS
GTK_LEVEL_BAR_MODE_DISCRETE
+ GtkStackTransitionType* = enum
+ GTK_STACK_TRANSITION_TYPE_NONE
+ GTK_STACK_TRANSITION_TYPE_CROSSFADE
+ GTK_STACK_TRANSITION_TYPE_SLIDE_RIGHT
+ GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT
+ GTK_STACK_TRANSITION_TYPE_SLIDE_UP
+ GTK_STACK_TRANSITION_TYPE_SLIDE_DOWN
+ GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT_RIGHT
+ GTK_STACK_TRANSITION_TYPE_SLIDE_UP_DOWN
+ GTK_STACK_TRANSITION_TYPE_OVER_UP
+ GTK_STACK_TRANSITION_TYPE_OVER_DOWN
+ GTK_STACK_TRANSITION_TYPE_OVER_LEFT
+ GTK_STACK_TRANSITION_TYPE_OVER_RIGHT
+ GTK_STACK_TRANSITION_TYPE_UNDER_UP
+ GTK_STACK_TRANSITION_TYPE_UNDER_DOWN
+ GTK_STACK_TRANSITION_TYPE_UNDER_LEFT
+ GTK_STACK_TRANSITION_TYPE_UNDER_RIGHT
+ GTK_STACK_TRANSITION_TYPE_OVER_UP_DOWN
+ GTK_STACK_TRANSITION_TYPE_OVER_DOWN_UP
+ GTK_STACK_TRANSITION_TYPE_OVER_LEFT_RIGHT
+ GTK_STACK_TRANSITION_TYPE_OVER_RIGHT_LEFT
+ GTK_STACK_TRANSITION_TYPE_ROTATE_LEFT
+ GTK_STACK_TRANSITION_TYPE_ROTATE_RIGHT
+ GTK_STACK_TRANSITION_TYPE_ROTATE_LEFT_RIGHT
+
GtkTextIter* = object
a, b: pointer
c, d, e, f, g, h: cint
@@ -158,6 +183,7 @@ type
GtkMediaStream* = distinct pointer
GtkListItemFactory* = distinct pointer
GtkSelectionModel* = distinct pointer
+ GtkStackPage* = distinct pointer
proc isNil*(obj: GtkTextBuffer): bool {.borrow.}
proc isNil*(obj: GtkTextTag): bool {.borrow.}
@@ -177,6 +203,9 @@ proc isNil*(obj: GtkParamSpec): bool {.borrow.}
proc isNil*(obj: GtkMediaStream): bool {.borrow.}
proc isNil*(obj: GtkListItemFactory): bool {.borrow.}
proc isNil*(obj: GtkSelectionModel): bool {.borrow.}
+proc isNil*(obj: GtkStackPage): bool {.borrow.}
+
+proc `==`*(x, y: GtkStackPage): bool {.borrow.}
template defineBitSet(typ) =
proc `==`*(a, b: typ): bool {.borrow.}
@@ -928,8 +957,33 @@ when GtkMinor >= 10:
proc gtk_search_entry_set_placeholder_text*(widget: GtkWidget, text: cstring)
# Gtk.Stack
-proc gtk_stack_add_named*(stack, child: GtkWidget, name: cstring)
+proc gtk_stack_new*(): GtkWidget
+proc gtk_stack_add_named*(stack, child: GtkWidget, name: cstring): GtkStackPage
proc gtk_stack_remove*(stack, child: GtkWidget)
+proc gtk_stack_add_titled*(stack, child: GtkWidget, name: cstring, title: cstring): GtkStackPage
+proc gtk_stack_set_hhomogeneous*(stack: GtkWidget, hhomogeneous: cbool)
+proc gtk_stack_set_interpolate_size*(stack: GtkWidget, interpolate_size: cbool)
+proc gtk_stack_set_transition_duration*(stack: GtkWidget, duration: cuint)
+proc gtk_stack_set_transition_type*(stack: GtkWidget, transition: GtkStackTransitionType)
+proc gtk_stack_set_vhomogeneous*(stack: GtkWidget, vhomogeneous: cbool)
+proc gtk_stack_set_visible_child*(stack, child: GtkWidget)
+proc gtk_stack_set_visible_child_name*(stack: GtkWidget, name: cstring)
+
+# Gtk.StackPage
+proc gtk_stack_page_set_icon_name*(self: GtkStackPage, setting: cstring)
+proc gtk_stack_page_set_name*(self: GtkStackPage, setting: cstring)
+proc gtk_stack_page_set_needs_attention*(self: GtkStackPage, setting: cbool)
+proc gtk_stack_page_set_title*(self: GtkStackPage, setting: cstring)
+proc gtk_stack_page_set_use_underline*(self: GtkStackPage, setting: cbool)
+proc gtk_stack_page_set_visible*(self: GtkStackPage, visible: cbool)
+
+# Gtk.StackSwitcher
+proc gtk_stack_switcher_new*(): GtkWidget
+proc gtk_stack_switcher_set_stack*(switcher, stack: GtkWidget)
+
+# Gtk.StackSidebar
+proc gtk_stack_sidebar_new*(): GtkWidget
+proc gtk_stack_sidebar_set_stack*(sidebar, stack: GtkWidget)
# Gtk.MenuButton
proc gtk_menu_button_new*(): GtkWidget
diff --git a/owlkettle/playground.nim b/owlkettle/playground.nim
index 8be597d..5afc3cc 100644
--- a/owlkettle/playground.nim
+++ b/owlkettle/playground.nim
@@ -213,6 +213,35 @@ proc toFormField(state: Viewable, field: ptr ScaleMark, fieldName: string): Widg
proc select(enumIndex: int) =
field[].position = enumIndex.ScalePosition
+proc toFormField(state: auto, fieldName: static string, index: int, typ: typedesc[tuple[name: string, title: string, text: string]]): Widget =
+ ## Provides a form to display a single entry of type `tuple[name: string, title: string, text: string]` in a list of entries.
+ let tup = state.getField(fieldName)[index]
+ return gui:
+ ActionRow:
+ title = "(Name, Title, Text)"
+ Entry() {.addSuffix.}:
+ text = state.getField(fieldName)[index].name
+ proc changed(text: string) =
+ state.getField(fieldName)[index].name = text
+
+ Entry() {.addSuffix.}:
+ text = state.getField(fieldName)[index].title
+ proc changed(text: string) =
+ state.getField(fieldName)[index].title = text
+
+ Entry() {.addSuffix.}:
+ text = state.getField(fieldName)[index].text
+ proc changed(text: string) =
+ state.getField(fieldName)[index].text = text
+
+ Button {.addSuffix.}:
+ icon = "user-trash-symbolic"
+ proc clicked() =
+ state.getField(fieldName).delete(index)
+
+proc toFormField[T](state: auto, fieldName: static string, typ: typedesc[seq[T]]): Widget =
+ ## Provides a form field for any field on `state` with a seq type.
+ ## Displays a dummy widget if there is no `toListFormField` implementation for type T.
proc addDeleteButton(formField: Widget, value: ptr seq[auto], index: int) =
let button = gui:
Button():
diff --git a/owlkettle/widgets.nim b/owlkettle/widgets.nim
index c6ed341..a4a846d 100644
--- a/owlkettle/widgets.nim
+++ b/owlkettle/widgets.nim
@@ -22,7 +22,7 @@
# Default widgets
-import std/[unicode, os, sets, tables, options, asyncfutures, strutils, sequtils, sugar, strformat, hashes, times]
+import std/[unicode, os, sugar, sets, tables, options, asyncfutures, hashes, times, strutils, sequtils, strformat]
when defined(nimPreviewSlimSystem):
import std/assertions
import widgetdef, cairo, widgetutils, common
@@ -2028,11 +2028,11 @@ renderable PopoverMenu of BasePopover:
newPage = pageWidget.update(page)
if not newPage.isNil:
gtk_stack_remove(stack, page.unwrapInternalWidget())
- gtk_stack_add_named(stack, newPage.unwrapInternalWidget(), name.cstring)
+ discard gtk_stack_add_named(stack, newPage.unwrapInternalWidget(), name.cstring)
state.pages[name] = newPage
else:
let page = pageWidget.build()
- gtk_stack_add_named(stack, page.unwrapInternalWidget(), name.cstring)
+ discard gtk_stack_add_named(stack, page.unwrapInternalWidget(), name.cstring)
state.pages[name] = page
adder add {.name: "main".}:
@@ -3788,6 +3788,273 @@ renderable Scale of BaseWidget:
app.value = newValue
+type StackTransitionType* = enum
+ StackTransitionNone
+ StackTransitionCrossFade
+ StackTransitionSlideRight
+ StackTransitionSlideLeft
+ StackTransitionSlideUp
+ StackTransitionSlideDown
+ StackTransitionSlideLeftRight
+ StackTransitionSlideUpDown
+ StackTransitionOverUp
+ StackTransitionOverDown
+ StackTransitionOverLeft
+ StackTransitionOverRight
+ StackTransitionUnderUp
+ StackTransitionUnderDown
+ StackTransitionUnderLeft
+ StackTransitionUnderRight
+ StackTransitionOverUpDown
+ StackTransitionOverDownUp
+ StackTransitionOverLeftRight
+ StackTransitionOverRightLeft
+ StackTransitionRotateLeft
+ StackTransitionRotateRight
+ StackTransitionRotateLeftRight
+
+proc toGtk*(x: StackTransitionType): GtkStackTransitionType =
+ GtkStackTransitionType(ord(x))
+
+proc updateStackPage(page: auto)
+
+renderable StackPage:
+ widget: Widget
+ internalObject: GtkStackPage
+ iconName: string
+ title: string
+ name: string
+ useUnderline: bool
+ visible: bool
+ needsAttention: bool
+
+ hooks:
+ beforeBuild:
+ state.internalWidget = gtk_box_new(
+ toGtk(OrientY),
+ 0.cint
+ )
+
+ hooks internalObject:
+ property:
+ if not state.internalObject.isNil():
+ updateStackPage(state)
+
+ hooks widget:
+ (build, update):
+ state.updateChild(state.widget, widget.valWidget, gtk_box_append, gtk_box_remove)
+
+ hooks iconName:
+ property:
+ if not state.internalObject.isNil():
+ gtk_stack_page_set_icon_name(state.internalObject, state.iconName.cstring)
+
+ hooks title:
+ property:
+ if not state.internalObject.isNil():
+ gtk_stack_page_set_title(state.internalObject, state.title.cstring)
+
+ # hooks name:
+ # property:
+ # if not state.internalObject.isNil():
+ # gtk_stack_page_set_title(state.internalObject, state.title.cstring)
+
+ hooks useUnderline:
+ property:
+ if not state.internalObject.isNil():
+ gtk_stack_page_set_use_underline(state.internalObject, state.useUnderline.cbool)
+
+ hooks visible:
+ property:
+ if not state.internalObject.isNil():
+ gtk_stack_page_set_visible(state.internalObject, state.visible.cbool)
+
+ hooks needsAttention:
+ property:
+ if not state.internalObject.isNil():
+ gtk_stack_page_set_needs_attention(state.internalObject, state.needsAttention.cbool)
+
+ adder add: # TODO: Refactor this to not need name
+ if widget.hasWidget:
+ raise newException(ValueError, "Unable to add multiple children to a StackPage. Use a Box widget to display multiple widgets in a StackPage")
+
+ widget.hasWidget = true
+ widget.valWidget = child
+
+proc updateStackPage(page: auto) =
+ if page.internalObject.isNil():
+ return
+ if page.iconName.len() > 0:
+ gtk_stack_page_set_icon_name(page.internalObject, page.iconName.cstring)
+ else:
+ gtk_stack_page_set_icon_name(page.internalObject, nil.cstring)
+
+ gtk_stack_page_set_title(page.internalObject, page.title.cstring)
+ gtk_stack_page_set_use_underline(page.internalObject, page.useUnderline.cbool)
+ gtk_stack_page_set_visible(page.internalObject, page.visible.cbool)
+ gtk_stack_page_set_needs_attention(page.internalObject, page.needsAttention.cbool)
+
+proc assignApp[T](children: Table[string, T], app: Viewable) =
+ for name, widget in children:
+ widget.assignApp(app)
+
+proc hasChangesComparedTo(x: StackPage, y: StackPageState): bool =
+ x.valInternalObject.pointer != y.internalObject.pointer or x.valTitle != y.title or x.valVisible != y.visible or x.valUseUnderline != y.useUnderline or x.valNeedsAttention != y.needsAttention or x.valIconName != y.iconName
+
+var counter = 0
+
+proc updateChildren*(state: Renderable,
+ stackChildren: var Table[string, WidgetState],
+ stackUpdates: Table[string, Widget],
+ addChild: proc(widget, child: GtkWidget, name, title: cstring): GtkStackPage {.cdecl, locker.},
+ removeChild: proc(widget, child: GtkWidget) {.cdecl, locker.}) =
+ stackUpdates.assignApp(state.app)
+ counter.inc
+ let newPageNames = stackUpdates.keys.toSeq().toSet()
+ let oldPageNames = stackChildren.keys.toSeq().toSet()
+ let newAddedPageNames = newPageNames.difference(oldPageNames)
+ let oldRemovedPageNames = oldPageNames.difference(newPageNames)
+ let sharedPageNames = oldPageNames.difference(oldRemovedPageNames)
+ var forceReadd = true
+
+ for pageName in sharedPageNames:
+ let newWidget = StackPage(stackUpdates[pageName])
+ let oldWidgetState = StackPageState(stackChildren[pageName])
+ let newWidgetState: StackPageState = newWidget.update(oldWidgetState).StackPageState
+
+ let hasChanges = not newWidgetState.isNil()
+
+ if hasChanges:
+ let oldGtkWidget = oldWidgetState.unwrapInternalWidget()
+ removeChild(state.internalWidget, oldGtkWidget)
+
+ let newGtkWidget = newWidgetState.unwrapInternalWidget()
+ let newPage: GtkStackPage = addChild(state.internalWidget, newGtkWidget, pageName.cstring, newWidget.valTitle.cstring)
+ StackPageState(stackChildren[pageName]).internalObject = newPage
+ StackPageState(stackChildren[pageName]).updateStackPage()
+ forceReadd = true
+
+ elif forceReadd:
+ let currentGtkWidget = oldWidgetState.unwrapInternalWidget()
+ g_object_ref(pointer(currentGtkWidget))
+ removeChild(state.internalWidget, currentGtkWidget)
+ let page = addChild(state.internalWidget, currentGtkWidget, pageName.cstring, oldWidgetState.title.cstring)
+ StackPageState(stackChildren[pageName]).internalObject = page
+ StackPageState(stackChildren[pageName]).updateStackPage()
+ g_object_unref(pointer(currentGtkWidget))
+
+ for newPageName in newAddedPageNames:
+ let
+ newStackUpdate = StackPage(stackUpdates[newPageName])
+ newStackState = StackPageState(newStackUpdate.build())
+ newGtkWidget = newStackState.unwrapInternalWidget()
+ let newPage = addChild(state.internalWidget, newGtkWidget, newPageName.cstring, newStackState.title.cstring)
+ newStackState.internalObject = newPage
+ newStackState.updateStackPage()
+ stackChildren[newPageName] = newStackState
+
+ for removedPageName in oldRemovedPageNames:
+ let stackChild = StackPageState(stackChildren[removedPageName])
+ let oldGtkWidget = stackChild.unwrapInternalWidget()
+ removeChild(state.internalWidget, oldGtkWidget)
+
+ stackChildren.del(removedPageName)
+
+renderable Stack of BaseWidget:
+ pages: Table[string, Widget]
+ hhomogenous: bool = true
+ interpolateSize: bool = true
+ transitionDuration: uint = 500
+ transitionType: StackTransitionType = StackTransitionSlideUp
+ vhomogenous: bool = true
+ visibleChildName: string
+
+ hooks:
+ beforeBuild:
+ state.internalWidget = gtk_stack_new()
+
+ hooks pages:
+ (build, update):
+ state.updateChildren(
+ state.pages,
+ widget.valPages,
+ gtk_stack_add_titled,
+ gtk_stack_remove
+ )
+
+ hooks hhomogenous:
+ property:
+ gtk_stack_set_hhomogeneous(state.internalWidget, state.hhomogenous.cbool)
+
+ hooks interpolateSize:
+ property:
+ gtk_stack_set_interpolate_size(state.internalWidget, state.interpolateSize.cbool)
+
+ hooks transitionDuration:
+ property:
+ gtk_stack_set_transition_duration(state.internalWidget, state.transitionDuration.cuint)
+
+ hooks transitionType:
+ property:
+ gtk_stack_set_transition_type(state.internalWidget, state.transitionType.toGtk())
+
+ hooks vhomogenous:
+ property:
+ gtk_stack_set_vhomogeneous(state.internalWidget, state.vhomogenous.cbool)
+
+ hooks visibleChildName:
+ property:
+ let hasVisibleChild = state.pages.hasKey(state.visibleChildName)
+ if hasVisibleChild:
+ let pageState = StackPageState(state.pages[state.visibleChildName])
+ let pageWidget = pageState.unwrapInternalWidget()
+ gtk_stack_set_visible_child(state.internalWidget, pageWidget)
+
+ adder add:
+ if not (child of StackPage):
+ raise newException(ValueError, "You can only add StackPages widgets directly to Stack")
+
+ let stackPage = StackPage(child)
+ if stackPage.valName in widget.valPages:
+ raise newException(ValueError, "Page \"" & stackPage.valName & "\" already exists")
+
+ widget.hasPages = true
+ widget.valPages[stackPage.valName] = stackPage
+
+renderable StackSwitcher of BaseWidget:
+ stack: Widget
+
+ hooks:
+ beforeBuild:
+ state.internalWidget = gtk_stack_switcher_new()
+
+ hooks stack:
+ (build, update):
+ state.updateChild(state.stack, widget.valStack, gtk_stack_switcher_set_stack)
+
+ adder add:
+ if widget.hasStack:
+ raise newException(ValueError, "It is not possible to add multiple Stacks to a StackSwitcher.")
+ widget.hasStack = true
+ widget.valStack = child
+
+renderable StackSidebar of BaseWidget:
+ stack: Widget
+
+ hooks:
+ beforeBuild:
+ state.internalWidget = gtk_stack_sidebar_new()
+
+ hooks stack:
+ (build, update):
+ state.updateChild(state.stack, widget.valStack, gtk_stack_sidebar_set_stack)
+
+ adder add:
+ if widget.hasStack:
+ raise newException(ValueError, "It is not possible to add multiple Stacks to a StackSidebar.")
+ widget.hasStack = true
+ widget.valStack = child
+
# See TODO at comment of PixbufObj regarding why we wrap GtkMediaStream with MediaStreamObj
type
MediaStreamObj = object
@@ -4336,4 +4603,5 @@ export EditableLabel
export PasswordEntry
export CenterBox
export ListView
-export ActionBar
\ No newline at end of file
+export ActionBar
+export Stack, StackPage, StackSwitcher, StackSidebar
\ No newline at end of file