Skip to content

Commit

Permalink
Add ContextMenu to Notification Status Bar item (#1211)
Browse files Browse the repository at this point in the history
* add Components project

* add ContextMenu

* use functor to generate identity instead of Identity component

* wire up context menu

* add context menu to notification status abr item

* fix warning

* add menu item focus

* extract placement from ContextMenu.t

* ContextMenu -> ContextMenuUpdated

* post-rebase fix

* originX/Y -> orientation

* add offsetX/Y

* adjust origin for orientation

* add box shadow

* use default mouse cursor on overlay
  • Loading branch information
glennsl authored Jan 22, 2020
1 parent 71c3794 commit 39fc1b5
Show file tree
Hide file tree
Showing 11 changed files with 479 additions and 12 deletions.
274 changes: 274 additions & 0 deletions src/Components/ContextMenu.re
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
open Oni_Core;

open Revery;
open Revery.UI;
open Revery.UI.Components;

module Option = Utility.Option;

module Constants = {
let menuWidth = 200;
// let maxMenuHeight = 600;
};

// TYPES

module Id: {
type t;
let create: unit => t;
} = {
type t = int;

let lastId = ref(0);
let create = () => {
incr(lastId);
lastId^;
};
};

[@deriving show({with_path: false})]
type item('data) = {
label: string,
// icon: option(IconTheme.IconDefinition.t),
data: [@opaque] 'data,
};

type placement = {
x: int,
y: int,
orientation: ([ | `Top | `Middle | `Bottom], [ | `Left | `Middle | `Right]),
};

type t('data) = {
id: Id.t,
placement: option(placement),
items: list(item('data)),
};

// MENUITEM

module MenuItem = {
module Constants = {
let fontSize = 12;
};

module Styles = {
open Style;

let bg = (~theme: Theme.t, ~isFocused) =>
isFocused ? theme.menuSelectionBackground : theme.menuBackground;

let container = (~theme, ~isFocused) => [
padding(10),
flexDirection(`Row),
backgroundColor(bg(~theme, ~isFocused)),
];

// let icon = fgColor => [
// fontFamily("seti.ttf"),
// fontSize(Constants.fontSize),
// marginRight(10),
// color(fgColor),
// ];

let label = (~font: UiFont.t, ~theme: Theme.t, ~isFocused) => [
fontFamily(font.fontFile),
textOverflow(`Ellipsis),
fontSize(Constants.fontSize),
color(theme.menuForeground),
backgroundColor(bg(~theme, ~isFocused)),
];
};

let component = React.Expert.component("MenuItem");
let make:
'data.
(
~item: item('data),
~theme: Theme.t,
~font: UiFont.t,
~onClick: unit => unit,
unit
) =>
_
=
(~item, ~theme, ~font, ~onClick, ()) =>
component(hooks => {
let ((isFocused, setIsFocused), hooks) = Hooks.state(false, hooks);

// let iconView =
// switch (item.icon) {
// | Some(icon) =>
// IconTheme.IconDefinition.(
// <Text
// style={Styles.icon(icon.fontColor)}
// text={FontIcon.codeToIcon(icon.fontCharacter)}
// />
// )

// | None => <Text style={Styles.icon(Colors.transparentWhite)} text="" />
// };

let labelView = {
let style = Styles.label(~font, ~theme, ~isFocused);
<Text style text={item.label} />;
};

(
<Clickable onClick>
<View
style={Styles.container(~theme, ~isFocused)}
onMouseOut={_ => setIsFocused(_ => false)}
onMouseOver={_ => setIsFocused(_ => true)}>
// iconView
labelView </View>
</Clickable>,
hooks,
);
});
};

// MENU

module Menu = {
module Styles = {
open Style;

let container = (~x, ~y, ~theme: Theme.t) => [
position(`Absolute),
top(y),
left(x),
backgroundColor(theme.menuBackground),
color(theme.menuForeground),
width(Constants.menuWidth),
boxShadow(
~xOffset=-5.,
~yOffset=-5.,
~blurRadius=25.,
~spreadRadius=-10.,
~color=Color.rgba(0., 0., 0., 0.0001),
),
];
};

let component = React.Expert.component("Menu");
let make = (~items, ~placement, ~theme, ~font, ~onItemSelect, ()) =>
component(hooks => {
let ((maybeRef, setRef), hooks) = Hooks.state(None, hooks);
let {x, y, orientation: (orientY, orientX)} = placement;

let height =
switch (maybeRef) {
| Some((node: node)) => node#measurements().height
| None => List.length(items) * 20
};
let width = Constants.menuWidth;

let x =
switch (orientX) {
| `Left => x
| `Middle => x - width / 2
| `Right => x - width
};

let y =
switch (orientY) {
| `Top => y - height
| `Middle => y - height / 2
| `Bottom => y
};

(
<View
style={Styles.container(~x, ~y, ~theme)}
ref={node => setRef(_ => Some(node))}>
{items
|> List.map(item => {
let onClick = () => onItemSelect(item);
<MenuItem item theme font onClick />;
})
|> React.listToElement}
</View>,
hooks,
);
});
};

// OVERLAY

module Overlay = {
module Styles = {
open Style;

let overlay = [
position(`Absolute),
top(0),
bottom(0),
left(0),
right(0),
pointerEvents(`Allow),
cursor(MouseCursors.arrow),
];
};

let make = (~model, ~theme, ~font, ~onOverlayClick, ~onItemSelect, ()) =>
switch (model) {
| {items, placement: Some(placement), _} =>
<Clickable onClick=onOverlayClick style=Styles.overlay>
<Menu items placement theme font onItemSelect />
</Clickable>
| _ => React.empty
};
};

module Make = (()) => {
let id = Id.create();

let init = items => {id, placement: None, items};

module Anchor = {
let component = React.Expert.component("Anchor");
let make =
(
~model as maybeModel,
~orientation=(`Bottom, `Left),
~offsetX=0,
~offsetY=0,
~onUpdate,
(),
) =>
component(hooks => {
let ((maybeRef, setRef), hooks) = Hooks.ref(None, hooks);

switch (maybeModel, maybeRef) {
| (Some(model), Some(node)) =>
if (model.id == id) {
let (x, y, width, _) =
Math.BoundingBox2d.getBounds(node#getBoundingBox());

let x =
switch (orientation) {
| (_, `Left) => x
| (_, `Middle) => x -. width /. 2.
| (_, `Right) => x -. width
};

let placement =
Some({
x: int_of_float(x) + offsetX,
y: int_of_float(y) + offsetY,
orientation,
});

if (model.placement != placement) {
onUpdate({...model, placement});
};
}

| _ => ()
};

(<View ref={node => setRef(Some(node))} />, hooks);
});
};
};
48 changes: 48 additions & 0 deletions src/Components/ContextMenu.rei
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
open Oni_Core;

