Tutorial

Let's build a simple Todo app using owlkettle. The source code for this example can be found here. Here is what it will look like when it is done.

Todo Application

First we model the application state. In this case we need to store a list of todo items (todos) where each item has a text and can be marked as done. We also store the current value of the entry.

type TodoItem = object
  text: string
  done: bool

viewable App:
  todos: seq[TodoItem]
  newItem: string

Next we need to define the view method of the application. For now, an empty window with the title "Todo" is created.

method view(app: AppState): Widget =
  result = gui:
    Window:
      title = "Todo"

To make development easier, we initialize the app with two placeholder todo items.

brew(gui(App(todos = @[
  TodoItem(text: "First Item", done: true),
  TodoItem(text: "Second Item")
])))

Here is what our application looks like currently:

An empty window

Let's show the placeholder todo items in a list. We use a ListBox which contains a Label for each item. A ScrolledWindow is used to add a scrollbar to the ListBox.

When defining GUIs, we can use structured control flow constructs such as for loops and if statements. In this case a for loop is used to create a label for each item. When app.todos changes, the GUI is updated automatically.

Window:
  title = "Todo"
  
  Box(orient = OrientY, spacing = 6, margin = 12):
    Frame:
      ScrolledWindow:
        ListBox:
          selectionMode = SelectionNone
          for it, todo in app.todos:
            Label:
              text = todo.text
              xAlign = 0
The application displays the placeholder items

In order to allow the user to mark items as done, we add a CheckButton next to each label. The changed event handler is called when the user toggles the CheckButton. In this case, we update the current state of the TodoItem.

 ...
 ListBox:
   selectionMode = SelectionNone
   for it, todo in app.todos:
+    Box:
+      spacing = 6
+      CheckButton {.expand: false.}:
+        state = todo.done
+        proc changed(state: bool) =
+          app.todos[it].done = state
       Label:
         text = todo.text
         xAlign = 0
Items can be marked as done

Next, we add an entry which allows the user to add new items to the todo list. The expand attribute of the Box which contains the entry and button is set to false in order to prevent the Box from growing to take up remaining space in the parent widget.

Window:
  ...
  Box(orient = OrientY, spacing = 6, margin = 12):
    Box(orient = OrientX, spacing = 6) {.expand: false.}:
      Entry:
        text = app.newItem
        proc changed(newItem: string) =
          app.newItem = newItem
      Button {.expand: false.}:
        icon = "list-add-symbolic"
        style = [ButtonSuggested]
        proc clicked() =
          app.todos.add(TodoItem(text: app.newItem))
          app.newItem = ""
    
    Frame:
      ScrolledWindow:
        ...
Todo application with an entry to add new items

Finally we add a HeaderBar and a menu which contains a button used to delete all checked items.

Window:
  ...
  HeaderBar {.addTitlebar.}:
    MenuButton {.addRight.}:
      icon = "open-menu-symbolic"
      Popover:
        Box(orient=OrientY, spacing=6, margin=6):
          Button:
            icon = "user-trash-symbolic"
            style = [ButtonDestructive]
            proc clicked() =
              app.todos = app.todos.filterIt(not it.done)
  Box:
    ...

Clicking on the MenuButton opens the menu.

Todo Application

The source code for this example can be found here.

Next Steps

  • Improve the Todo App
    • Add an option to hide completed items
    • Replace the "Delete" button with a ModelButton and create a simple menu structure. Check out the popover_menu example for how to create a simple menu structure.
    • Use owlkettle/adw to convert the Todo app to a libadwaita application.
    • Save and load todos from a file
  • Check out the other examples
  • Start working on your own projects! You might find the following links useful: