{-# LANGUAGE OverloadedStrings #-}
module NanoUI.Widgets.Menu
( contextMenu
, contextMenuArea
, useContextMenu
, menuButton
, menuButton'
, MenuItem (..)
, menuItemWith
, menuItem
, menuItem'
, menuItemShortcut
, menuItemDisabled
, menuSeparator
, menuHeader
)
where
import Control.Monad (void, when)
import Data.IntMap.Strict qualified as IM
import Data.Text (Text)
import Effectful (Eff, type (:>))
import NanoUI.Context (Context (..), getStore, intKey, modifyStore)
import NanoUI.Font (menuItemPadX, menuItemRowH, menuMinW, menuOuterPad, menuSepH, widgetContentInset)
import NanoUI.Input (inputMousePos, inputMouseReleased)
import NanoUI.Monad (Ui, askContext, askDefaultLayout, askInput, nextId, uiIO)
import NanoUI.Store (WidgetStore (..), slotKey, Slot (..))
import NanoUI.Style (Layout (..), Padding (..), defaultLayout, fillW, fixedH, fontMuted, gap, minW, padXY, tight)
import NanoUI.Types (PopupAnchor (..), PopupPlacement (..), V2 (..))
import NanoUI.WidgetText (buttonFlagMenu, buttonFlagMenuBar)
import NanoUI.Widgets.Combinators (buttonStyled)
import NanoUI.Widgets.Layout (columnWith, labelEx, rowWith, separator)
import NanoUI.Layout.Arena (NodeType (..))
import NanoUI.Widgets.Node (HasResponse, Response (..), containerResponse, respClicked, respHovered, respRightClicked)
import NanoUI.Widgets.Popup (PopupConfig (..), popup)
contextMenu ::
(Ui :> es, HasResponse r) =>
r ->
Eff es a ->
Eff es (Maybe a)
r
target Eff es a
child = do
menu <- Eff es (Bool, V2, V2 -> Eff es (), Eff es ())
forall (es :: [Effect]).
(Ui :> es) =>
Eff es (Bool, V2, V2 -> Eff es (), Eff es ())
useContextMenu
runContextMenu menu (respRightClicked target) (const child)
contextMenuArea ::
Ui :> es =>
(Layout -> Layout) ->
Eff es a ->
(V2 -> Eff es b) ->
Eff es (a, Maybe b)
Layout -> Layout
f Eff es a
areaContent V2 -> Eff es b
menuContent = do
menu <- Eff es (Bool, V2, V2 -> Eff es (), Eff es ())
forall (es :: [Effect]).
(Ui :> es) =>
Eff es (Bool, V2, V2 -> Eff es (), Eff es ())
useContextMenu
base <- askDefaultLayout
(areaRes, areaResp) <- containerResponse NodeContainer (f base) areaContent
(,) areaRes <$> runContextMenu menu (respRightClicked areaResp) menuContent
runContextMenu ::
Ui :> es =>
(Bool, V2, V2 -> Eff es (), Eff es ()) ->
Bool ->
(V2 -> Eff es a) ->
Eff es (Maybe a)
(Bool
isOpen0, V2
pos0, V2 -> Eff es ()
openAt, Eff es ()
close) Bool
rightClick V2 -> Eff es a
child = do
inp <- Eff es Input
forall (es :: [Effect]). (Ui :> es) => Eff es Input
askInput
let mouse = Input -> V2
inputMousePos Input
inp
pos = if Bool
rightClick then V2
mouse else V2
pos0
cfg =
PopupConfig
{ cfgAnchor :: PopupAnchor
cfgAnchor = V2 -> PopupAnchor
AnchorPoint V2
pos
, cfgPlacement :: PopupPlacement
cfgPlacement = PopupPlacement
PlacementAtCursor
, cfgDismissable :: Bool
cfgDismissable = Bool
True
, cfgOffset :: Float
cfgOffset = Float
0
}
when rightClick (openAt mouse)
(popupResp, mBody) <- popup (isOpen0 || rightClick) cfg (columnWith (tight . gap 0) (child pos))
let picked = Response -> Bool
forall r. HasResponse r => r -> Bool
respHovered Response
popupResp Bool -> Bool -> Bool
&& Input -> Bool
inputMouseReleased Input
inp
when (respClicked popupResp || picked) close
pure mBody
useContextMenu ::
Ui :> es =>
Eff es (Bool, V2, V2 -> Eff es (), Eff es ())
= do
wid <- Eff es WidgetId
forall (es :: [Effect]). (Ui :> es) => Eff es WidgetId
nextId
ctx <- askContext
let key = WidgetId -> Int
intKey WidgetId
wid
openK = Slot -> Int -> Int
slotKey Slot
SlotMenuOpen Int
key
posK = Slot -> Int -> Int
slotKey Slot
SlotMenuPos Int
key
store <- uiIO (getStore ctx)
let isOpen = Int -> Int -> IntMap Int -> Int
forall a. a -> Int -> IntMap a -> a
IM.findWithDefault Int
0 Int
openK (WidgetStore -> IntMap Int
storeInt WidgetStore
store) Int -> Int -> Bool
forall a. Eq a => a -> a -> Bool
/= Int
0
(px, py) = IM.findWithDefault (0, 0) posK (storePoint store)
openAt (V2 Float
x Float
y) =
IO () -> Eff es ()
forall (es :: [Effect]) a. (Ui :> es) => IO a -> Eff es a
uiIO (IO () -> Eff es ()) -> IO () -> Eff es ()
forall a b. (a -> b) -> a -> b
$
Context -> (WidgetStore -> WidgetStore) -> IO ()
modifyStore Context
ctx ((WidgetStore -> WidgetStore) -> IO ())
-> (WidgetStore -> WidgetStore) -> IO ()
forall a b. (a -> b) -> a -> b
$ \WidgetStore
st ->
WidgetStore
st
{ storeInt = IM.insert openK 1 (storeInt st)
, storePoint = IM.insert posK (x, y) (storePoint st)
}
close = IO () -> Eff es ()
forall (es :: [Effect]) a. (Ui :> es) => IO a -> Eff es a
uiIO (IO () -> Eff es ()) -> IO () -> Eff es ()
forall a b. (a -> b) -> a -> b
$ Context -> (WidgetStore -> WidgetStore) -> IO ()
modifyStore Context
ctx ((WidgetStore -> WidgetStore) -> IO ())
-> (WidgetStore -> WidgetStore) -> IO ()
forall a b. (a -> b) -> a -> b
$ \WidgetStore
st -> WidgetStore
st {storeInt = IM.delete openK (storeInt st)}
pure (isOpen, V2 px py, openAt, close)
data =
{ :: !Text
, :: !(Maybe Text)
, :: !Bool
}
deriving (MenuItem -> MenuItem -> Bool
(MenuItem -> MenuItem -> Bool)
-> (MenuItem -> MenuItem -> Bool) -> Eq MenuItem
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: MenuItem -> MenuItem -> Bool
== :: MenuItem -> MenuItem -> Bool
$c/= :: MenuItem -> MenuItem -> Bool
/= :: MenuItem -> MenuItem -> Bool
Eq, Int -> MenuItem -> ShowS
[MenuItem] -> ShowS
MenuItem -> String
(Int -> MenuItem -> ShowS)
-> (MenuItem -> String) -> ([MenuItem] -> ShowS) -> Show MenuItem
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> MenuItem -> ShowS
showsPrec :: Int -> MenuItem -> ShowS
$cshow :: MenuItem -> String
show :: MenuItem -> String
$cshowList :: [MenuItem] -> ShowS
showList :: [MenuItem] -> ShowS
Show)
menuItemWith :: Ui :> es => MenuItem -> Eff es Response
(MenuItem Text
lbl Maybe Text
hint Bool
enabled)
| Bool
enabled = Text -> Float -> Layout -> Int -> Eff es Response
forall (es :: [Effect]).
(Ui :> es) =>
Text -> Float -> Layout -> Int -> Eff es Response
buttonStyled Text
text Float
0 Layout
menuRowLayout Int
buttonFlagMenu
| Bool
otherwise = do
ctx <- Eff es Context
forall (es :: [Effect]). (Ui :> es) => Eff es Context
askContext
let (ix, _) = widgetContentInset (ctxFontMetrics ctx)
padLeft = Float
menuItemPadX Float -> Float -> Float
forall a. Num a => a -> a -> a
+ Float
ix
padRight = Float -> Float -> Float
forall a. Ord a => a -> a -> a
max Float
0 (Float
2 Float -> Float -> Float
forall a. Num a => a -> a -> a
* (Float
menuOuterPad Float -> Float -> Float
forall a. Num a => a -> a -> a
+ Float
menuItemPadX) Float -> Float -> Float
forall a. Num a => a -> a -> a
- Float
padLeft)
rowLayout = (Float -> Layout -> Layout
minW Float
menuMinW Layout
defaultLayout) {layoutPadding = Padding padLeft padRight 0 0}
(_, resp) <-
containerResponse NodeContainer rowLayout $
labelEx (fixedH menuItemRowH . tight . fontMuted $ defaultLayout) text
pure
resp
{ rawRespHovered = False
, rawRespPressed = False
, rawRespClicked = False
, rawRespRightPressed = False
, rawRespRightClicked = False
}
where
text :: Text
text = Text -> (Text -> Text) -> Maybe Text -> Text
forall b a. b -> (a -> b) -> Maybe a -> b
maybe Text
lbl (\Text
s -> [Text] -> Text
forall a. Monoid a => [a] -> a
mconcat [Text
lbl, Text
" ", Text
s]) Maybe Text
hint
{-# INLINE menuItem #-}
menuItem :: Ui :> es => Text -> Eff es Bool
Text
txt = Response -> Bool
forall r. HasResponse r => r -> Bool
respClicked (Response -> Bool) -> Eff es Response -> Eff es Bool
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Text -> Eff es Response
forall (es :: [Effect]). (Ui :> es) => Text -> Eff es Response
menuItem' Text
txt
{-# INLINE menuItem' #-}
menuItem' :: Ui :> es => Text -> Eff es Response
Text
txt = MenuItem -> Eff es Response
forall (es :: [Effect]). (Ui :> es) => MenuItem -> Eff es Response
menuItemWith (Text -> Maybe Text -> Bool -> MenuItem
MenuItem Text
txt Maybe Text
forall a. Maybe a
Nothing Bool
True)
menuItemShortcut :: Ui :> es => Text -> Text -> Eff es Bool
Text
txt Text
hint = Response -> Bool
forall r. HasResponse r => r -> Bool
respClicked (Response -> Bool) -> Eff es Response -> Eff es Bool
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> MenuItem -> Eff es Response
forall (es :: [Effect]). (Ui :> es) => MenuItem -> Eff es Response
menuItemWith (Text -> Maybe Text -> Bool -> MenuItem
MenuItem Text
txt (Text -> Maybe Text
forall a. a -> Maybe a
Just Text
hint) Bool
True)
menuItemDisabled :: Ui :> es => Text -> Eff es ()
Text
txt = Eff es Response -> Eff es ()
forall (f :: * -> *) a. Functor f => f a -> f ()
void (MenuItem -> Eff es Response
forall (es :: [Effect]). (Ui :> es) => MenuItem -> Eff es Response
menuItemWith (Text -> Maybe Text -> Bool -> MenuItem
MenuItem Text
txt Maybe Text
forall a. Maybe a
Nothing Bool
False))
menuRowLayout :: Layout
= Float -> Layout -> Layout
minW Float
menuMinW (Layout -> Layout) -> (Layout -> Layout) -> Layout -> Layout
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Float -> Layout -> Layout
fixedH Float
menuItemRowH (Layout -> Layout) -> (Layout -> Layout) -> Layout -> Layout
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Layout -> Layout
tight (Layout -> Layout) -> (Layout -> Layout) -> Layout -> Layout
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Layout -> Layout
fillW (Layout -> Layout) -> Layout -> Layout
forall a b. (a -> b) -> a -> b
$ Layout
defaultLayout
{-# INLINE menuButton #-}
menuButton :: Ui :> es => Text -> Bool -> Eff es Bool
Text
txt Bool
open = Response -> Bool
forall r. HasResponse r => r -> Bool
respClicked (Response -> Bool) -> Eff es Response -> Eff es Bool
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Text -> Bool -> Eff es Response
forall (es :: [Effect]).
(Ui :> es) =>
Text -> Bool -> Eff es Response
menuButton' Text
txt Bool
open
menuButton' :: Ui :> es => Text -> Bool -> Eff es Response
Text
txt Bool
open =
Text -> Float -> Layout -> Int -> Eff es Response
forall (es :: [Effect]).
(Ui :> es) =>
Text -> Float -> Layout -> Int -> Eff es Response
buttonStyled Text
txt (if Bool
open then Float
1 else Float
0) Layout
menuBarTitleLayout Int
buttonFlagMenuBar
menuBarTitleLayout :: Layout
= Layout -> Layout
tight (Layout -> Layout) -> Layout -> Layout
forall a b. (a -> b) -> a -> b
$ Layout
defaultLayout
menuSeparator :: Ui :> es => Eff es ()
= do
(Layout -> Layout) -> Eff es () -> Eff es ()
forall (es :: [Effect]) a.
(Ui :> es) =>
(Layout -> Layout) -> Eff es a -> Eff es a
rowWith (Float -> Layout -> Layout
fixedH Float
menuSepH (Layout -> Layout) -> (Layout -> Layout) -> Layout -> Layout
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Float -> Float -> Layout -> Layout
padXY (Float
menuItemPadX Float -> Float -> Float
forall a. Num a => a -> a -> a
- Float
menuOuterPad) Float
4.5 (Layout -> Layout) -> (Layout -> Layout) -> Layout -> Layout
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Layout -> Layout
fillW) (Eff es () -> Eff es ()) -> Eff es () -> Eff es ()
forall a b. (a -> b) -> a -> b
$
(Layout -> Layout) -> Eff es () -> Eff es ()
forall (es :: [Effect]) a.
(Ui :> es) =>
(Layout -> Layout) -> Eff es a -> Eff es a
columnWith (Layout -> Layout
tight (Layout -> Layout) -> (Layout -> Layout) -> Layout -> Layout
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Layout -> Layout
fillW) Eff es ()
forall (es :: [Effect]). (Ui :> es) => Eff es ()
separator
menuHeader :: Ui :> es => Text -> Eff es ()
Text
txt =
Eff es Response -> Eff es ()
forall (f :: * -> *) a. Functor f => f a -> f ()
void (Layout -> Text -> Eff es Response
forall (es :: [Effect]).
(Ui :> es) =>
Layout -> Text -> Eff es Response
labelEx (Float -> Float -> Layout -> Layout
padXY Float
6 Float
2 Layout
defaultLayout) Text
txt)