open Revery.UI;

[@deriving show]
type item('data) = {
label: string,
// icon: option(IconTheme.IconDefinition.t),
data: [@opaque] 'data,
};

type t('data);

module Overlay: {
let make:
(
~model: t('data),
~theme: Theme.t,
~font: UiFont.t,
~onOverlayClick: unit => unit,
~onItemSelect: item('data) => unit,
unit
) =>
React.element(React.node);
};

module Make:
() =>
{
let init: list(item('data)) => t('data);

module Anchor: {
let make:
(
~model: option(t('data)),
~orientation: (
[ | `Top | `Middle | `Bottom],
[ | `Left | `Middle | `Right],
)
=?,
~offsetX: int=?,
~offsetY: int=?,
~onUpdate: t('data) => unit,
unit
) =>
React.element(React.node);
};
};
14 changes: 12 additions & 2 deletions src/Components/dune
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
(library
(name Oni_Components)
(public_name Oni2.components)
(libraries editor-core-types Oni2.core Revery)
(preprocess (pps ppx_deriving_yojson ppx_deriving.show brisk-reconciler.ppx)))
(preprocess (pps brisk-reconciler.ppx ppx_deriving.show))
(libraries
str
bigarray
zed_oni
lwt
lwt.unix
Oni2.core
Rench
Revery
editor-core-types
))
5 changes: 5 additions & 0 deletions src/Model/Actions.re
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ open Oni_Input;
open Oni_Syntax;

module Ext = Oni_Extensions;
module ContextMenu = Oni_Components.ContextMenu;

[@deriving show({with_path: false})]
type t =
Expand Down Expand Up @@ -55,6 +56,9 @@ type t =
| TextInput([@opaque] Revery.Events.textInputEvent)
| HoverShow
| ChangeMode([@opaque] Vim.Mode.t)
| ContextMenuUpdated([@opaque] ContextMenu.t(t))
| ContextMenuOverlayClicked
| ContextMenuItemSelected(ContextMenu.item(t))
| DiagnosticsHotKey
| DiagnosticsSet(Uri.t, string, [@opaque] list(Diagnostic.t))
| DiagnosticsClear(string)
Expand All @@ -80,6 +84,7 @@ type t =
| EditorScrollToColumn(EditorId.t, int)
| ShowNotification(Notification.t)
| HideNotification(Notification.t)
| ClearNotifications
| FileExplorer(FileExplorer.action)
| LanguageFeature(LanguageFeatures.action)
| QuickmenuShow(quickmenuVariant)
Expand Down
4 changes: 4 additions & 0 deletions src/Model/Notifications.re
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ open Notification;

type t = list(Notification.t);

module ContextMenu =
Oni_Components.ContextMenu.Make({});

let initial: t = [];

let reduce = (state, action: Actions.t) => {
switch (action) {
| ShowNotification(item) => [item, ...state]
| HideNotification(item) => List.filter(it => it.id != item.id, state)
| ClearNotifications => initial
| _ => state
};
};
Expand Down
Loading

0 comments on commit 39fc1b5

Please sign in to comment.