Skip to main content

Activity Bar with Dockable Sidebar in Pharo & Spec2

ยท 4 min read
Oleksandr Zaitsev
CS Researcher at UMR SENS, CIRAD

In this post I introduce a modern activity bar plus dockable sidebar for Cormas and explain why this pattern improves navigation, extensibility, and focus during modelling and simulation. I discuss the design goals behind adopting this interface and how it supports future plugins. Finally, I propose a concrete implementation strategy in Pharo using the Spec2 UI framework.

What is it and why do we want it?โ€‹

The Activity Bar plus Dockable Sidebar is a modern interface pattern where a vertical strip of icons acts as a navigation rail that lets users switch between different views, while a collapsible sidebar provides a flexible container that can appear, disappear, or change depending on the selected context. This approach scales well for complex applications because it keeps the interface compact, supports modular plugins, and allows users to quickly toggle tools without opening new windows or cluttering the main workspace.

Here is an example image generated by AI. It demonstrates how the sidebar can be used for different kinds of menus, value settings or logs and outputs.

Example generated by AI

The Activity Bar plus Dockable Sidebar works well for Cormas because it keeps the simulation at the center while letting users switch quickly between settings, parameters, analysis tools, etc. without opening many windows. It also supports a plugin oriented architecture, since new Cormas features can add their own views through icons without changing the main interface. This makes the UI cleaner, more scalable, and easier to use during modelling and participatory sessions.

Implementation in Spec2โ€‹

Let's implement this sidebar in Spec2. The way it is used can be similar to SpNotebookPresenter. Users create a notebook (sidebar) and add pages to it with custom presenters. Each page can have an icon and a label (can be displayed as help when hovering over the icon) as well as a presenter provider - a block which can be evaluated to the instance of the presenter that should be displayed in a dockable sidebar. We can reuse SpNotebookPagePresenter to represent the sidebar pages.

Activity Barโ€‹

The activity bar will be composed of toggle buttons.

SpPresenter << #CMActivityBar
slots: { #buttons . #toggleButtons };
package: 'Cormas-UI'
CMActivityBar
initializePresenters
buttons := OrderedCollection new.
toggleButtons := OrderedCollection new
CMActivityBar
addButtonIcon: anIcon help: aString action: aBlock
buttons add: (self newButton
icon: anIcon;
help: aString;
action: aBlock;
yourself)
CMActivityBar
addToggleButtonIcon: anIcon help: aString whenActivatedDo: anActivatedBlock whenDeactivatedDo: aDeactivatedBlock

| newButton |

newButton := self newToggleButton
icon: anIcon;
help: aString;
whenActivatedDo: anActivatedBlock;
whenDeactivatedDo: aDeactivatedBlock;
yourself.

toggleButtons add: newButton.

toggleButtons size > 1 ifTrue: [
toggleButtons first associatedToggleButtons: toggleButtons allButFirst ]
CMActivityBar
defaultLayout

| buttonsLayout |
buttonsLayout := SpBoxLayout newTopToBottom.

toggleButtons do: [ :button |
buttonsLayout add: button height: self class activityButtonHeight ].

buttonsLayout add: self newNullPresenter.

buttons do: [ :button |
buttonsLayout add: button height: self class activityButtonHeight ].

^ buttonsLayout
CMActivityBar class
activityButtonHeight
^ 40
CMActivityBar class
example

| activityBar |
activityBar := self new.

activityBar
addToggleButtonIcon: (activityBar iconNamed: #package)
help: 'Packages'
whenActivatedDo: [ self inform: 'Packages displayed' ]
whenDeactivatedDo: [ self inform: 'Packages hidden' ].

activityBar
addToggleButtonIcon: (activityBar iconNamed: #class)
help: 'Classes'
whenActivatedDo: [ self inform: 'Classes displayed' ]
whenDeactivatedDo: [ self inform: 'Classes hidden' ].

activityBar
addButtonIcon: (activityBar iconNamed: #configuration)
help: 'Settings'
action: [ self inform: 'Open settings!' ].

activityBar open

Dockable Sidebarโ€‹

SpPresenter << #CMDockableSidebar
slots: { #activityBar . #sidebar . #pages };
package: 'Cormas-UI'
CMDockableSidebar
initializePresenters
activityBar := self instantiate: CMActivityBar
CMDockableSidebar
layoutWithSidebarHidden
^ SpBoxLayout newLeftToRight
add: activityBar width: activityBar class activityButtonHeight;
yourself
CMDockableSidebar
layoutWithSidebarShown
^ SpBoxLayout newLeftToRight
add: activityBar width: activityBar class activityButtonHeight;

"This is a hack. We have to use a null presenter instead of spacing
because otherwise the parent layout can't calculate size correctly
when using expand: false"
add: self newNullPresenter width: 5;

add: sidebar width: 250;
yourself
CMDockableSidebar
defaultLayout
^ self layoutWithSidebarHidden
CMDockableSidebar
hideSidebar
self layout: self layoutWithSidebarHidden.
owner layout: owner layout

showSidebar
self layout: self layoutWithSidebarShown.
owner layout: owner layout
CMDockableSidebar
addPage: aPage

activityBar
addToggleButtonIcon: aPage icon
help: aPage title
whenActivatedDo: [
sidebar := aPage retrievePresenter.
self showSidebar ]
whenDeactivatedDo: [ self hideSidebar ]

And a method to add a button which simply delegates to the activity bar.

CMDockableSidebar
addButtonIcon: anIcon help: aString action: aBlock

activityBar addButtonIcon: anIcon help: aString action: aBlock

Usage Exampleโ€‹

SpPresenter << #CMDockableSidebarExample
slots: { #sidebar . #mainPresenter };
package: 'Cormas-UI'