Skip to content

Commit

Permalink
Add top, main and bottom area
Browse files Browse the repository at this point in the history
  • Loading branch information
trungleduc committed Aug 3, 2023
1 parent 2303d75 commit 1529405
Show file tree
Hide file tree
Showing 2 changed files with 224 additions and 17 deletions.
233 changes: 216 additions & 17 deletions packages/voila/src/shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
* *
* The full license is in the file LICENSE, distributed with this software. *
****************************************************************************/

import { JupyterFrontEnd } from '@jupyterlab/application';

import { DocumentRegistry } from '@jupyterlab/docregistry';

import { BoxLayout, Widget } from '@lumino/widgets';
import { ArrayExt } from '@lumino/algorithm';
import { IMessageHandler, Message, MessageLoop } from '@lumino/messaging';
import { Debouncer } from '@lumino/polling';
import { Signal } from '@lumino/signaling';
import { BoxLayout, BoxPanel, Panel, Widget } from '@lumino/widgets';

export type IShell = VoilaShell;

Expand All @@ -22,9 +23,27 @@ export namespace IShell {
/**
* The areas of the application shell where widgets can reside.
*/
export type Area = 'top' | 'bottom' | 'main';
export type Area =
| 'main'
| 'header'
| 'top'
| 'menu'
| 'left'
| 'right'
| 'bottom'
| 'down';
}

/**
* The class name added to AppShell instances.
*/
const APPLICATION_SHELL_CLASS = 'jp-LabShell';

/**
* The default rank of items added to a sidebar.
*/
const DEFAULT_RANK = 900;

/**
* The application shell.
*/
Expand All @@ -34,9 +53,39 @@ export class VoilaShell extends Widget implements JupyterFrontEnd.IShell {
this.id = 'main';
const rootLayout = new BoxLayout();
rootLayout.alignment = 'start';
rootLayout.spacing = 0;
this.addClass(APPLICATION_SHELL_CLASS);

const topHandler = (this._topHandler = new Private.PanelHandler());
topHandler.panel.id = 'voila-top-panel';
topHandler.panel.node.setAttribute('role', 'banner');
BoxLayout.setStretch(topHandler.panel, 0);
topHandler.panel.hide();
rootLayout.addWidget(topHandler.panel);

const hboxPanel = (this._mainPanel = new BoxPanel());
hboxPanel.id = 'jp-main-content-panel';
hboxPanel.direction = 'top-to-bottom';
BoxLayout.setStretch(hboxPanel, 1);
rootLayout.addWidget(hboxPanel);

const bottomPanel = (this._bottomPanel = new Panel());
bottomPanel.node.setAttribute('role', 'contentinfo');
bottomPanel.id = 'voila-bottom-panel';
BoxLayout.setStretch(bottomPanel, 0);
rootLayout.addWidget(bottomPanel);
bottomPanel.hide();

this.layout = rootLayout;
}

/**
* The current widget in the shell's main area.
*/
get currentWidget(): Widget | null {
return this._mainPanel.widgets[0];
}

activateById(id: string): void {
// no-op
}
Expand All @@ -57,31 +106,181 @@ export class VoilaShell extends Widget implements JupyterFrontEnd.IShell {
): void {
switch (area) {
case 'top':
Widget.attach(
widget,
this.node,
this.node.firstElementChild as HTMLElement
);
this._addToTopArea(widget, options);
break;
case 'bottom':
Widget.attach(widget, this.node);
this._addToBottomArea(widget, options);
break;
case 'main':
(this.layout as BoxLayout).addWidget(widget);
this._mainPanel.addWidget(widget);
break;
default:
console.warn(`Area ${area} is not implemented yet!`);
break;
}
}

widgets(area: IShell.Area): IterableIterator<Widget> {
switch (area) {
case 'top':
return this._topHandler.panel.children();
case 'bottom':
return this._bottomPanel.children();
case 'main':
this._mainPanel.children();
break;
default:
return [][Symbol.iterator]();
}
return [][Symbol.iterator]();
}

