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 Search Entry Widget + + Stack + Stack + Text View 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