Module Desktop EnableExplicit ;- Constants #Timer_Clock = 0 #Taskbar_Width = 72 #Taskbar_ItemHeight = 40 #Icon_Size = 28 #Menu_W = 240 #Menu_Rail_W = 52 #Menu_Header_H = 32 #Menu_Row_H = 44 #Menu_Icon_Size = 22 ; Hit-test return values for the start menu Enumeration #Hit_None = -1 #Hit_Settings = -2 #Hit_Logout = -3 EndEnumeration ;- Globals ; Colors Global Color_Bar_Bg = RGB( 28, 28, 40) Global Color_Bar_Active = RGB( 52, 52, 72) Global Color_Accent = RGB(100, 120, 255) Global Color_Icon = RGBA(220, 220, 220, 255) Global Color_Menu_Bg = RGB( 36, 36, 52) Global Color_Menu_Rail = RGB( 22, 22, 34) Global Color_Menu_Header = RGB( 22, 22, 34) Global Color_Menu_Hover = RGB( 55, 55, 78) Global Color_Menu_Sep = RGB( 48, 48, 65) Global Color_Menu_Text = RGB(200, 200, 215) Global Color_Menu_Dim = RGB(110, 110, 135) Global Color_Logout_Hover = RGB( 65, 30, 35) Global Color_Logout_Icon = RGBA(200, 90, 90, 255) Global IconFont = LoadFont(#PB_Any, "sans-serif", 18) Global MenuFont = LoadFont(#PB_Any, "sans-serif", 13) ; Desktop windows Global TaskBarWindow Global ClockLabel Global StartButton Global StartMenuWindow ; App registry Structure App Name.s IconImg.i Proc.i ID.s EndStructure #MaxInstalledApps = 32 Global Dim AppRegistry.App(#MaxInstalledApps) Global _AppCount Global _MenuCanvas Global _MenuHover = #Hit_None ; System shortcut icons (created in Open) Global _IconSettings Global _IconLogout ; Window manager state Structure Window Window.i Button.i Name.s Icon.i EndStructure Global _ActiveWindow Global NewList WindowManager.Window() ;- Private declarations Declare _MakeIconImage(Label.s, Size = #Icon_Size, Color = 0) Declare _MenuHeight() Declare _HitTest(MX, MY) Declare _RebuildMenu() Declare _DrawMenu() Declare _CloseMenu() Declare _FindByWindow(Win) Declare _FindByButton(Btn) Declare _DrawButton() Declare _SetActiveButton() Declare _RebuildButtons() Declare _LoadInstalledAppsCallback(Success, DataString.s) Declare Handler_Logout() Declare LogoutCallback(Success, Response.s) Declare Handler_Resize() Declare Handler_Clock() Declare Handler_StartButton() Declare Handler_StartMenu_Focus() Declare Handler_MenuCanvas() Declare Handler_TaskbarButton() Declare Handler_AppClose() Declare Handler_AppActivate() ;- Public procedures Procedure Open(Username.s) Protected Width, Height Protected *AppProc Width = DesktopWidth(0) Height = DesktopHeight(0) ; System shortcut icons — pass explicit colors at creation time _IconSettings = _MakeIconImage("⚙", #Menu_Icon_Size, Color_Logout_Icon) _IconLogout = _MakeIconImage("⏻", #Menu_Icon_Size, Color_Logout_Icon) TaskBarWindow = OpenWindow(#PB_Any, 0, 0, #Taskbar_Width, Height, "", #PB_Window_BorderLess) BindEvent(#PB_Event_ActivateWindow, @Handler_AppActivate(), TaskBarWindow) StickyWindow(TaskBarWindow, #True) SetWindowColor(TaskBarWindow, Color_Bar_Bg) StartButton = ButtonGadget(#PB_Any, 0, 0, #Taskbar_Width, #Taskbar_ItemHeight, "≡") BindGadgetEvent(StartButton, @Handler_StartButton()) ClockLabel = TextGadget(#PB_Any, 0, Height - #Taskbar_ItemHeight, #Taskbar_Width, #Taskbar_ItemHeight, "", #PB_Text_Center | #PB_Text_VerticalCenter) SetGadgetColor(ClockLabel, #PB_Gadget_FrontColor, Color_Menu_Text) Handler_Clock() AddWindowTimer(TaskBarWindow, #Timer_Clock, 1000) BindEvent(#PB_Event_Timer, @Handler_Clock(), TaskBarWindow) StartMenuWindow = OpenWindow(#PB_Any, #Taskbar_Width + 4, 0, #Menu_W, 100, "", #PB_Window_BorderLess | #PB_Window_Invisible) StickyWindow(StartMenuWindow, #True) _MenuCanvas = CanvasGadget(#PB_Any, 0, 0, #Menu_W, 100) BindGadgetEvent(_MenuCanvas, @Handler_MenuCanvas()) BindEvent(#PB_Event_SizeDesktop, @Handler_Resize()) BindEvent(#PB_Event_DeactivateWindow, @Handler_StartMenu_Focus(), StartMenuWindow) AppRuntime::Init() !p_appproc = fileexplorer$f_open; InstallApp("File Explorer", *AppProc, "📁") !p_appproc = webbrowser$f_open; InstallApp("Web Browser", *AppProc, "🌐") !p_appproc = appmanager$f_open; InstallApp("App Manager", *AppProc, "📦") Handler_Resize() HTTPRequest(#PB_HTTP_Get, "/api/apps/list", "", @_LoadInstalledAppsCallback()) EndProcedure Procedure InstallApp(AppName.s, *LaunchProc, Icon.s = "") Protected Slot Protected Label.s If _AppCount >= #MaxInstalledApps : ProcedureReturn : EndIf Slot = _AppCount Label = Icon If Label = "" : Label = Left(AppName, 2) : EndIf AppRegistry(Slot)\Name = AppName AppRegistry(Slot)\Proc = *LaunchProc AppRegistry(Slot)\IconImg = _MakeIconImage(Label, #Menu_Icon_Size) _AppCount + 1 _RebuildMenu() EndProcedure Procedure Register(AppName.s, Win, Icon.s = "") Protected Y, GadgetList, Btn Protected Label.s AddElement(WindowManager()) WindowManager()\Window = Win WindowManager()\Name = AppName Label = Icon If Label = "" : Label = Left(AppName, 2) : EndIf WindowManager()\Icon = _MakeIconImage(Label) Y = #Taskbar_ItemHeight + (ListIndex(WindowManager()) * #Taskbar_ItemHeight) GadgetList = UseGadgetList(WindowID(TaskBarWindow)) Btn = CanvasGadget(#PB_Any, 0, Y, #Taskbar_Width, #Taskbar_ItemHeight) UseGadgetList(GadgetList) WindowManager()\Button = Btn SetGadgetData(Btn, Win) SetWindowData(Win, Btn) BindGadgetEvent(Btn, @Handler_TaskbarButton()) BindEvent(#PB_Event_CloseWindow, @Handler_AppClose(), Win) BindEvent(#PB_Event_ActivateWindow, @Handler_AppActivate(), Win) _ActiveWindow = Win _SetActiveButton() EndProcedure Procedure Unregister(Win) If Not _FindByWindow(Win) : ProcedureReturn : EndIf UnbindEvent(#PB_Event_CloseWindow, @Handler_AppClose(), Win) UnbindEvent(#PB_Event_ActivateWindow, @Handler_AppActivate(), Win) UnbindGadgetEvent(WindowManager()\Button, @Handler_TaskbarButton()) FreeGadget(WindowManager()\Button) CloseWindow(Win) DeleteElement(WindowManager()) If _ActiveWindow = Win : _ActiveWindow = GetActiveWindow() : EndIf _RebuildButtons() EndProcedure Procedure InstallThirdPartyApp(AppID.s, ManifestJSON.s, Permissions.s, Icon.s = "") Protected Slot, i Protected AppName.s, Label.s, N.s Slot = -1 For i = 0 To _AppCount - 1 If AppRegistry(i)\ID = AppID : Slot = i : Break : EndIf Next If Slot < 0 If _AppCount >= #MaxInstalledApps : ProcedureReturn : EndIf Slot = _AppCount _AppCount + 1 Else If IsImage(AppRegistry(Slot)\IconImg) : FreeImage(AppRegistry(Slot)\IconImg) : EndIf EndIf AppName = AppID If ParseJSON(0, ManifestJSON) N = GetJSONString(GetJSONMember(JSONValue(0), "name")) If N <> "" : AppName = N : EndIf FreeJSON(0) EndIf AppRegistry(Slot)\Name = AppName AppRegistry(Slot)\ID = AppID !(function() { ! var _id = v_appid, _m = v_manifestjson, _p = v_permissions; ! desktop$a_AppRegistry.array[v_slot]._Proc = function() { ! appruntime$f_launch(_id, _m, _p); ! }; !})(); Label = Icon If Label = "" : Label = "📦" : EndIf AppRegistry(Slot)\IconImg = _MakeIconImage(Label, #Menu_Icon_Size) _RebuildMenu() EndProcedure Procedure UninstallThirdPartyApp(AppID.s) Protected i, j For i = 0 To _AppCount - 1 If AppRegistry(i)\ID = AppID If IsImage(AppRegistry(i)\IconImg) : FreeImage(AppRegistry(i)\IconImg) : EndIf ; Compact the registry For j = i To _AppCount - 2 AppRegistry(j)\Name = AppRegistry(j + 1)\Name AppRegistry(j)\ID = AppRegistry(j + 1)\ID AppRegistry(j)\IconImg = AppRegistry(j + 1)\IconImg AppRegistry(j)\Proc = AppRegistry(j + 1)\Proc !desktop$a_AppRegistry.array[v_j]._Proc = desktop$a_AppRegistry.array[v_j + 1]._Proc; Next AppRegistry(_AppCount - 1)\Name = "" AppRegistry(_AppCount - 1)\ID = "" AppRegistry(_AppCount - 1)\IconImg = 0 AppRegistry(_AppCount - 1)\Proc = 0 !desktop$a_AppRegistry.array[desktop$g__appcount]._Proc = 0; _AppCount - 1 _RebuildMenu() ProcedureReturn EndIf Next EndProcedure ;- Private procedures Procedure _MakeIconImage(Label.s, Size = #Icon_Size, Color = 0) Protected Img Img = CreateImage(#PB_Any, Size, Size, 32, RGBA(0, 0, 0, 0)) If Not IsImage(Img) : ProcedureReturn 0 : EndIf If StartVectorDrawing(ImageVectorOutput(Img)) VectorFont(IconFont, Size * 0.65) If Color = 0 VectorSourceColor(Color_Icon) Else VectorSourceColor(Color) EndIf ;MovePathCursor((Size - VectorTextWidth(Label)) / 2, (Size - VectorTextHeight(Label)) / 2) ;In 3.20 DrawVectorText() draws vertically centered? MovePathCursor((Size - VectorTextWidth(Label)) / 2, Size - (VectorTextHeight(Label) / 2)) DrawVectorText(Label) StopVectorDrawing() EndIf ProcedureReturn Img EndProcedure Procedure _MenuHeight() Protected RightH = #Menu_Header_H + _AppCount * #Menu_Row_H + 8 Protected MinH = 2 * #Menu_Row_H + 16 ; always room for both rail shortcuts If RightH < MinH : ProcedureReturn MinH : EndIf ProcedureReturn RightH EndProcedure ; Returns app index (>=0), #Hit_Settings, #Hit_Logout, or #Hit_None. Procedure _HitTest(MX, MY) Protected H, LogoutY, SettingsY, RY H = _MenuHeight() If MX < #Menu_Rail_W ; Left rail — system shortcuts are bottom-anchored LogoutY = H - #Menu_Row_H SettingsY = H - 2 * #Menu_Row_H If MY >= SettingsY And MY < LogoutY ProcedureReturn #Hit_Settings ElseIf MY >= LogoutY And MY < H ProcedureReturn #Hit_Logout EndIf Else ; Right panel — app rows below header RY = MY - #Menu_Header_H If RY >= 0 And RY < _AppCount * #Menu_Row_H ProcedureReturn RY / #Menu_Row_H EndIf EndIf ProcedureReturn #Hit_None EndProcedure Procedure _RebuildMenu() Protected H = _MenuHeight() If IsWindow(StartMenuWindow) : ResizeWindow(StartMenuWindow, #PB_Ignore, #PB_Ignore, #Menu_W, H) : EndIf If IsGadget(_MenuCanvas) : ResizeGadget(_MenuCanvas, 0, 0, #Menu_W, H) : EndIf _DrawMenu() EndProcedure Procedure _DrawMenu() If Not IsGadget(_MenuCanvas) : ProcedureReturn : EndIf If Not StartDrawing(CanvasOutput(_MenuCanvas)) : ProcedureReturn : EndIf Protected W = OutputWidth() Protected H = OutputHeight() Protected TH, IX, IY, i, Y ; ── Left rail ──────────────────────────────────────────────────────── Box(0, 0, #Menu_Rail_W, H, Color_Menu_Rail) Protected SettingsY = H - 2 * #Menu_Row_H Protected LogoutY = H - #Menu_Row_H If _MenuHover = #Hit_Settings Box(0, SettingsY, #Menu_Rail_W, #Menu_Row_H, Color_Menu_Hover) EndIf If _MenuHover = #Hit_Logout Box(0, LogoutY, #Menu_Rail_W, #Menu_Row_H, Color_Logout_Hover) EndIf If IsImage(_IconSettings) IX = (#Menu_Rail_W - #Menu_Icon_Size) / 2 IY = SettingsY + (#Menu_Row_H - #Menu_Icon_Size) / 2 DrawAlphaImage(ImageID(_IconSettings), IX, IY) EndIf If IsImage(_IconLogout) IX = (#Menu_Rail_W - #Menu_Icon_Size) / 2 IY = LogoutY + (#Menu_Row_H - #Menu_Icon_Size) / 2 DrawAlphaImage(ImageID(_IconLogout), IX, IY) EndIf ; Separator between rail and right panel Box(#Menu_Rail_W, 0, 1, H, Color_Menu_Sep) ; ── Right panel ────────────────────────────────────────────────────── Box(#Menu_Rail_W + 1, 0, W - #Menu_Rail_W - 1, H, Color_Menu_Bg) ; Header Box(#Menu_Rail_W + 1, 0, W - #Menu_Rail_W - 1, #Menu_Header_H, Color_Menu_Header) DrawingMode(#PB_2DDrawing_Transparent) If IsFont(MenuFont) : DrawingFont(FontID(MenuFont)) : EndIf TH = TextHeight("A") DrawText(#Menu_Rail_W + 14, (#Menu_Header_H - TH) / 2, "Apps", Color_Menu_Dim) ; App rows Y = #Menu_Header_H For i = 0 To _AppCount - 1 If _MenuHover = i DrawingMode(#PB_2DDrawing_Default) Box(#Menu_Rail_W + 1, Y, W - #Menu_Rail_W - 1, #Menu_Row_H, Color_Menu_Hover) EndIf If IsImage(AppRegistry(i)\IconImg) IX = #Menu_Rail_W + 12 IY = Y + (#Menu_Row_H - #Menu_Icon_Size) / 2 DrawAlphaImage(ImageID(AppRegistry(i)\IconImg), IX, IY) EndIf DrawingMode(#PB_2DDrawing_Transparent) TH = TextHeight("A") DrawText(#Menu_Rail_W + 12 + #Menu_Icon_Size + 8, Y + (#Menu_Row_H - TH) / 2, AppRegistry(i)\Name, Color_Menu_Text) Y + #Menu_Row_H Next StopDrawing() EndProcedure Procedure _CloseMenu() HideWindow(StartMenuWindow, #True) _MenuHover = #Hit_None _DrawMenu() EndProcedure Procedure _FindByWindow(Win) ForEach WindowManager() If WindowManager()\Window = Win : ProcedureReturn #True : EndIf Next EndProcedure Procedure _FindByButton(Btn) ForEach WindowManager() If WindowManager()\Button = Btn : ProcedureReturn #True : EndIf Next EndProcedure Procedure _DrawButton() Protected Active, W, H Active = Bool(WindowManager()\Window = _ActiveWindow And _ActiveWindow <> 0) If Not IsGadget(WindowManager()\Button) : ProcedureReturn : EndIf If StartDrawing(CanvasOutput(WindowManager()\Button)) W = OutputWidth() H = OutputHeight() If Active Box(0, 0, W, H, Color_Bar_Active) Box(0, H / 4, 3, H / 2, Color_Accent) Else Box(0, 0, W, H, Color_Bar_Bg) EndIf If IsImage(WindowManager()\Icon) DrawAlphaImage(ImageID(WindowManager()\Icon), (W - #Icon_Size) / 2, (H - #Icon_Size) / 2) EndIf StopDrawing() EndIf EndProcedure Procedure _SetActiveButton() ForEach WindowManager() _DrawButton() Next EndProcedure Procedure _RebuildButtons() ForEach WindowManager() ResizeGadget(WindowManager()\Button, 0, #Taskbar_ItemHeight + (ListIndex(WindowManager()) * #Taskbar_ItemHeight), #Taskbar_Width, #Taskbar_ItemHeight) _DrawButton() Next EndProcedure Procedure _LoadInstalledAppsCallback(Success, DataString.s) Protected Root, Total, Item, ManiNode, PermNode, PermCount, i, j Protected AppID.s, Icon.s, Perms.s, ManiStr.s If Not Success Or Not ParseJSON(0, DataString) : ProcedureReturn : EndIf Root = JSONValue(0) Total = JSONArraySize(Root) For i = 0 To Total - 1 Item = GetJSONElement(Root, i) ManiNode = GetJSONMember(Item, "manifest") AppID = GetJSONString(GetJSONMember(Item, "app_id")) Icon = GetJSONString(GetJSONMember(ManiNode, "icon")) PermNode = GetJSONMember(Item, "permissions") PermCount = JSONArraySize(PermNode) Perms = "[" For j = 0 To PermCount - 1 If j > 0 : Perms + "," : EndIf Perms + ~"\"" + GetJSONString(GetJSONElement(PermNode, j)) + ~"\"" Next Perms + "]" ; Re-serialise the manifest object to a JSON string for InstallThirdPartyApp ManiStr = "{" + ~"\"id\":\"" + GetJSONString(GetJSONMember(ManiNode, "id")) + ~"\"," + ~"\"name\":\"" + GetJSONString(GetJSONMember(ManiNode, "name")) + ~"\"," + ~"\"version\":\"" + GetJSONString(GetJSONMember(ManiNode, "version")) + ~"\"," + ~"\"icon\":\"" + ReplaceString(Icon, ~"\"", ~"\\\"") + ~"\"," + ~"\"entry\":\"" + GetJSONString(GetJSONMember(ManiNode, "entry")) + ~"\"}" InstallThirdPartyApp(AppID, ManiStr, Perms, Icon) Next FreeJSON(0) EndProcedure ;- Event handlers Procedure Handler_Resize() Protected Width = DesktopWidth(0) Protected Height = DesktopHeight(0) ResizeWindow(TaskBarWindow, 0, 0, #Taskbar_Width, Height) ResizeGadget(ClockLabel, 0, Height - #Taskbar_ItemHeight, #Taskbar_Width, #Taskbar_ItemHeight) EndProcedure Procedure Handler_Clock() SetGadgetText(ClockLabel, FormatDate("%hh:%ii:%ss", Date())) EndProcedure Procedure Handler_StartButton() HideWindow(StartMenuWindow, #False) SetActiveWindow(StartMenuWindow) EndProcedure Procedure Handler_StartMenu_Focus() _CloseMenu() EndProcedure Procedure Handler_Logout() HTTPRequest(#PB_HTTP_Post, "/api/auth/logout", "", @LogoutCallback()) EndProcedure Procedure LogoutCallback(Success, Response.s) !location.reload() EndProcedure Procedure Handler_MenuCanvas() Protected EType, MX, MY, Hit EType = EventType() MX = GetGadgetAttribute(_MenuCanvas, #PB_Canvas_MouseX) MY = GetGadgetAttribute(_MenuCanvas, #PB_Canvas_MouseY) Hit = _HitTest(MX, MY) Select EType Case #PB_EventType_MouseMove If Hit <> _MenuHover _MenuHover = Hit _DrawMenu() EndIf Case #PB_EventType_MouseLeave If _MenuHover <> #Hit_None _MenuHover = #Hit_None _DrawMenu() EndIf Case #PB_EventType_LeftButtonUp Select Hit Case #Hit_Settings _CloseMenu() !settings$f_open(); Case #Hit_Logout _CloseMenu() Handler_Logout() Default If Hit >= 0 And Hit < _AppCount And AppRegistry(Hit)\Proc <> 0 _CloseMenu() !desktop$a_AppRegistry.array[v_hit]._Proc(); EndIf EndSelect EndSelect EndProcedure Procedure Handler_TaskbarButton() Protected Btn, Win If EventType() <> #PB_EventType_LeftButtonUp : ProcedureReturn : EndIf Btn = EventGadget() Win = GetGadgetData(Btn) If Not IsWindow(Win) : ProcedureReturn : EndIf If _ActiveWindow = Win HideWindow(Win, #True) _ActiveWindow = 0 Else HideWindow(Win, #False) SetActiveWindow(Win) _ActiveWindow = Win EndIf _SetActiveButton() EndProcedure Procedure Handler_AppClose() Protected Win = EventWindow() If Not _FindByWindow(Win) : ProcedureReturn : EndIf UnbindEvent(#PB_Event_CloseWindow, @Handler_AppClose(), Win) UnbindEvent(#PB_Event_ActivateWindow, @Handler_AppActivate(), Win) UnbindGadgetEvent(WindowManager()\Button, @Handler_TaskbarButton()) FreeGadget(WindowManager()\Button) DeleteElement(WindowManager()) If _ActiveWindow = Win : _ActiveWindow = GetActiveWindow() : EndIf _RebuildButtons() CloseWindow(Win) EndProcedure Procedure Handler_AppActivate() Protected Win = EventWindow() If Win = TaskBarWindow : ProcedureReturn : EndIf _ActiveWindow = Win _SetActiveButton() EndProcedure EndModule ; IDE Options = SpiderBasic 3.20 (Windows - x86) ; CursorPosition = 95 ; FirstLine = 82 ; Folding = BAAAg ; iOSAppOrientation = 0 ; AndroidAppCode = 0 ; AndroidAppOrientation = 0 ; EnableXP ; DPIAware ; CompileSourceDirectory