KUMOS/Client/Includes/Desktop.sbi
2026-05-02 15:49:06 +02:00

603 lines
18 KiB
Plaintext

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