/**
* The current widget in the shell's main area.
* Add a widget to the top content area.
*
* #### Notes
* Widgets must have a unique `id` property, which will be used as the DOM id.
*/
get currentWidget(): Widget | null {
return null;
private _addToTopArea(
widget: Widget,
options?: DocumentRegistry.IOpenOptions
): void {
if (!widget.id) {
console.error('Widgets added to app shell must have unique id property.');
return;
}
options = options || {};
const rank = options.rank ?? DEFAULT_RANK;
this._topHandler.addWidget(widget, rank);
this._onLayoutModified();
if (this._topHandler.panel.isHidden) {
this._topHandler.panel.show();
}
}

widgets(area: IShell.Area): IterableIterator<Widget> {
return [][Symbol.iterator]();
/**
* Add a widget to the bottom content area.
*
* #### Notes
* Widgets must have a unique `id` property, which will be used as the DOM id.
*/
private _addToBottomArea(
widget: Widget,
options?: DocumentRegistry.IOpenOptions
): void {
if (!widget.id) {
console.error('Widgets added to app shell must have unique id property.');
return;
}
this._bottomPanel.addWidget(widget);
this._onLayoutModified();

if (this._bottomPanel.isHidden) {
this._bottomPanel.show();
}
}

/**
* Handle a change to the layout.
*/
private _onLayoutModified(): void {
void this._layoutDebouncer.invoke();
}

private _topHandler: Private.PanelHandler;
private _mainPanel: BoxPanel;
private _bottomPanel: Panel;
private _layoutDebouncer = new Debouncer(() => {
this._layoutModified.emit(undefined);
}, 0);
private _layoutModified = new Signal<this, void>(this);
}

namespace Private {
/**
* An object which holds a widget and its sort rank.
*/
export interface IRankItem {
/**
* The widget for the item.
*/
widget: Widget;

/**
* The sort rank of the widget.
*/
rank: number;
}

/**
* A less-than comparison function for side bar rank items.
*/
export function itemCmp(first: IRankItem, second: IRankItem): number {
return first.rank - second.rank;
}

/**
* A class which manages a panel and sorts its widgets by rank.
*/
export class PanelHandler {
constructor() {
MessageLoop.installMessageHook(this._panel, this._panelChildHook);
}

/**
* Get the panel managed by the handler.
*/
get panel(): Panel {
return this._panel;
}

/**
* Add a widget to the panel.
*
* If the widget is already added, it will be moved.
*/
addWidget(widget: Widget, rank: number): void {
widget.parent = null;
const item = { widget, rank };
const index = ArrayExt.upperBound(this._items, item, Private.itemCmp);
ArrayExt.insert(this._items, index, item);
this._panel.insertWidget(index, widget);
}

/**
* A message hook for child add/remove messages on the main area dock panel.
*/
private _panelChildHook = (
handler: IMessageHandler,
msg: Message
): boolean => {
switch (msg.type) {
case 'child-added':
{
const widget = (msg as Widget.ChildMessage).child;
// If we already know about this widget, we're done
if (this._items.find((v) => v.widget === widget)) {
break;
}

// Otherwise, add to the end by default
const rank = this._items[this._items.length - 1].rank;
this._items.push({ widget, rank });
}
break;
case 'child-removed':
{
const widget = (msg as Widget.ChildMessage).child;
ArrayExt.removeFirstWhere(this._items, (v) => v.widget === widget);
}
break;
default:
break;
}
return true;
};

private _items = new Array<Private.IRankItem>();
private _panel = new Panel();
}
}
8 changes: 8 additions & 0 deletions packages/voila/style/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ body {
div#main {
height: 100vh;
}
div#voila-top-panel {
min-height: var(--jp-private-menubar-height);
display: flex;
}
div#voila-bottom-panel {
min-height: var(--jp-private-menubar-height);
display: flex;
}
div#rendered_cells {
padding: var(--jp-notebook-padding);
overflow: auto;
Expand Down

0 comments on commit 1529405

Please sign in to comment.