Bar.purs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  1. module PureTabs.Sidebar.Bar where
  2. import Browser.Tabs (Tab(..), TabId)
  3. import Browser.Utils (eqBy, sortByKeyIndex)
  4. import Control.Bind (bind, discard, map, void, (<#>), (>>=))
  5. import Data.Array ((:))
  6. import Data.Array as A
  7. import Data.Array.NonEmpty (NonEmptyArray)
  8. import Data.Array.NonEmpty as NonEmptyArray
  9. import Data.Eq ((/=))
  10. import Data.Foldable (for_)
  11. import Data.Function (($))
  12. import Data.Map as M
  13. import Data.Maybe (Maybe(..), fromMaybe, fromMaybe', maybe)
  14. import Data.MediaType.Common (textPlain)
  15. import Data.Number (fromString)
  16. import Data.Set (Set, toUnfoldable) as S
  17. import Data.Set.NonEmpty (cons, max) as NES
  18. import Data.Symbol (SProxy(..))
  19. import Data.Traversable (sequence, traverse)
  20. import Data.Tuple (Tuple(..))
  21. import Data.Tuple as T
  22. import Data.Unit (Unit, unit)
  23. import Effect.Aff.Class (class MonadAff)
  24. import Effect.Class (class MonadEffect, liftEffect)
  25. import Effect.Console (log)
  26. import Halogen as H
  27. import Halogen.HTML as HH
  28. import Halogen.HTML.Events as HE
  29. import Halogen.HTML.Properties as HP
  30. import Prelude (flip, pure, show, (#), (&&), (+), (-), (<$>), (<<<), (<>), (==), (>), (>>>))
  31. import PureTabs.Model.Group (GroupId(..))
  32. import PureTabs.Model.GroupMapping (GroupData(..))
  33. import PureTabs.Model.SidebarEvent (SidebarEvent(..))
  34. import PureTabs.Model.TabWithGroup (TabWithGroup(..))
  35. import PureTabs.Sidebar.Component.GroupName as GroupName
  36. import PureTabs.Sidebar.Component.TopMenu as TopMenu
  37. import PureTabs.Sidebar.Tabs (Output(..))
  38. import PureTabs.Sidebar.Tabs as Tabs
  39. import Sidebar.Utils (moveElem, whenC)
  40. import Web.HTML.Event.DataTransfer as DT
  41. import Web.HTML.Event.DragEvent as DE
  42. -- TODO: correctly use `pos` when adding or deleting a group (i.e. making sure
  43. -- the pos are contiguous from 0 to #groups - 1)
  44. type Group
  45. = { name :: String
  46. , pos :: Int
  47. }
  48. type State
  49. = { groups :: M.Map GroupId Group
  50. , tabsToGroup :: M.Map TabId GroupId
  51. , groupTabsPositions :: Array (Tuple TabId GroupId)
  52. , currentGroup :: GroupId
  53. , draggedCurrentGroup :: Maybe GroupId
  54. }
  55. data Action
  56. = UserSelectedGroup GroupId
  57. | UserRenameGroup GroupId String
  58. | UserCreatedGroup
  59. | UserChangedDeletion Boolean
  60. | UserDeletedGroup GroupId
  61. | HandleTabsOutput GroupId Tabs.Output
  62. | GroupNameDragOver DE.DragEvent GroupId
  63. | DragEnd DE.DragEvent
  64. data Query a
  65. = TabsQuery (Tabs.Query a)
  66. | InitialTabsWithGroup (Array GroupData) (Array TabWithGroup) a
  67. | InitializeGroups (Array GroupData) a
  68. | TabCreated Tab (Maybe GroupId) a
  69. | GroupDeleted GroupId (Maybe TabId) a
  70. initialGroup :: M.Map GroupId Group
  71. initialGroup = M.fromFoldable [ Tuple (GroupId 0) { name: "main", pos: 0 } ]
  72. initialState :: forall i. i -> State
  73. initialState _ =
  74. { groups: initialGroup
  75. , tabsToGroup: M.empty
  76. , groupTabsPositions : []
  77. , currentGroup: GroupId 0
  78. , draggedCurrentGroup: Nothing
  79. }
  80. type Slots
  81. = ( tabs :: Tabs.Slot GroupId
  82. , groupName :: GroupName.Slot GroupId
  83. , topMenu :: TopMenu.Slot Unit)
  84. _tabs :: SProxy "tabs"
  85. _tabs = (SProxy :: _ "tabs")
  86. _groupName :: SProxy "groupName"
  87. _groupName = (SProxy :: _ "groupName")
  88. _topMenu :: SProxy "topMenu"
  89. _topMenu = (SProxy :: _ "topMenu")
  90. component :: forall i m. MonadEffect m => MonadAff m => H.Component HH.HTML Query i SidebarEvent m
  91. component =
  92. H.mkComponent
  93. { initialState
  94. , render: render
  95. , eval:
  96. H.mkEval
  97. $ H.defaultEval
  98. { handleQuery = handleQuery
  99. , handleAction = handleAction
  100. }
  101. }
  102. where
  103. render :: State -> H.ComponentHTML Action Slots m
  104. render state =
  105. let
  106. currentGroupShown = fromMaybe state.currentGroup state.draggedCurrentGroup
  107. topMenu = HH.slot _topMenu unit TopMenu.component unit (
  108. Just <<< case _ of
  109. TopMenu.CreateGroup -> UserCreatedGroup
  110. TopMenu.ChangedDeletion value -> UserChangedDeletion value
  111. )
  112. -- TODO: order groups by `pos`
  113. barListGroup = HH.div [ HP.id_ "bar-list" ] [HH.ul [ HP.id_ "bar-list-group"] $
  114. (M.toUnfoldable state.groups) <#> \(Tuple gid g) -> renderGroup gid (gid == currentGroupShown) g
  115. ]
  116. tabsDivs = (S.toUnfoldable $ (M.keys state.groups)) <#>
  117. (\gid -> HH.div [
  118. HP.classes [(H.ClassName "bar-tabs"), whenC (gid == currentGroupShown) (H.ClassName "bar-tabs-active")]
  119. ] [renderGroupTabs gid])
  120. in
  121. HH.div [ HP.id_ "bar", HE.onDragEnd \evt -> Just $ DragEnd evt ] $ topMenu : barListGroup : tabsDivs
  122. renderGroupTabs :: GroupId -> H.ComponentHTML Action Slots m
  123. renderGroupTabs groupId = HH.slot _tabs groupId Tabs.component unit (Just <<< (HandleTabsOutput groupId))
  124. renderGroup :: GroupId -> Boolean -> Group -> H.ComponentHTML Action Slots m
  125. renderGroup groupId isActive group =
  126. HH.li [
  127. HP.classes [(H.ClassName "group-name"), whenC isActive (H.ClassName "active-group")]
  128. , HE.onClick (\_ -> Just (UserSelectedGroup groupId))
  129. , HE.onDragOver \evt -> Just $ GroupNameDragOver evt groupId
  130. ] [ HH.slot _groupName groupId GroupName.component group.name
  131. case _ of
  132. GroupName.NewName newName -> Just (UserRenameGroup groupId newName)
  133. GroupName.DeleteGroup -> Just (UserDeletedGroup groupId)
  134. ]
  135. handleAction :: forall m. MonadEffect m => Action -> H.HalogenM State Action Slots SidebarEvent m Unit
  136. handleAction =
  137. case _ of
  138. UserSelectedGroup gid -> do
  139. H.modify_ _ { currentGroup = gid }
  140. UserRenameGroup gid newName -> do
  141. H.modify_ \s -> s { groups = M.update (\g -> Just $ g { name = newName }) gid s.groups }
  142. H.raise $ SbRenamedGroup gid newName
  143. UserCreatedGroup -> do
  144. s <- H.get
  145. let Tuple gid newGroup = createGroup Nothing s
  146. H.modify_ $ insertGroup gid newGroup
  147. H.raise $ SbCreatedGroup gid newGroup.name
  148. UserChangedDeletion value -> void $ H.queryAll _groupName $ H.tell $ GroupName.DeletionEnabled value
  149. UserDeletedGroup gid -> do
  150. s <- H.get
  151. if M.size s.groups > 1 then
  152. H.raise $ SbDeletedGroup gid $ getTabIdsOfGroup gid s.tabsToGroup
  153. else
  154. void $ H.query _groupName gid $ H.tell $ GroupName.TriedToDeleteLastGroup
  155. GroupNameDragOver dragEvent gid -> do
  156. let
  157. dataTransfer = DE.dataTransfer dragEvent
  158. dragData <- H.liftEffect $ DT.getData textPlain dataTransfer
  159. case fromString dragData of
  160. Nothing -> H.liftEffect $ log $ "sb: group drag over, got something else than a number: " <> dragData
  161. Just tid -> do
  162. H.modify_ _ { draggedCurrentGroup = Just gid }
  163. H.liftEffect $ log $ "sb: dragging " <> (show tid) <> " over " <> (show gid)
  164. DragEnd evt -> do
  165. H.modify_ _ { draggedCurrentGroup = Nothing }
  166. H.liftEffect $ log $ "sb: drag end from bar component"
  167. HandleTabsOutput gid output ->
  168. case output of
  169. OutputTabDragEnd tid' -> do
  170. s <- H.get
  171. case Tuple tid' s.draggedCurrentGroup of
  172. -- Only perform a move when we're dragging a tab onto a different group
  173. Tuple (Just tid) (Just draggedGroup) | s.currentGroup /= draggedGroup ->
  174. moveTabToGroup tid gid draggedGroup s
  175. _ -> pure unit
  176. H.modify_ _ { draggedCurrentGroup = Nothing }
  177. TabsSidebarAction (SbMoveTab tid groupIndex) -> sidebarMoveTab tid gid groupIndex
  178. TabsSidebarAction sbEvent -> H.raise sbEvent
  179. where
  180. moveTabToGroup
  181. :: MonadEffect m => TabId
  182. -> GroupId
  183. -> GroupId
  184. -> State
  185. -> H.HalogenM State Action Slots SidebarEvent m Unit
  186. moveTabToGroup tid fromGroup toGroup state = do
  187. let
  188. -- XXX: The goal is to put it at the end, but if you:
  189. -- - create a new group
  190. -- - drag a tab from the first one to it
  191. -- - drag it back to the first group
  192. -- Then it will be at the beginning of the group, not the end.
  193. -- Right now we only put it at the end of the list.
  194. -- We don't support dragging at a specific place.
  195. newTabIndex =
  196. fromMaybe (A.length state.groupTabsPositions)
  197. $ lastWinTabIndexInGroup toGroup state.groupTabsPositions
  198. s <- H.modify \s ->
  199. s { tabsToGroup = M.update (\_ -> Just toGroup) tid s.tabsToGroup
  200. , groupTabsPositions =
  201. s.groupTabsPositions
  202. <#>
  203. (\(Tuple tid' gid') -> if tid' == tid then Tuple tid' toGroup else Tuple tid' gid')
  204. -- Reassign the current group directly here to avoid flickering
  205. , currentGroup = toGroup
  206. }
  207. let newIndexInGroup = getPositionTabInGroup newTabIndex toGroup s.groupTabsPositions
  208. deletedTab' <- H.query _tabs fromGroup $ H.request $ Tabs.TabDeleted tid
  209. case deletedTab' of
  210. Just (Just (Tab tab)) ->
  211. void $ H.query _tabs toGroup $ H.tell
  212. $ Tabs.TabCreated $ Tab (tab { index = newIndexInGroup })
  213. _ -> pure unit
  214. H.raise $ SbMoveTab tid newTabIndex
  215. H.raise $ SbActivateTab tid
  216. H.raise $ SbChangeTabGroup tid (Just toGroup)
  217. sidebarMoveTab
  218. :: TabId
  219. -> GroupId
  220. -> Int
  221. -> H.HalogenM State Action Slots SidebarEvent m Unit
  222. sidebarMoveTab tid gid groupIndex = do
  223. s <- H.get
  224. let
  225. oldPosition = getPositionTab tid gid s.groupTabsPositions
  226. newIndex = do
  227. prevIdx <- oldPosition
  228. s.groupTabsPositions #
  229. A.mapWithIndex (Tuple)
  230. >>> A.filter (\(Tuple _ (Tuple _ gid')) -> gid' == gid)
  231. >>> (flip A.index) groupIndex
  232. >>> map T.fst
  233. -- Important: we ask Firefox to do the move, but we don't
  234. -- perform it ourselves. This means we don't update the state.
  235. -- We will get back a TabMoved event that will then be
  236. -- processed accordingly.
  237. newIndex # maybe (pure unit) \idx -> H.raise $ SbMoveTab tid idx
  238. handleQuery :: forall a m. MonadEffect m => Query a -> H.HalogenM State Action Slots SidebarEvent m (Maybe a)
  239. handleQuery = case _ of
  240. TabsQuery q -> handleTabsQuery q
  241. InitializeGroups groups a -> do
  242. let newGroups = M.fromFoldable $
  243. A.mapWithIndex (\idx (GroupData g) -> Tuple g.groupId { name: g.name, pos: idx}) groups
  244. -- TODO: re-assign existing tabs to the new groups.
  245. H.modify_ \s ->
  246. if newGroups == s.groups then
  247. s
  248. else
  249. s { groups = newGroups }
  250. pure (Just a)
  251. InitialTabsWithGroup groups tabs a -> do
  252. -- Assign the tabs to their group and save the tabs positions
  253. s <- H.modify \s ->
  254. let
  255. newGroups =
  256. case groups of
  257. [] -> initialGroup
  258. newGroups' ->
  259. M.fromFoldable $
  260. A.mapWithIndex
  261. (\idx (GroupData g) -> Tuple g.groupId { name: g.name, pos: idx})
  262. newGroups'
  263. existingGroups = M.keys newGroups
  264. tabIdGroup = tabs <#> \(TabWithGroup (Tab t) gid) -> Tuple t.id gid
  265. in
  266. s { groups = newGroups, tabsToGroup = M.fromFoldable tabIdGroup, groupTabsPositions = tabIdGroup }
  267. -- Update the browser state to re-assign correctly all the tabs
  268. let
  269. (groupsTupled :: Array (Tuple TabId GroupId)) = M.toUnfoldableUnordered s.tabsToGroup
  270. setGroups = groupsTupled <#>
  271. (\(Tuple tid gid) -> H.raise $ SbChangeTabGroup tid (Just gid))
  272. void $ sequence setGroups
  273. -- Initialize each child tabs component with its tabs
  274. let
  275. tabsGroups = tabs <#> \(TabWithGroup tab@(Tab t) _) -> Tuple tab $ fromMaybe s.currentGroup (M.lookup t.id s.tabsToGroup)
  276. groupedTabs = A.groupBy (eqBy T.snd) (sortByKeyIndex T.snd tabsGroups)
  277. void $ traverse initializeGroup groupedTabs
  278. -- Activate the right tab and its group
  279. let activatedTab = tabsGroups # A.head <<< A.filter (\(Tuple (Tab t) _) -> t.active)
  280. activatedTab # maybe (pure unit) \(Tuple (Tab t) gid) -> do
  281. void $ tellChild gid $ Tabs.TabActivated Nothing t.id
  282. handleAction $ UserSelectedGroup gid
  283. pure (Just a)
  284. where
  285. initializeGroup :: forall act. NonEmptyArray (Tuple Tab GroupId) -> H.HalogenM State act Slots SidebarEvent m Unit
  286. initializeGroup groupedTabs =
  287. let
  288. gid = T.snd $ NonEmptyArray.head groupedTabs
  289. in
  290. void $ tellChild gid $ Tabs.InitialTabList $ A.fromFoldable $ T.fst <$> groupedTabs
  291. TabCreated (Tab tab) groupId a -> do
  292. liftEffect $ log $ "[sb] created tab " <> (show tab.id)
  293. s <- H.get
  294. let tabGroupId = fromMaybe s.currentGroup groupId
  295. newGroupTabsPositions =
  296. fromMaybe s.groupTabsPositions
  297. $ A.insertAt tab.index (Tuple tab.id tabGroupId) s.groupTabsPositions
  298. inGroupPosition = getPositionTabInGroup tab.index tabGroupId newGroupTabsPositions
  299. newTab = Tab $ tab { index = inGroupPosition }
  300. newS <- H.modify \state ->
  301. state
  302. { tabsToGroup = M.insert tab.id tabGroupId s.tabsToGroup
  303. , groupTabsPositions = newGroupTabsPositions
  304. }
  305. void $ tellChild tabGroupId $ Tabs.TabCreated newTab
  306. H.raise $ SbChangeTabGroup tab.id (Just tabGroupId)
  307. -- XXX: Temporary fix because Background.onTabCreated launches an async
  308. -- computation to create a tab instead of doing it synchronously, which
  309. -- makes the tab activation trigger *before* the tab creation.
  310. if tab.active then
  311. void $ handleTabsQuery $ Tabs.TabActivated Nothing tab.id Nothing
  312. else
  313. pure unit
  314. pure (Just a)
  315. GroupDeleted gid currentTid a -> do
  316. H.modify_ \s ->
  317. let
  318. currentGroup = fromMaybe s.currentGroup $ currentTid >>= (flip M.lookup s.tabsToGroup)
  319. in
  320. s { groups = M.delete gid s.groups, currentGroup = currentGroup }
  321. pure $ Just a
  322. handleTabsQuery :: forall act a m. MonadEffect m => Tabs.Query a -> H.HalogenM State act Slots SidebarEvent m (Maybe a)
  323. handleTabsQuery = case _ of
  324. Tabs.InitialTabList tabs a -> pure $ Just a
  325. -- TODO: log an error, this shouldn't happen
  326. Tabs.TabCreated tab a -> pure $ Just a
  327. Tabs.TabDeleted tid reply -> do
  328. doOnTabGroup tid \gid -> do
  329. H.modify_ (\s -> s
  330. { tabsToGroup = M.delete tid s.tabsToGroup
  331. , groupTabsPositions = A.deleteBy
  332. -- This is ugly. There is no function to delete the
  333. -- first element of an array that matches a condition.
  334. (\(Tuple tid1 _) (Tuple tid2 _) -> tid1 == tid2)
  335. (Tuple tid s.currentGroup)
  336. s.groupTabsPositions
  337. })
  338. void $ H.query _tabs gid $ H.request $ Tabs.TabDeleted tid
  339. pure (Just (reply Nothing))
  340. Tabs.TabActivated prevTid' tid a -> do
  341. liftEffect $ log $ "[sb] activated tab " <> (show tid) <> " from " <> (show prevTid')
  342. for_ prevTid' \prevTid ->
  343. doOnTabGroup prevTid \gid ->
  344. void $ tellChild gid $ Tabs.TabActivated prevTid' tid
  345. doOnTabGroup tid \gid -> do
  346. { tabsToGroup } <- H.modify (_ { currentGroup = gid})
  347. liftEffect $ log $ "[sb] group of " <> (show tid) <> " is " <> (show gid)
  348. H.raise $ SbSelectedGroup $ getTabIdsOfGroup gid tabsToGroup
  349. void $ tellChild gid $ Tabs.TabActivated prevTid' tid
  350. pure (Just a)
  351. Tabs.TabMoved tid next a -> do
  352. doOnTabGroup tid \gid -> do
  353. { groupTabsPositions } <- H.get
  354. let
  355. newGroupTabsPositions = fromMaybe groupTabsPositions $ do
  356. prevPosition <- getPositionTab tid gid groupTabsPositions
  357. moveElem prevPosition next groupTabsPositions
  358. nextGroupPosition = getPositionTabInGroup next gid newGroupTabsPositions
  359. H.modify_ (_ { groupTabsPositions = newGroupTabsPositions })
  360. void $ tellChild gid $ Tabs.TabMoved tid nextGroupPosition
  361. pure (Just a)
  362. Tabs.TabInfoChanged tid cinfo a -> do
  363. doOnTabGroup tid \gid -> do
  364. void $ tellChild gid $ Tabs.TabInfoChanged tid cinfo
  365. pure (Just a)
  366. Tabs.TabDetached tid a -> do
  367. handleTabsQuery $ Tabs.TabDeleted tid \_ -> a
  368. Tabs.TabAttached tab a -> do
  369. handleTabsQuery $ Tabs.TabCreated tab a
  370. where
  371. doOnTabGroup
  372. :: TabId
  373. -> (GroupId -> H.HalogenM State act Slots SidebarEvent m Unit)
  374. -> H.HalogenM State act Slots SidebarEvent m Unit
  375. doOnTabGroup tabId f = do
  376. { tabsToGroup } <- H.get
  377. case M.lookup tabId tabsToGroup of
  378. Just groupId -> f groupId
  379. Nothing -> pure unit
  380. tellChild :: forall act m. GroupId -> (H.Tell Tabs.Query) -> H.HalogenM State act Slots SidebarEvent m (Maybe Unit)
  381. tellChild gid q = H.query _tabs gid $ H.tell q
  382. getPositionTabInGroup
  383. :: Int
  384. -> GroupId
  385. -> Array (Tuple TabId GroupId)
  386. -> Int
  387. getPositionTabInGroup index gid =
  388. (A.take $ index + 1)
  389. >>> (A.filter \(Tuple _ gid') -> gid' == gid)
  390. >>> A.length
  391. >>> (flip (-) $ 1)
  392. getPositionTab
  393. :: TabId
  394. -> GroupId
  395. -> Array (Tuple TabId GroupId)
  396. -> Maybe Int
  397. getPositionTab tid gid arr = A.findIndex (\(Tuple tid' gid') -> tid' == tid && gid' == gid) arr
  398. getTabIdsOfGroup
  399. :: GroupId
  400. -> M.Map TabId GroupId
  401. -> Array TabId
  402. getTabIdsOfGroup gid =
  403. M.toUnfoldable
  404. >>> A.filter (\(Tuple tid gid') -> gid' == gid)
  405. >>> map T.fst
  406. -- | Obtain the window index of the last tab of a group.
  407. lastWinTabIndexInGroup
  408. :: GroupId
  409. -> Array (Tuple TabId GroupId)
  410. -> Maybe Int
  411. lastWinTabIndexInGroup gid =
  412. A.mapWithIndex (Tuple)
  413. >>> A.filter (T.snd >>> T.snd >>> (==) gid)
  414. >>> map T.fst
  415. >>> A.last
  416. findNextGroupId :: S.Set GroupId -> GroupId
  417. findNextGroupId values =
  418. let GroupId(maxValue) = NES.max (NES.cons (GroupId 0) values)
  419. in GroupId(maxValue + 1)
  420. createGroup :: (Maybe GroupId) -> State -> Tuple GroupId Group
  421. createGroup mGid s =
  422. let
  423. gid = fromMaybe' (\_ -> findNextGroupId $ M.keys s.groups) mGid
  424. in
  425. Tuple gid { name: "new group", pos: M.size s.groups }
  426. insertGroup :: GroupId -> Group -> State -> State
  427. insertGroup gid group s = s { groups = M.insert gid group s.groups }