-
Notifications
You must be signed in to change notification settings - Fork 282
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add ContextMenu to Notification Status Bar item #1211
Changes from all commits
a3b2218
62ad7c3
d10d360
d425b63
cce3f18
ed0a405
4484b20
b785a17
37637ed
3bc5690
b59409d
460eb7a
8735d1c
77f00ba
9f8d779
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 = { | ||
glennsl marked this conversation as resolved.
Show resolved
Hide resolved
|
||
x: int, | ||
y: int, | ||
orientation: ([ | `Top | `Middle | `Bottom], [ | `Left | `Middle | `Right]), | ||
glennsl marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}; | ||
|
||
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), | ||
), | ||
Comment on lines
+144
to
+150
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It works! But it's also kind of broken. The alpha value of the color doesn't seem to be respected, and there seems to be some built-in offset that has to be countered by specifying negative offsets. I managed to tone it down a bit by adding a negative spread, but it still looks a bit off. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Hooray! 🎉
Ya, unfortunately there are bugs and quirks with the current implementation. Skia will give us a more robust set of primitives for stuff like this! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the pointer! Looks like fixing it so the alpha value is taken into account shouldn't be too hard, just pass in a vec4 instead of a vec3 and multiply the alpha value with the blur value. But the offset bug seems trickier. I can't se where that comes from. That's also less of an issue I think though, so it can definitely wait for Skia. |
||
]; | ||
}; | ||
|
||
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); | ||
}); | ||
}; | ||
}; |
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); | ||
}; | ||
}; |
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 | ||
)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IconTheme
currently lives inModel
. I'm not sure ifCore
is the right place for it, so for now we'll have to do without.