From 72e4fa080a2152e55333030beecd42807e4bad46 Mon Sep 17 00:00:00 2001 From: Daniel Hammer Date: Sun, 10 May 2026 10:45:05 +0200 Subject: [PATCH] feat: screenshot app improvement, WIndow Server supports full-screen overlay, Disks app UI improvements --- kernel/src/Api/Syscall.cpp | 4 +- kernel/src/Api/Syscall.hpp | 4 + kernel/src/Api/WinServer.cpp | 12 + kernel/src/Api/WinServer.hpp | 2 + kernel/src/Api/Window.hpp | 6 +- programs/GNUmakefile | 4 +- programs/include/Api/Syscall.hpp | 4 + programs/include/gui/standalone.hpp | 5 + programs/include/gui/window.hpp | 1 + programs/include/libc/montauk.h | 8 + programs/include/montauk/syscall.h | 3 + .../obj/src/libprintersapplet.o | Bin 17392 -> 17560 bytes .../obj/src/libtimezonesapplet.o | Bin 17528 -> 17704 bytes programs/src/desktop/compose.cpp | 175 +-- programs/src/desktop/desktop_internal.hpp | 10 + programs/src/desktop/input.cpp | 243 ++-- programs/src/desktop/main.cpp | 27 + programs/src/desktop/window.cpp | 71 +- programs/src/disks/disks.h | 42 +- programs/src/disks/main.cpp | 20 +- programs/src/disks/render.cpp | 149 ++- programs/src/screenshot/Makefile | 5 +- programs/src/screenshot/main.cpp | 1029 +++++++++++++++-- programs/src/screenshot/stb_truetype_impl.cpp | 33 + template/sysroot/include/Api/Syscall.hpp | 4 + template/sysroot/include/gui/standalone.hpp | 5 + template/sysroot/include/gui/window.hpp | 1 + template/sysroot/include/libc/montauk.h | 8 + template/sysroot/include/montauk/syscall.h | 3 + 29 files changed, 1483 insertions(+), 395 deletions(-) create mode 100644 programs/src/screenshot/stb_truetype_impl.cpp diff --git a/kernel/src/Api/Syscall.cpp b/kernel/src/Api/Syscall.cpp index 6ef2377..a65d51e 100644 --- a/kernel/src/Api/Syscall.cpp +++ b/kernel/src/Api/Syscall.cpp @@ -30,7 +30,7 @@ #include "Device.hpp" // SYS_DEVLIST, SYS_DISKINFO #include "Input.hpp" // SYS_INPUT_WAIT #include "Storage.hpp" // SYS_PARTLIST, SYS_DISKREAD, SYS_DISKWRITE -#include "Window.hpp" // SYS_WINCREATE, SYS_WINDESTROY, SYS_WINPRESENT, SYS_WINPOLL, SYS_WINENUM, SYS_WINMAP, SYS_WINSENDEVENT, SYS_WINRESIZE, SYS_WINSETSCALE, SYS_WINGETSCALE +#include "Window.hpp" // SYS_WINCREATE, SYS_WINDESTROY, SYS_WINPRESENT, SYS_WINPOLL, SYS_WINENUM, SYS_WINMAP, SYS_WINSENDEVENT, SYS_WINRESIZE, SYS_WINSETCURSOR, SYS_WINSETFLAGS, SYS_WINSETSCALE, SYS_WINGETSCALE #include "Audio.hpp" // SYS_AUDIOOPEN, SYS_AUDIOCLOSE, SYS_AUDIOWRITE, SYS_AUDIOCTL #include "BluetoothSyscall.hpp" // SYS_BTSCAN, SYS_BTCONNECT, SYS_BTDISCONNECT, SYS_BTLIST, SYS_BTINFO #include "IpcSyscall.hpp" // SYS_DUPHANDLE, SYS_WAIT_HANDLE, SYS_STREAM_CREATE, SYS_STREAM_READ, SYS_STREAM_WRITE, SYS_MAILBOX_CREATE, SYS_MAILBOX_SEND, SYS_MAILBOX_RECV, SYS_WAITSET_CREATE, SYS_WAITSET_ADD, SYS_WAITSET_REMOVE, SYS_WAITSET_WAIT, SYS_PROC_OPEN, SYS_SURFACE_CREATE, SYS_SURFACE_MAP, SYS_SURFACE_RESIZE @@ -297,6 +297,8 @@ namespace Montauk { return (int64_t)Sys_WinGetScale(); case SYS_WINSETCURSOR: return (int64_t)Sys_WinSetCursor((int)frame->arg1, (int)frame->arg2); + case SYS_WINSETFLAGS: + return (int64_t)Sys_WinSetFlags((int)frame->arg1, (uint32_t)frame->arg2); case SYS_MEMSTATS: if (!UserMemory::Writable(frame->arg1)) return -1; Sys_MemStats((MemStats*)frame->arg1); diff --git a/kernel/src/Api/Syscall.hpp b/kernel/src/Api/Syscall.hpp index bb05dbe..c40d980 100644 --- a/kernel/src/Api/Syscall.hpp +++ b/kernel/src/Api/Syscall.hpp @@ -129,6 +129,7 @@ namespace Montauk { static constexpr uint64_t SYS_WINSETSCALE = 65; static constexpr uint64_t SYS_WINGETSCALE = 66; static constexpr uint64_t SYS_WINSETCURSOR = 68; + static constexpr uint64_t SYS_WINSETFLAGS = 126; /* Process.hpp */ static constexpr uint64_t SYS_PROCLIST = 61; @@ -329,8 +330,11 @@ namespace Montauk { uint8_t dirty; uint8_t cursor; // 0=arrow, 1=resize_h, 2=resize_v uint8_t _pad[2]; + uint32_t flags; }; + static constexpr uint32_t WIN_FLAG_FULLSCREEN = 1u << 0; + struct WinCreateResult { int32_t id; // -1 on failure uint32_t _pad; diff --git a/kernel/src/Api/WinServer.cpp b/kernel/src/Api/WinServer.cpp index aa9b287..e805991 100644 --- a/kernel/src/Api/WinServer.cpp +++ b/kernel/src/Api/WinServer.cpp @@ -159,6 +159,7 @@ namespace WinServer { slot.ownerVa = userVa; slot.dirty = false; slot.cursor = 0; + slot.flags = 0; int tlen = 0; while (title[tlen] && tlen < 63) { @@ -224,6 +225,7 @@ namespace WinServer { info.height = g_slots[i].height; info.dirty = g_slots[i].dirty ? 1 : 0; info.cursor = g_slots[i].cursor; + info.flags = g_slots[i].flags; g_slots[i].dirty = false; count++; } @@ -336,6 +338,16 @@ namespace WinServer { return 0; } + int SetFlags(int windowId, int callerPid, uint32_t flags) { + WsGuard guard; + if (windowId < 0 || windowId >= MaxWindows) return -1; + + WindowSlot& slot = g_slots[windowId]; + if (!slot.used || slot.ownerPid != callerPid) return -1; + slot.flags = flags & Montauk::WIN_FLAG_FULLSCREEN; + return 0; + } + int SetScale(int scale) { WsGuard guard; if (scale < 0) scale = 0; diff --git a/kernel/src/Api/WinServer.hpp b/kernel/src/Api/WinServer.hpp index eca95a1..333e46f 100644 --- a/kernel/src/Api/WinServer.hpp +++ b/kernel/src/Api/WinServer.hpp @@ -24,6 +24,7 @@ namespace WinServer { uint64_t ownerVa; // mapped VA of liveSurface in owner bool dirty; uint8_t cursor; // 0=arrow, 1=resize_h, 2=resize_v + uint32_t flags; }; int Create(int ownerPid, uint64_t ownerPml4, const char* title, int w, int h, @@ -39,6 +40,7 @@ namespace WinServer { uint64_t& heapNext, uint64_t& outVa); void CleanupProcess(int pid); int SetCursor(int windowId, int callerPid, int cursor); + int SetFlags(int windowId, int callerPid, uint32_t flags); int SetScale(int scale); int GetScale(); diff --git a/kernel/src/Api/Window.hpp b/kernel/src/Api/Window.hpp index dd707da..c6f5a4f 100644 --- a/kernel/src/Api/Window.hpp +++ b/kernel/src/Api/Window.hpp @@ -2,7 +2,7 @@ * Window.hpp * SYS_WINCREATE, SYS_WINDESTROY, SYS_WINPRESENT, SYS_WINPOLL, * SYS_WINENUM, SYS_WINMAP, SYS_WINSENDEVENT, SYS_WINRESIZE, - * SYS_WINSETSCALE, SYS_WINGETSCALE syscalls + * SYS_WINSETCURSOR, SYS_WINSETFLAGS, SYS_WINSETSCALE, SYS_WINGETSCALE syscalls * Copyright (c) 2026 Daniel Hammer */ @@ -75,6 +75,10 @@ namespace Montauk { return WinServer::SetCursor(windowId, Sched::GetCurrentPid(), cursor); } + static int Sys_WinSetFlags(int windowId, uint32_t flags) { + return WinServer::SetFlags(windowId, Sched::GetCurrentPid(), flags); + } + static int Sys_WinSetScale(int scale) { return WinServer::SetScale(scale); } diff --git a/programs/GNUmakefile b/programs/GNUmakefile index cca346e..0c6d7ff 100644 --- a/programs/GNUmakefile +++ b/programs/GNUmakefile @@ -267,8 +267,8 @@ rpgdemo: libc doom: libc $(MAKE) -C src/doom -# Build screenshot tool (depends on libc and libjpegwrite). -screenshot: libc libjpegwrite +# Build screenshot tool (depends on libc, libjpegwrite, libloader, and libdialogs). +screenshot: libc libjpegwrite libloader dialogs $(MAKE) -C src/screenshot # Build TCC (Tiny C Compiler) via its own Makefile (depends on libc). diff --git a/programs/include/Api/Syscall.hpp b/programs/include/Api/Syscall.hpp index 0692b72..81de880 100644 --- a/programs/include/Api/Syscall.hpp +++ b/programs/include/Api/Syscall.hpp @@ -82,6 +82,7 @@ namespace Montauk { static constexpr uint64_t SYS_WINSETSCALE = 65; static constexpr uint64_t SYS_WINGETSCALE = 66; static constexpr uint64_t SYS_WINSETCURSOR = 68; + static constexpr uint64_t SYS_WINSETFLAGS = 126; // Process management syscalls static constexpr uint64_t SYS_PROCLIST = 61; @@ -270,8 +271,11 @@ namespace Montauk { uint8_t dirty; uint8_t cursor; // 0=arrow, 1=resize_h, 2=resize_v uint8_t _pad2[2]; + uint32_t flags; }; + static constexpr uint32_t WIN_FLAG_FULLSCREEN = 1u << 0; + struct WinCreateResult { int32_t id; // -1 on failure uint32_t _pad; diff --git a/programs/include/gui/standalone.hpp b/programs/include/gui/standalone.hpp index 36e090e..2a2fcd4 100644 --- a/programs/include/gui/standalone.hpp +++ b/programs/include/gui/standalone.hpp @@ -70,6 +70,11 @@ struct WsWindow { montauk::win_setcursor(id, cursor); } + void set_flags(uint32_t flags) const { + if (id >= 0) + montauk::win_setflags(id, flags); + } + void destroy() { if (id >= 0) montauk::win_destroy(id); diff --git a/programs/include/gui/window.hpp b/programs/include/gui/window.hpp index 7c67c1d..fa16561 100644 --- a/programs/include/gui/window.hpp +++ b/programs/include/gui/window.hpp @@ -67,6 +67,7 @@ struct Window { bool external; // true = shared-memory window from external process int ext_win_id; // window server ID (valid when external == true) uint8_t ext_cursor; // cursor style requested by external app (0=arrow, 1=resize_h, 2=resize_v) + uint32_t ext_flags; Rect titlebar_rect() const { return {frame.x, frame.y, frame.w, TITLEBAR_HEIGHT}; diff --git a/programs/include/libc/montauk.h b/programs/include/libc/montauk.h index 3ba78d5..c51addd 100644 --- a/programs/include/libc/montauk.h +++ b/programs/include/libc/montauk.h @@ -88,6 +88,7 @@ extern "C" { #define MTK_SYS_WINGETSCALE 66 #define MTK_SYS_MEMSTATS 67 #define MTK_SYS_WINSETCURSOR 68 +#define MTK_SYS_WINSETFLAGS 126 #define MTK_SYS_FDELETE 77 #define MTK_SYS_FMKDIR 78 #define MTK_SYS_FRENAME 94 @@ -133,6 +134,8 @@ extern "C" { #define MTK_EVENT_CLOSE 3 #define MTK_EVENT_SCALE 4 +#define MTK_WIN_FLAG_FULLSCREEN (1u << 0) + /* Audio control commands */ #define MTK_AUDIO_SET_VOLUME 0 #define MTK_AUDIO_GET_VOLUME 1 @@ -189,6 +192,7 @@ typedef struct { uint8_t dirty; uint8_t cursor; uint8_t _pad2[2]; + uint32_t flags; } mtk_win_info; typedef struct { @@ -599,6 +603,10 @@ static inline int mtk_win_setcursor(int id, int cursor) { return (int)_mtk_syscall2(MTK_SYS_WINSETCURSOR, (long)id, (long)cursor); } +static inline int mtk_win_setflags(int id, uint32_t flags) { + return (int)_mtk_syscall2(MTK_SYS_WINSETFLAGS, (long)id, (long)flags); +} + static inline int mtk_win_setscale(int scale) { return (int)_mtk_syscall1(MTK_SYS_WINSETSCALE, (long)scale); } diff --git a/programs/include/montauk/syscall.h b/programs/include/montauk/syscall.h index 7007f3f..51a60dd 100644 --- a/programs/include/montauk/syscall.h +++ b/programs/include/montauk/syscall.h @@ -554,5 +554,8 @@ namespace montauk { inline int win_setcursor(int id, int cursor) { return (int)syscall2(Montauk::SYS_WINSETCURSOR, (uint64_t)id, (uint64_t)cursor); } + inline int win_setflags(int id, uint32_t flags) { + return (int)syscall2(Montauk::SYS_WINSETFLAGS, (uint64_t)id, (uint64_t)flags); + } } diff --git a/programs/libs/libprintersapplet/obj/src/libprintersapplet.o b/programs/libs/libprintersapplet/obj/src/libprintersapplet.o index 7f5df688232e873191579a918afe0fc480361c73..a75e69bd6f509e269e1ccf21d6f784724cc22e0d 100644 GIT binary patch delta 4270 zcmZ9Pdr(}}9mmgE!ospZ0_a>XiPV(MV_>PDZ0mYuogXCUrDU(@tgV)TEEm)Xvz}I`*9J{h{kQGkfpn z_x+yV@0@$i*~{|TZaF$4A1{<2#)^*1o5m-vllf`ilS1fE`aU6qPb5gUx{E|D!P}@k zYIOOhPuIpPGHpyL$znPUxE3bQ&{~rB$izw4^$F2--!2v2=hEUza2>c4Tn}CaZUA?I zo4~h#8^Jx`X7Fv`FnA642Jl*N1iTKsIEr9Bf+gTy@KSIe_(pI)xCOikyaIeXxD~tw z+y>qXZU=7z-vk~2cYyEoMfKCKL@)@W6TB0=3Ooev0^bK-4UU3u2Hy|91w0Jy20sA4 z75pIhHt<8>H7*fV!#|8*EsR~@b>Q9L_250=UhoKb19&fZBX}RU5BvzYAN(kI6Zre! z+rf`1R^#7+;Bgq6!7=a_@Dt#z;HSXbz&`*FfcJy%1doF60zVDD8~h{iAou`yyXL6) zF@ha14uW@rp8*en$H4c14}tHUt&&C3bH2^`E~W{1FCdiQ7r`#@OWO zKLf$5*$uiB_%*Oo=rGtR^g1{Jea68~(IenQ=o4T!_$b(^a}4bA>OroQn1%FSp`%Xe zbc3}@W>2j1-0O0`nwDQ{h%F#n+X?J_h7WGzgaK1*7c}#hQYK+u7wu$^9rKI)*MJ0gV zOgr5*UnHd0_>*cAtG$t|>+D(HO}6TjC=!xu{H|JmftJ-fP|8S_RFLi$k*t7P)#+(S zOmq_WNbj{uoKY=RHE*rA+V7QF6Msm5D8;j^sjamw)YjfgU3OnqM`T%d^U_ebySaVo z($>ZmO-;)tes0$#c$LxG)TXW0iT}+y>Y6y=+mj%dP$<`uQR>VM*l()M)2-9Fi4=9$ zi}w_bsXiU_Ms8RxB$;Q)a=l;XUo#~;U<(0_`bXldXAkWZ=F3{b4VlAqJd1z~wJ zT`sWf|EdF~+XbWNMe-MR2WPWu;aq)N=T+8o4ShY07lvh+{!^Ahvyf6KRzJZ^2kb(ire$a5A zr?WV zTahmf_c*&ryfaffHN+!k8Tu|>B_9o!TXu=z?%>_8qH*Om8t&En4O&i<M-lpY-eghBCW#}n9pqzcWfbM^Dv`^{zIPb^M?L4 z4>QW=XKMWx^ZGqc!U1zx6>^4(Sxr+^Vklp3fquiB-JHqa2iYd1*&ecDI-sV$w7rk?h6@Aoj zhj{}=4gFud`3DU>kq^yrLqErd<}E|t#C0YeojMj|ME-8{d7E=f^mJ9ztEF*-XI^3G zZmwBP;|nZ%q2XSoOQ;s6$pu!?GQ)j@k7K)`KRMyA>UGOKG*Wj;K0}fEQ}P6P8p3iF zwKPQUCw| delta 4165 zcmZ{m3vg7`8OP7Ln`}1OJexYFHwa457v(mJn&X zL{yTdROshx#7a}KfK(-H1u4^^lo>lfXIiQ)%-DxiJI{}`Z^a2Yd*aFxS(^D3DTMiDjR_$Ng-3ezRiqjTUbpe6 z)d}Qe8{-9;HLew($fg0O*_UIqH2+xql(XCuTV42wZZ36XGrH~2f?&EPR`5BR&_E#TeYuYe2i^ZHSCSfeXObGJ zCfpgC={=t$dS6d&{E-`Xn$S6>D*9WJZ-n%BbyaLgUNdrtXqU^4HW-8kW0hRM!XTV(FlKjUr`1d7OrHolA$yf^HXgN4WRIwW&mDs!Yj? zlwKZ|4HPX`?qxogH?7FmXt;bs=ris|;a>V*KVzI#Wa-N(uOci^GI(IO%6wiVXWqh3;@dD3efu+Aep?Qin z&htf9T8@V^R$KaHdSWe|mVSqr9?RbY-53kmU|@pZhI+lCzFj`Y$>D z1hvdp?pe!yM?a*d{D%6p`=;e4{se`1m&WI-$W_Z-&m;cF()aM~+Gny#Km%;Bb zIvC2!8?a)o(nLtnTOpr2W;v65+dWCLM!C;fZVo?UFH@*S$y3y>V=h=Rzvua0qP;aL z_O|72V)s2uKhNWSYUzc1xM{qq={?@garu@$&WBZ)&|}vQ>oT8em6dRmpVc}`FXaJR zEj@!Lw93+V^T6vqr|bX9G8VdMVu6wabY+2ZW1kBW;!m91L3y<*(yOyfF*D;?puLUt z={@)>_p&o7p0^S4Pg7s5a-UDiGMV^R(AUE>uH9q*C-)RxtW~jJS-BPHSBMLiUdH+x zmfphpWlP6LqV}$^ZdRH+^T~w17N=);4tMmS)lro7Pb_^E>mL2>V~Sb0`L&ydE@^b; z2Ib~j?oh%MU!*DRx=F6--h5w~GKT^Sm0OzRnx1#T6QY))+HGRjER1g$_^WAnp^9}R z<(jXY-|<0qTlzBwzIpmtwe*hoxwT8Y1vFux?V#QO<`F~ zA2-#}`;9(1LW`U0(o^_PsE3A{>!kkQZ*J0AQ@T#2P)m~()TiqkX+qcfi$~XoD737} z`NbmYU)GrM$`{NUg!p#`{cPDn=f#=y!Lml@*+QymZFF8Oq<(!Fsi$MDjr!AYs#WI& TsAhR%#;f&-yseVzm(KbxP@c)_ diff --git a/programs/libs/libtimezonesapplet/obj/src/libtimezonesapplet.o b/programs/libs/libtimezonesapplet/obj/src/libtimezonesapplet.o index 150d1ec5851a630418444d6ccd07993a1c13b4eb..a61cdbe988c11d0c19e71379f8fd8e34f8d7a7cc 100644 GIT binary patch delta 4306 zcmZ{mdvH|M9mmhPAfrw_`|)@uKd(5V!e4rFwyeav(yohgH5=sDl}3+$Yko|)|Z zobUH{e&=~_&gDJw&_4NGu~l3jE%{X56_awQEX??p6hi-H?iWJjiv*dZo+438@E*_} z^;^EIEN#3iGscyYBC?3A#*}QWC3}v<&RMr7L^|_#sp?~v7FUDo!Cl}6a5vZw?g6)e z?*=!6`@pT>d%;0)Kll#tI&cVlANbA)D(g{M1|9$}2M>b34c-6_gExX#f*%C8gExaa zz+1qb;34o`;H}_Q;D_@gdiJYP8HUjX-U03gkAQo?kAc^KBjB~*$H8}lN5Q?|?}6_D zKLNfM{3N*F5)sw?PE^*x*af~1yc@h8yazl0-V44Tybt^Uct3a${1kWt_-XJ)@H5~C z!OtpI{Xc}tb1*i6qu|Zp=fPXRFM@}_-v@669{@iL9s@rDei{57@DISl;8(!gG)Ke_ zQP~dTAb1D(RqzOS9Q$>9oG&C?MDvodII#w@v;mS)VHjZce} zrU$V=yVf=+OB5cZp^Wmn1Ip2T=4yA_yv^#f?y68*Z|m|vu(!2y z`SSMWl`So8u`itZ1dlS>TRODW9-BSq=T_`Y{&<43EKN^Kh-&vCbTw1`MJQTOCVxRE z3T)@aRM))6eO!-OMHdT#PNUjlU7m3t*WGdo&o7frG&(=%)NwCojhITBnr~aPQt8V4 z#m+f(cy!6p%c{!sP*zf4!5R4(IfX%aoch%72lP(iu>6XAMP>3&6jr~LG^&30(nOK% zY~?%$ahz}DxkOV%L1!(ypBU~H3KZKaXIF8rlgCrCaU5@Cc#kGk%zFB3aZm=SVquy5 z3H2?sogBOcLJS-6Llj*Yl)t01>i1vdD+yXo8V#1T%C~5u#Fjhgqmp5{h61HQN546` zL}s|p>B*PScxkWv4ar5e)6VMRVcTPGn_Zv3)Hu0I6TNZP=r_J#+2&0US;T4 zIo@yRNj%kM8?GLcE>9Tx3p}1@ z4Skefk(X&g_4T&lW^$_-O)2-d;r8+@eox+VJ95Tw%Q^GshW0?4PIqNQDNU;3))?+lUi4-|pTjT13PZn%<9iJKQ|^7h z(0B4Q_{&8D6<#M|#9ZP7x}TyIwsXL6|Heo1bvmKk!-kv1ui4M&VufAtq2W$%>>muh zl4tQb`6_L?zcM>@`ape6;YvG_%%h%O^pANAxrUz4o1JgyuW)~*hJKPaug1{-!kgdh z>hxu$*I8l2+`&0}$XjLi4H)iW&b-yoLtKv-dXz`L-_Y~8moeH>Wjk*g?ltaZjK-_% z(3^Zl^!9Ne&f5E|Pp{q#ZuQf+cwSJ%pCVapJLlqBX~bPcJ)? zv|Wh2>bj*aFuLfG=#U~^Q69U$8l)EO*)l0A&o)CjHrQAo@)f3|r z0sm?8-fBn2;@avj?7w-J4;p$RADUx^KFP=WmxjKP^Gq3fKflcXaCLWCb;rNrM5TP= zojzH6^|)23H1s6ypw`rR^BPIk*nyzop63;4GxVqU{B|1p3$d=6;Usklnyf!AFHp4M zn!HR=e^7SQS$|DxV}U!kQ{*((q;~Vas6py#tdYN_XrtfCUP7lE8&tSA;8*vvu=;(8 zq5;2EUQ4F~4e}~^oBY7K1qONA(D`Uge9r#f*nlNVnBo>Mhc^AevuipE!7TV+K#1871SAf&U4?;_006l z?7hGDdEWDV&)s`ukGwi0_m^0Gb)%(k%DlLgb+Rb!t5OL4OCJ$JqSZU;XMzQ+1>^Ax$I0^bVI2jxVyY2h}Y-Qwu=9!W`mJoQfDre^;KbIgT|1u%So=6Ci zbh@=;PV(6VG5NK`;4KdB)~WOKCI4>;z7~9!)ulj_ykTy0g6Y+oD)MRHQZ+NYW2{s? z-a;)~eOQ8Oh5d9Sy&|bqITQ4D`s~^LivF#B{`UdM$*2h?)h6kTA?dGHV&*cbM1QS+SF1!bS2VjTYLn zf{qqmkiVmWqKMo}XVmpDWf%9#S7^AnTpp*B>Y7FG6_-1kxjDi^C$0~rnL;JDJWtV* zh^(d265E;L4Y_D!zCsfv8$+LRJ2oDo|Ftv1wek(UiJIp}WFb8{zdSUk=dVhqk@FYU zdky_ox~A$rPT|ss{38vQMy!u~^lIr%&I4Q_Fw1RlW?gTlP+706p`&FHd5Er*+0skC z1sk0c_?Hvnl#w5zfd#hH&qIIBa2*<7V9V#}%!1y~avTC7iupd%<2fG55<`ETx)<7X zaG^K4+HgEvW38c2``o#-8~Pa@RTo`ZXgfO%x0z?Qn}Un%C>ibmxA}~rPjjtfhW=~L zKS2YFY-iGN-&Lnmmp{q@i@z=Te-D{`!PR85gJqOZH9ZA*MB3OFSnzehKsKrHOF2<-_GONW#|KZNcK^v z!gdZA?mu{ThpAP$al>8Cvp7wo6?W{L;Z|_V7YselBYDTr(>Q+L&>!RcPYk_c^dp4 zqsyV}>>eZM1G*BjDQ~gY88Mt`e(av0@M7EfiQ#7Q5qp8U7u)g_jj5b-M$Q{N-V1bo zu^oHIa68z2-_Xx;zn>U-E^lrM&uV6kcXD39(8qYQa$P-keY2)`SIdkFFY;NfGV~(u zpwZA%c|dCneJ^*s{&TwelPqRoGhJC?%N`0=+D`0qNkY8MmD{Mf(vEhiTDs_waU$YA z)@Rn>uWas0$meB5{!=ur`g%5@mPULesQXd6q}-SOPu)|LeWM-wjZt?#+7;rQp%=6M zx}k?zpE7iO9V+h<>w2b%BcFElb=W=i-Qs>Jsr!dUL(NLlm;}u^@o)%^Qcw1MG3BMc{e;EDrr=?4eaWP@dE>YEln)7 zV{Hj_^?USH-pDP6{t-X31BO1y`jDZo;&zT1dOIK6ckha0R-RWul`S6@>T;Rabva%F?;pzz8D ztG}8CSJunl(u4}GR+H6OpZdlZ+~%&P(yGR#*6-)gU}L>+GS^iDG?o{jw;Btqsa&#} m>aCY+Xk}Bq`l1|YYVchRx>@aRR#QRhziM3N5lMTN&;2ivcGM66 diff --git a/programs/src/desktop/compose.cpp b/programs/src/desktop/compose.cpp index ee6026e..0e405c0 100644 --- a/programs/src/desktop/compose.cpp +++ b/programs/src/desktop/compose.cpp @@ -240,93 +240,103 @@ void gui::desktop_compose(DesktopState* ds) { } } - // Draw panel on top - desktop_draw_panel(ds); - - // Draw app menu if open - if (ds->app_menu_open) { - desktop_draw_app_menu(ds); + bool topFullscreen = false; + for (int i = ds->window_count - 1; i >= 0; i--) { + if (ds->windows[i].state == WIN_MINIMIZED || ds->windows[i].state == WIN_CLOSED) + continue; + topFullscreen = desktop_window_fullscreen(&ds->windows[i]); + break; } - // Draw network popup if open - if (ds->net_popup_open) { - desktop_draw_net_popup(ds); - } + if (!topFullscreen) { + // Draw panel on top + desktop_draw_panel(ds); - // Draw volume popup if open - if (ds->vol_popup_open) { - desktop_draw_vol_popup(ds); - } - - // Draw right-click context menu if open - if (ds->ctx_menu_open) { - static constexpr int CTX_MENU_W = 180; - static constexpr int CTX_ITEM_H = 36; - static constexpr int CTX_ITEM_COUNT = 6; - int cmx = ds->ctx_menu_x; - int cmy = ds->ctx_menu_y; - int cmh = CTX_ITEM_H * CTX_ITEM_COUNT + 8; - - // Clamp to screen - if (cmx + CTX_MENU_W > sw) cmx = sw - CTX_MENU_W; - if (cmy + cmh > sh) cmy = sh - cmh; - - ensure_filemanager_icons_loaded(ds); - - draw_shadow(fb, cmx, cmy, CTX_MENU_W, cmh, 4, colors::SHADOW); - fill_rounded_rect(fb, cmx, cmy, CTX_MENU_W, cmh, 8, colors::MENU_BG); - draw_rect(fb, cmx, cmy, CTX_MENU_W, cmh, colors::BORDER); - - struct CtxItem { const char* label; SvgIcon* icon; }; - CtxItem ctx_items[CTX_ITEM_COUNT] = { - { "Terminal", &ds->icon_terminal }, - { "Files", &ds->icon_filemanager }, - { "System Configuration", &ds->icon_system_configuration_menu }, - { "Sleep", &ds->icon_sleep }, - { "Reboot", &ds->icon_reboot }, - { "Shutdown", &ds->icon_shutdown }, - }; - - int mmx = ds->mouse.x; - int mmy = ds->mouse.y; - - for (int i = 0; i < CTX_ITEM_COUNT; i++) { - int iy = cmy + 4 + i * CTX_ITEM_H; - Rect item_r = {cmx + 4, iy, CTX_MENU_W - 8, CTX_ITEM_H}; - - if (item_r.contains(mmx, mmy)) { - fill_rounded_rect(fb, item_r.x, item_r.y, item_r.w, item_r.h, 4, colors::MENU_HOVER); - } - - int icon_x = item_r.x + 8; - int icon_y = item_r.y + (CTX_ITEM_H - 20) / 2; - if (ctx_items[i].icon && ctx_items[i].icon->pixels) { - fb.blit_alpha(icon_x, icon_y, ctx_items[i].icon->width, ctx_items[i].icon->height, ctx_items[i].icon->pixels); - } - - int tx = icon_x + 28; - int ty = item_r.y + (CTX_ITEM_H - system_font_height()) / 2; - draw_text(fb, tx, ty, ctx_items[i].label, colors::TEXT_COLOR); + // Draw app menu if open + if (ds->app_menu_open) { + desktop_draw_app_menu(ds); } - } - if (ds->launcher_open) { - desktop_draw_launcher(ds); - } + // Draw network popup if open + if (ds->net_popup_open) { + desktop_draw_net_popup(ds); + } - // Draw snap preview overlay while dragging to screen edge - for (int i = 0; i < ds->window_count; i++) { - Window* win = &ds->windows[i]; - if (win->dragging) { - int dmx = ds->mouse.x; - if (dmx <= 0) { - fb.fill_rect_alpha(0, PANEL_HEIGHT, sw / 2, sh - PANEL_HEIGHT, - Color::from_rgba(0x33, 0x77, 0xCC, 0x30)); - } else if (dmx >= sw - 1) { - fb.fill_rect_alpha(sw / 2, PANEL_HEIGHT, sw / 2, sh - PANEL_HEIGHT, - Color::from_rgba(0x33, 0x77, 0xCC, 0x30)); + // Draw volume popup if open + if (ds->vol_popup_open) { + desktop_draw_vol_popup(ds); + } + + // Draw right-click context menu if open + if (ds->ctx_menu_open) { + static constexpr int CTX_MENU_W = 180; + static constexpr int CTX_ITEM_H = 36; + static constexpr int CTX_ITEM_COUNT = 6; + int cmx = ds->ctx_menu_x; + int cmy = ds->ctx_menu_y; + int cmh = CTX_ITEM_H * CTX_ITEM_COUNT + 8; + + // Clamp to screen + if (cmx + CTX_MENU_W > sw) cmx = sw - CTX_MENU_W; + if (cmy + cmh > sh) cmy = sh - cmh; + + ensure_filemanager_icons_loaded(ds); + + draw_shadow(fb, cmx, cmy, CTX_MENU_W, cmh, 4, colors::SHADOW); + fill_rounded_rect(fb, cmx, cmy, CTX_MENU_W, cmh, 8, colors::MENU_BG); + draw_rect(fb, cmx, cmy, CTX_MENU_W, cmh, colors::BORDER); + + struct CtxItem { const char* label; SvgIcon* icon; }; + CtxItem ctx_items[CTX_ITEM_COUNT] = { + { "Terminal", &ds->icon_terminal }, + { "Files", &ds->icon_filemanager }, + { "System Configuration", &ds->icon_system_configuration_menu }, + { "Sleep", &ds->icon_sleep }, + { "Reboot", &ds->icon_reboot }, + { "Shutdown", &ds->icon_shutdown }, + }; + + int mmx = ds->mouse.x; + int mmy = ds->mouse.y; + + for (int i = 0; i < CTX_ITEM_COUNT; i++) { + int iy = cmy + 4 + i * CTX_ITEM_H; + Rect item_r = {cmx + 4, iy, CTX_MENU_W - 8, CTX_ITEM_H}; + + if (item_r.contains(mmx, mmy)) { + fill_rounded_rect(fb, item_r.x, item_r.y, item_r.w, item_r.h, 4, colors::MENU_HOVER); + } + + int icon_x = item_r.x + 8; + int icon_y = item_r.y + (CTX_ITEM_H - 20) / 2; + if (ctx_items[i].icon && ctx_items[i].icon->pixels) { + fb.blit_alpha(icon_x, icon_y, ctx_items[i].icon->width, ctx_items[i].icon->height, ctx_items[i].icon->pixels); + } + + int tx = icon_x + 28; + int ty = item_r.y + (CTX_ITEM_H - system_font_height()) / 2; + draw_text(fb, tx, ty, ctx_items[i].label, colors::TEXT_COLOR); + } + } + + if (ds->launcher_open) { + desktop_draw_launcher(ds); + } + + // Draw snap preview overlay while dragging to screen edge + for (int i = 0; i < ds->window_count; i++) { + Window* win = &ds->windows[i]; + if (win->dragging) { + int dmx = ds->mouse.x; + if (dmx <= 0) { + fb.fill_rect_alpha(0, PANEL_HEIGHT, sw / 2, sh - PANEL_HEIGHT, + Color::from_rgba(0x33, 0x77, 0xCC, 0x30)); + } else if (dmx >= sw - 1) { + fb.fill_rect_alpha(sw / 2, PANEL_HEIGHT, sw / 2, sh - PANEL_HEIGHT, + Color::from_rgba(0x33, 0x77, 0xCC, 0x30)); + } + break; } - break; } } @@ -339,7 +349,8 @@ void gui::desktop_compose(DesktopState* ds) { cur_style = cursor_for_edge(win->resize_edge); break; } - if (win->state == WIN_MINIMIZED || win->state == WIN_CLOSED || win->state == WIN_MAXIMIZED) + if (win->state == WIN_MINIMIZED || win->state == WIN_CLOSED || + win->state == WIN_MAXIMIZED || desktop_window_fullscreen(win)) continue; if (win->frame.contains(ds->mouse.x, ds->mouse.y)) { ResizeEdge edge = hit_test_resize_edge(win->frame, ds->mouse.x, ds->mouse.y); @@ -354,7 +365,7 @@ void gui::desktop_compose(DesktopState* ds) { if (cur_style == CURSOR_ARROW && ds->focused_window >= 0) { Window* fwin = &ds->windows[ds->focused_window]; if (fwin->external && fwin->ext_cursor > 0) { - Rect cr = fwin->content_rect(); + Rect cr = desktop_content_rect(fwin); if (cr.contains(ds->mouse.x, ds->mouse.y)) { if (fwin->ext_cursor == 1) cur_style = CURSOR_RESIZE_H; else if (fwin->ext_cursor == 2) cur_style = CURSOR_RESIZE_V; diff --git a/programs/src/desktop/desktop_internal.hpp b/programs/src/desktop/desktop_internal.hpp index 1d33778..471ba2d 100644 --- a/programs/src/desktop/desktop_internal.hpp +++ b/programs/src/desktop/desktop_internal.hpp @@ -112,6 +112,16 @@ inline int menu_add_external(const char* label, const char* binary, SvgIcon* ico // Forward Declarations (internal functions) // ============================================================================ +inline bool desktop_window_fullscreen(const gui::Window* win) { + return win && win->external && (win->ext_flags & Montauk::WIN_FLAG_FULLSCREEN); +} + +inline gui::Rect desktop_content_rect(const gui::Window* win) { + return desktop_window_fullscreen(win) + ? win->frame + : win->content_rect(); +} + // window.cpp gui::ResizeEdge hit_test_resize_edge(const gui::Rect& f, int mx, int my); gui::CursorStyle cursor_for_edge(gui::ResizeEdge edge); diff --git a/programs/src/desktop/input.cpp b/programs/src/desktop/input.cpp index a531d0f..0fd36a7 100644 --- a/programs/src/desktop/input.cpp +++ b/programs/src/desktop/input.cpp @@ -94,6 +94,47 @@ static void handle_lock_keyboard(DesktopState* ds, const Montauk::KeyEvent& key) } } +static void forward_external_mouse(gui::Window* win, + const gui::MouseEvent& ev, + int mx, + int my, + uint8_t buttons, + uint8_t prev) { + gui::Rect cr = desktop_content_rect(win); + Montauk::WinEvent wev; + montauk::memset(&wev, 0, sizeof(wev)); + wev.type = 1; // mouse + wev.mouse.x = mx - cr.x; + wev.mouse.y = my - cr.y; + wev.mouse.scroll = ev.scroll; + wev.mouse.buttons = buttons; + wev.mouse.prev_buttons = prev; + montauk::win_sendevent(win->ext_win_id, &wev); +} + +static bool route_fullscreen_external_mouse(gui::DesktopState* ds, + const gui::MouseEvent& ev, + int mx, + int my, + uint8_t buttons, + uint8_t prev) { + for (int i = ds->window_count - 1; i >= 0; i--) { + gui::Window* win = &ds->windows[i]; + if (win->state == gui::WIN_MINIMIZED || win->state == gui::WIN_CLOSED) + continue; + if (!desktop_window_fullscreen(win)) + return false; + + if (i != ds->window_count - 1) { + gui::desktop_raise_window(ds, i); + win = &ds->windows[ds->window_count - 1]; + } + forward_external_mouse(win, ev, mx, my, buttons, prev); + return true; + } + return false; +} + // ============================================================================ // Desktop Mouse Handling // ============================================================================ @@ -104,11 +145,6 @@ void gui::desktop_handle_mouse(DesktopState* ds) { return; } - if (ds->launcher_open) { - desktop_handle_launcher_mouse(ds); - return; - } - int mx = ds->mouse.x; int my = ds->mouse.y; uint8_t buttons = ds->mouse.buttons; @@ -125,6 +161,19 @@ void gui::desktop_handle_mouse(DesktopState* ds) { ev.prev_buttons = prev; ev.scroll = ds->mouse.scrollDelta; + if (route_fullscreen_external_mouse(ds, ev, mx, my, buttons, prev)) { + ds->app_menu_open = false; + ds->ctx_menu_open = false; + ds->vol_popup_open = false; + ds->net_popup_open = false; + return; + } + + if (ds->launcher_open) { + desktop_handle_launcher_mouse(ds); + return; + } + // Handle context menu clicks if (ds->ctx_menu_open) { if (left_pressed) { @@ -183,7 +232,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) { win->frame = {0, PANEL_HEIGHT, ds->screen_w / 2, ds->screen_h - PANEL_HEIGHT}; win->state = WIN_MAXIMIZED; if (!win->external) { - Rect cr = win->content_rect(); + Rect cr = desktop_content_rect(win); if (cr.w != win->content_w || cr.h != win->content_h) { if (win->content) montauk::free(win->content); win->content_w = cr.w; @@ -192,7 +241,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) { montauk::memset(win->content, 0xFF, cr.w * cr.h * 4); } } else { - Rect cr = win->content_rect(); + Rect cr = desktop_content_rect(win); Montauk::WinEvent rev; montauk::memset(&rev, 0, sizeof(rev)); rev.type = 2; @@ -205,7 +254,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) { win->frame = {ds->screen_w / 2, PANEL_HEIGHT, ds->screen_w / 2, ds->screen_h - PANEL_HEIGHT}; win->state = WIN_MAXIMIZED; if (!win->external) { - Rect cr = win->content_rect(); + Rect cr = desktop_content_rect(win); if (cr.w != win->content_w || cr.h != win->content_h) { if (win->content) montauk::free(win->content); win->content_w = cr.w; @@ -214,7 +263,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) { montauk::memset(win->content, 0xFF, cr.w * cr.h * 4); } } else { - Rect cr = win->content_rect(); + Rect cr = desktop_content_rect(win); Montauk::WinEvent rev; montauk::memset(&rev, 0, sizeof(rev)); rev.type = 2; @@ -271,7 +320,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) { win->resizing = false; // Reallocate content buffer if dimensions changed (skip for external) if (!win->external) { - Rect cr = win->content_rect(); + Rect cr = desktop_content_rect(win); if (cr.w != win->content_w || cr.h != win->content_h) { if (win->content) montauk::free(win->content); win->content_w = cr.w; @@ -281,7 +330,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) { } win->dirty = true; } else { - Rect cr = win->content_rect(); + Rect cr = desktop_content_rect(win); Montauk::WinEvent rev; montauk::memset(&rev, 0, sizeof(rev)); rev.type = 2; @@ -505,92 +554,94 @@ void gui::desktop_handle_mouse(DesktopState* ds) { Window* win = &ds->windows[i]; if (win->state == WIN_MINIMIZED || win->state == WIN_CLOSED) continue; - // Check close button - Rect close_r = win->close_btn_rect(); - if (close_r.contains(mx, my)) { - desktop_close_window(ds, i); - return; - } + if (!desktop_window_fullscreen(win)) { + // Check close button + Rect close_r = win->close_btn_rect(); + if (close_r.contains(mx, my)) { + desktop_close_window(ds, i); + return; + } - // Check minimize button - Rect min_r = win->min_btn_rect(); - if (min_r.contains(mx, my)) { - win->state = WIN_MINIMIZED; - if (ds->focused_window == i) { - ds->focused_window = -1; - for (int j = ds->window_count - 1; j >= 0; j--) { - if (ds->windows[j].state == WIN_NORMAL || ds->windows[j].state == WIN_MAXIMIZED) { - ds->focused_window = j; - ds->windows[j].focused = true; - break; + // Check minimize button + Rect min_r = win->min_btn_rect(); + if (min_r.contains(mx, my)) { + win->state = WIN_MINIMIZED; + if (ds->focused_window == i) { + ds->focused_window = -1; + for (int j = ds->window_count - 1; j >= 0; j--) { + if (ds->windows[j].state == WIN_NORMAL || ds->windows[j].state == WIN_MAXIMIZED) { + ds->focused_window = j; + ds->windows[j].focused = true; + break; + } } } + return; } - return; - } - // Check maximize button - Rect max_r = win->max_btn_rect(); - if (max_r.contains(mx, my)) { - if (win->state == WIN_MAXIMIZED) { - win->frame = win->saved_frame; - win->state = WIN_NORMAL; - } else { - win->saved_frame = win->frame; - win->frame = {0, PANEL_HEIGHT, ds->screen_w, ds->screen_h - PANEL_HEIGHT}; - win->state = WIN_MAXIMIZED; - } - // Reallocate content buffer for local windows only - if (!win->external) { - Rect cr = win->content_rect(); - if (cr.w != win->content_w || cr.h != win->content_h) { - if (win->content) montauk::free(win->content); - win->content_w = cr.w; - win->content_h = cr.h; - win->content = (uint32_t*)montauk::alloc(cr.w * cr.h * 4); - montauk::memset(win->content, 0xFF, cr.w * cr.h * 4); + // Check maximize button + Rect max_r = win->max_btn_rect(); + if (max_r.contains(mx, my)) { + if (win->state == WIN_MAXIMIZED) { + win->frame = win->saved_frame; + win->state = WIN_NORMAL; + } else { + win->saved_frame = win->frame; + win->frame = {0, PANEL_HEIGHT, ds->screen_w, ds->screen_h - PANEL_HEIGHT}; + win->state = WIN_MAXIMIZED; } - } else { - Rect cr = win->content_rect(); - Montauk::WinEvent rev; - montauk::memset(&rev, 0, sizeof(rev)); - rev.type = 2; - rev.resize.w = cr.w; - rev.resize.h = cr.h; - montauk::win_sendevent(win->ext_win_id, &rev); + // Reallocate content buffer for local windows only + if (!win->external) { + Rect cr = desktop_content_rect(win); + if (cr.w != win->content_w || cr.h != win->content_h) { + if (win->content) montauk::free(win->content); + win->content_w = cr.w; + win->content_h = cr.h; + win->content = (uint32_t*)montauk::alloc(cr.w * cr.h * 4); + montauk::memset(win->content, 0xFF, cr.w * cr.h * 4); + } + } else { + Rect cr = desktop_content_rect(win); + Montauk::WinEvent rev; + montauk::memset(&rev, 0, sizeof(rev)); + rev.type = 2; + rev.resize.w = cr.w; + rev.resize.h = cr.h; + montauk::win_sendevent(win->ext_win_id, &rev); + } + desktop_raise_window(ds, i); + return; } - desktop_raise_window(ds, i); - return; - } - // Check resize edges (before titlebar drag, so corner grabs work) - if (win->state != WIN_MAXIMIZED) { - ResizeEdge edge = hit_test_resize_edge(win->frame, mx, my); - if (edge != RESIZE_NONE) { + // Check resize edges (before titlebar drag, so corner grabs work) + if (win->state != WIN_MAXIMIZED) { + ResizeEdge edge = hit_test_resize_edge(win->frame, mx, my); + if (edge != RESIZE_NONE) { + desktop_raise_window(ds, i); + int new_idx = ds->window_count - 1; + ds->windows[new_idx].resizing = true; + ds->windows[new_idx].resize_edge = edge; + ds->windows[new_idx].resize_start_frame = ds->windows[new_idx].frame; + ds->windows[new_idx].resize_start_mx = mx; + ds->windows[new_idx].resize_start_my = my; + return; + } + } + + // Check titlebar (start drag) + Rect tb = win->titlebar_rect(); + if (tb.contains(mx, my)) { desktop_raise_window(ds, i); int new_idx = ds->window_count - 1; - ds->windows[new_idx].resizing = true; - ds->windows[new_idx].resize_edge = edge; - ds->windows[new_idx].resize_start_frame = ds->windows[new_idx].frame; - ds->windows[new_idx].resize_start_mx = mx; - ds->windows[new_idx].resize_start_my = my; + ds->windows[new_idx].dragging = true; + ds->windows[new_idx].drag_offset_x = mx - ds->windows[new_idx].frame.x; + ds->windows[new_idx].drag_offset_y = my - ds->windows[new_idx].frame.y; return; } } - // Check titlebar (start drag) - Rect tb = win->titlebar_rect(); - if (tb.contains(mx, my)) { - desktop_raise_window(ds, i); - int new_idx = ds->window_count - 1; - ds->windows[new_idx].dragging = true; - ds->windows[new_idx].drag_offset_x = mx - ds->windows[new_idx].frame.x; - ds->windows[new_idx].drag_offset_y = my - ds->windows[new_idx].frame.y; - return; - } - // Check content area - Rect cr = win->content_rect(); + Rect cr = desktop_content_rect(win); if (cr.contains(mx, my)) { desktop_raise_window(ds, i); int new_idx = ds->window_count - 1; @@ -630,7 +681,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) { if (!left_pressed && ds->focused_window >= 0) { Window* win = &ds->windows[ds->focused_window]; if (win->state != WIN_MINIMIZED && win->state != WIN_CLOSED) { - Rect cr = win->content_rect(); + Rect cr = desktop_content_rect(win); // Forward if mouse is over content (hover/move) or button is held/released (drag continuity) if (cr.contains(mx, my) || left_held || left_released) { if (win->external) { @@ -655,7 +706,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) { // Handle scroll events on focused window if (ev.scroll != 0 && ds->focused_window >= 0) { Window* win = &ds->windows[ds->focused_window]; - Rect cr = win->content_rect(); + Rect cr = desktop_content_rect(win); if (cr.contains(mx, my)) { if (win->external) { Montauk::WinEvent wev; @@ -696,12 +747,30 @@ void gui::desktop_handle_mouse(DesktopState* ds) { } void gui::desktop_handle_keyboard(DesktopState* ds, const Montauk::KeyEvent& key) { - if (desktop_handle_launcher_keyboard(ds, key)) { + if (ds->screen_locked) { + handle_lock_keyboard(ds, key); return; } - if (ds->screen_locked) { - handle_lock_keyboard(ds, key); + for (int i = ds->window_count - 1; i >= 0; i--) { + Window* win = &ds->windows[i]; + if (win->state == WIN_MINIMIZED || win->state == WIN_CLOSED) + continue; + if (!desktop_window_fullscreen(win)) + break; + + Montauk::WinEvent ev; + montauk::memset(&ev, 0, sizeof(ev)); + ev.type = 0; // key + ev.key = key; + montauk::win_sendevent(win->ext_win_id, &ev); + ds->launcher_open = false; + ds->app_menu_open = false; + ds->ctx_menu_open = false; + return; + } + + if (desktop_handle_launcher_keyboard(ds, key)) { return; } diff --git a/programs/src/desktop/main.cpp b/programs/src/desktop/main.cpp index 57eaaf4..3f87c92 100644 --- a/programs/src/desktop/main.cpp +++ b/programs/src/desktop/main.cpp @@ -419,6 +419,7 @@ bool desktop_poll_external_windows(DesktopState* ds) { if (ds->windows[i].external && ds->windows[i].ext_win_id == extId) { found = true; bool remapRequested = false; + bool wasFullscreen = desktop_window_fullscreen(&ds->windows[i]); uint64_t currentVa = (uint64_t)(uintptr_t)ds->windows[i].content; if (ds->windows[i].content_w != extWins[e].width || ds->windows[i].content_h != extWins[e].height) { @@ -436,6 +437,26 @@ bool desktop_poll_external_windows(DesktopState* ds) { changed = true; } } + if (ds->windows[i].ext_flags != extWins[e].flags) { + ds->windows[i].ext_flags = extWins[e].flags; + bool isFullscreen = desktop_window_fullscreen(&ds->windows[i]); + if (isFullscreen) { + if (!wasFullscreen) ds->windows[i].saved_frame = ds->windows[i].frame; + ds->windows[i].frame = {0, 0, ds->screen_w, ds->screen_h}; + ds->windows[i].state = WIN_NORMAL; + } else if (wasFullscreen) { + ds->windows[i].frame = ds->windows[i].saved_frame; + ds->windows[i].state = WIN_NORMAL; + } + changed = true; + } + if (desktop_window_fullscreen(&ds->windows[i]) && + (ds->windows[i].frame.x != 0 || ds->windows[i].frame.y != 0 || + ds->windows[i].frame.w != ds->screen_w || ds->windows[i].frame.h != ds->screen_h)) { + ds->windows[i].frame = {0, 0, ds->screen_w, ds->screen_h}; + ds->windows[i].state = WIN_NORMAL; + changed = true; + } // Update dirty flag and cursor if (extWins[e].dirty) { ds->windows[i].dirty = true; @@ -524,6 +545,12 @@ bool desktop_poll_external_windows(DesktopState* ds) { win->app_data = nullptr; win->external = true; win->ext_win_id = extId; + win->ext_cursor = extWins[e].cursor; + win->ext_flags = extWins[e].flags; + if (desktop_window_fullscreen(win)) { + win->frame = {0, 0, ds->screen_w, ds->screen_h}; + win->state = WIN_NORMAL; + } // Unfocus previous window if (ds->focused_window >= 0 && ds->focused_window < ds->window_count) { diff --git a/programs/src/desktop/window.cpp b/programs/src/desktop/window.cpp index 6d04548..68eda5a 100644 --- a/programs/src/desktop/window.cpp +++ b/programs/src/desktop/window.cpp @@ -39,6 +39,8 @@ int gui::desktop_create_window(DesktopState* ds, const char* title, int x, int y win->app_data = nullptr; win->external = false; win->ext_win_id = -1; + win->ext_cursor = 0; + win->ext_flags = 0; // Unfocus previous window if (ds->focused_window >= 0 && ds->focused_window < ds->window_count) { @@ -131,50 +133,53 @@ void gui::desktop_draw_window(DesktopState* ds, int idx) { int y = win->frame.y; int w = win->frame.w; int h = win->frame.h; + bool fullscreen = desktop_window_fullscreen(win); - // Draw shadow - if (ds->settings.show_shadows) - draw_shadow(fb, x, y, w, h, SHADOW_SIZE, colors::SHADOW); + if (!fullscreen) { + // Draw shadow + if (ds->settings.show_shadows) + draw_shadow(fb, x, y, w, h, SHADOW_SIZE, colors::SHADOW); - // Draw window body - fb.fill_rect(x, y, w, h, colors::WINDOW_BG); + // Draw window body + fb.fill_rect(x, y, w, h, colors::WINDOW_BG); - // Draw titlebar - Color tb_bg = win->focused ? colors::TITLEBAR_BG : Color::from_rgb(0xE8, 0xE8, 0xE8); - fb.fill_rect(x, y, w, TITLEBAR_HEIGHT, tb_bg); + // Draw titlebar + Color tb_bg = win->focused ? colors::TITLEBAR_BG : Color::from_rgb(0xE8, 0xE8, 0xE8); + fb.fill_rect(x, y, w, TITLEBAR_HEIGHT, tb_bg); - // Draw border - draw_rect(fb, x, y, w, h, colors::BORDER); - // Titlebar bottom separator - draw_hline(fb, x, y + TITLEBAR_HEIGHT - 1, w, colors::BORDER); + // Draw border + draw_rect(fb, x, y, w, h, colors::BORDER); + // Titlebar bottom separator + draw_hline(fb, x, y + TITLEBAR_HEIGHT - 1, w, colors::BORDER); - // Draw window buttons (macOS style: close, minimize, maximize) - Rect close_r = win->close_btn_rect(); - Rect min_r = win->min_btn_rect(); - Rect max_r = win->max_btn_rect(); + // Draw window buttons (macOS style: close, minimize, maximize) + Rect close_r = win->close_btn_rect(); + Rect min_r = win->min_btn_rect(); + Rect max_r = win->max_btn_rect(); - fill_circle(fb, close_r.x + BTN_RADIUS, close_r.y + BTN_RADIUS, BTN_RADIUS, colors::CLOSE_BTN); - fill_circle(fb, min_r.x + BTN_RADIUS, min_r.y + BTN_RADIUS, BTN_RADIUS, colors::MIN_BTN); - fill_circle(fb, max_r.x + BTN_RADIUS, max_r.y + BTN_RADIUS, BTN_RADIUS, colors::MAX_BTN); + fill_circle(fb, close_r.x + BTN_RADIUS, close_r.y + BTN_RADIUS, BTN_RADIUS, colors::CLOSE_BTN); + fill_circle(fb, min_r.x + BTN_RADIUS, min_r.y + BTN_RADIUS, BTN_RADIUS, colors::MIN_BTN); + fill_circle(fb, max_r.x + BTN_RADIUS, max_r.y + BTN_RADIUS, BTN_RADIUS, colors::MAX_BTN); - // Draw title text centered in titlebar (after buttons) - int title_x = x + 12 + 44 + BTN_RADIUS * 2 + 12; // after buttons - int title_y = y + (TITLEBAR_HEIGHT - system_font_height()) / 2; - int title_w = text_width(win->title); - // Center in remaining space - int remaining_w = w - (title_x - x) - 12; - if (remaining_w > title_w) { - title_x += (remaining_w - title_w) / 2; - } - draw_text(fb, title_x, title_y, win->title, colors::TEXT_COLOR); + // Draw title text centered in titlebar (after buttons) + int title_x = x + 12 + 44 + BTN_RADIUS * 2 + 12; // after buttons + int title_y = y + (TITLEBAR_HEIGHT - system_font_height()) / 2; + int title_w = text_width(win->title); + // Center in remaining space + int remaining_w = w - (title_x - x) - 12; + if (remaining_w > title_w) { + title_x += (remaining_w - title_w) / 2; + } + draw_text(fb, title_x, title_y, win->title, colors::TEXT_COLOR); - // Call app draw callback to render content (skip during resize — buffer is old size) - if (win->on_draw && !win->resizing) { - win->on_draw(win, fb); + // Call app draw callback to render content (skip during resize — buffer is old size) + if (win->on_draw && !win->resizing) { + win->on_draw(win, fb); + } } // Blit content buffer to framebuffer (clip to actual buffer size during resize) - Rect cr = win->content_rect(); + Rect cr = desktop_content_rect(win); if (win->content) { if (win->external && (cr.w != win->content_w || cr.h != win->content_h)) { // Nearest-neighbor scale for external windows (fixed-size shared buffer) diff --git a/programs/src/disks/disks.h b/programs/src/disks/disks.h index 1d75d7d..481f2ce 100644 --- a/programs/src/disks/disks.h +++ b/programs/src/disks/disks.h @@ -25,29 +25,17 @@ using namespace gui; static constexpr int INIT_W = 640; static constexpr int INIT_H = 460; -static constexpr int TOOLBAR_H = 40; -static constexpr int HEADER_H = 26; +static constexpr int TOOLBAR_H = 44; +static constexpr int HEADER_H = 28; static constexpr int ITEM_H = 32; static constexpr int MAP_H = 48; static constexpr int MAP_PAD = 16; static constexpr int MAX_PARTS = 32; static constexpr int MAX_DISKS = 8; -static constexpr int STATUS_H = 26; +static constexpr int STATUS_H = 44; static constexpr int TB_BTN_Y = 7; -static constexpr int TB_BTN_H = 26; -static constexpr int TB_BTN_RAD = 6; - -static constexpr Color BG_COLOR = Color::from_rgb(0xFF, 0xFF, 0xFF); -static constexpr Color TOOLBAR_BG = Color::from_rgb(0xF5, 0xF5, 0xF5); -static constexpr Color HEADER_BG = Color::from_rgb(0xF0, 0xF0, 0xF0); -static constexpr Color BORDER_COLOR = Color::from_rgb(0xCC, 0xCC, 0xCC); -static constexpr Color TEXT_COLOR = Color::from_rgb(0x22, 0x22, 0x22); -static constexpr Color DIM_TEXT = Color::from_rgb(0x66, 0x66, 0x66); -static constexpr Color FAINT_TEXT = Color::from_rgb(0x88, 0x88, 0x88); -static constexpr Color HOVER_BG = Color::from_rgb(0xE8, 0xF0, 0xF8); -static constexpr Color STATUS_BG_COL = Color::from_rgb(0xF0, 0xF0, 0xF0); -static constexpr Color WHITE = Color::from_rgb(0xFF, 0xFF, 0xFF); +static constexpr int TB_BTN_H = 30; static constexpr int NUM_PART_COLORS = 8; @@ -119,6 +107,7 @@ struct DiskToolState { extern int g_win_w, g_win_h; extern DiskToolState g_state; +extern Color g_accent; // ============================================================================ // Partition colors @@ -133,33 +122,34 @@ extern const Color part_colors[NUM_PART_COLORS]; inline int disk_button_width(int idx) { char label[8]; snprintf(label, sizeof(label), "Disk %d", idx); - return text_width(label) + 16; + return text_width(label) + 20; } inline Rect toolbar_disk_button_rect(int idx) { int bx = 8; for (int i = 0; i < idx; i++) - bx += disk_button_width(i) + 6; + bx += disk_button_width(i) + 8; return {bx, TB_BTN_Y, disk_button_width(idx), TB_BTN_H}; } inline Rect toolbar_refresh_button_rect() { - return {g_win_w - 8 - 64, TB_BTN_Y, 64, TB_BTN_H}; + int y = g_win_h - STATUS_H + (STATUS_H - TB_BTN_H) / 2; + return {g_win_w - 12 - 84, y, 84, TB_BTN_H}; } inline Rect toolbar_mount_button_rect() { Rect refresh = toolbar_refresh_button_rect(); - return {refresh.x - 6 - 60, TB_BTN_Y, 60, TB_BTN_H}; + return {refresh.x - 8 - 72, refresh.y, 72, TB_BTN_H}; } inline Rect toolbar_format_button_rect() { Rect mount = toolbar_mount_button_rect(); - return {mount.x - 6 - 64, TB_BTN_Y, 64, TB_BTN_H}; + return {mount.x - 8 - 72, mount.y, 72, TB_BTN_H}; } inline Rect toolbar_newpart_button_rect() { Rect format = toolbar_format_button_rect(); - return {format.x - 6 - 74, TB_BTN_Y, 74, TB_BTN_H}; + return {format.x - 8 - 96, format.y, 96, TB_BTN_H}; } inline int content_title_y() { @@ -188,15 +178,13 @@ inline Rect format_option_rect(int dialog_w, int index) { inline Rect dialog_primary_button_rect(int dialog_w, int dialog_h) { int btn_w = 90; int btn_h = 30; - int gap = 16; - int total_w = btn_w * 2 + gap; - int bx = (dialog_w - total_w) / 2; - return {bx, dialog_h - btn_h - 16, btn_w, btn_h}; + int bx = dialog_w - btn_w - 16; + return {bx, dialog_h - btn_h - 12, btn_w, btn_h}; } inline Rect dialog_secondary_button_rect(int dialog_w, int dialog_h) { Rect primary = dialog_primary_button_rect(dialog_w, dialog_h); - return {primary.x + primary.w + 16, primary.y, primary.w, primary.h}; + return {primary.x - 8 - primary.w, primary.y, primary.w, primary.h}; } // ============================================================================ diff --git a/programs/src/disks/main.cpp b/programs/src/disks/main.cpp index 259fe9a..73f83af 100644 --- a/programs/src/disks/main.cpp +++ b/programs/src/disks/main.cpp @@ -5,6 +5,7 @@ */ #include "disks.h" +#include #include // ============================================================================ @@ -15,6 +16,7 @@ int g_win_w = INIT_W; int g_win_h = INIT_H; DiskToolState g_state; +Color g_accent = colors::ACCENT; const Color part_colors[NUM_PART_COLORS] = { Color::from_rgb(0x42, 0x7A, 0xB5), @@ -73,7 +75,6 @@ static bool handle_toolbar_click(int mx, int my) { auto& dt = g_state; if (dt.disk_count == 0) return false; - bool has_sel = dt.selected_part >= 0; // Disk selector buttons for (int i = 0; i < dt.disk_count; i++) { @@ -85,7 +86,16 @@ static bool handle_toolbar_click(int mx, int my) { } } - // Right-side buttons (must match render layout) + return false; +} + +static bool handle_action_click(int mx, int my) { + if (my < g_win_h - STATUS_H) return false; + + auto& dt = g_state; + if (dt.disk_count == 0) return false; + bool has_sel = dt.selected_part >= 0; + if (toolbar_refresh_button_rect().contains(mx, my)) { disktool_refresh(); set_status("Refreshed"); @@ -285,7 +295,9 @@ static bool handle_mouse(const Montauk::WinEvent& ev) { bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1); if (clicked) { - if (handle_toolbar_click(ev.mouse.x, ev.mouse.y) || handle_content_click(ev.mouse.x, ev.mouse.y)) + if (handle_toolbar_click(ev.mouse.x, ev.mouse.y) || + handle_action_click(ev.mouse.x, ev.mouse.y) || + handle_content_click(ev.mouse.x, ev.mouse.y)) return true; } @@ -365,6 +377,8 @@ extern "C" void _start() { if (!fonts::init()) montauk::exit(1); + g_accent = mtk::load_system_accent(); + disktool_refresh(); WsWindow win; diff --git a/programs/src/disks/render.cpp b/programs/src/disks/render.cpp index 0b63ee8..e2fa01e 100644 --- a/programs/src/disks/render.cpp +++ b/programs/src/disks/render.cpp @@ -9,22 +9,20 @@ #include static mtk::Theme disks_theme() { - mtk::Theme theme = mtk::make_theme(); - theme.window_bg = BG_COLOR; - theme.surface = TOOLBAR_BG; - theme.surface_alt = HEADER_BG; - theme.surface_hover = HOVER_BG; - theme.border = BORDER_COLOR; - theme.text = TEXT_COLOR; - theme.text_muted = FAINT_TEXT; - theme.text_subtle = DIM_TEXT; - theme.selection = HOVER_BG; - theme.accent_soft = HOVER_BG; - theme.disabled_bg = Color::from_rgb(0xF0, 0xF0, 0xF0); + mtk::Theme theme = mtk::make_theme(g_accent); + theme.selection = theme.accent_soft; + theme.disabled_bg = Color::from_rgb(0xE4, 0xE4, 0xE4); theme.disabled_fg = Color::from_rgb(0x9A, 0x9A, 0x9A); return theme; } +static mtk::TableStyle partition_table_style(const mtk::Theme& theme) { + mtk::TableStyle style = mtk::make_table_style(theme); + style.row_selected_bg = theme.accent_soft; + style.row_hover_bg = mtk::mix(theme.window_bg, theme.accent, 12); + return style; +} + static bool mouse_in_rect(const Rect& rect) { return rect.contains(g_state.mouse_x, g_state.mouse_y); } @@ -79,6 +77,13 @@ static void draw_centered_text(Canvas& c, int width, int y, const char* text, Co c.text((width - tw) / 2, y, text, color); } +static void draw_dialog_footer(Canvas& c, int dialog_w, int dialog_h, const mtk::Theme& theme) { + constexpr int FOOTER_H = 54; + int y = dialog_h - FOOTER_H; + c.fill_rect(0, y, dialog_w, FOOTER_H, theme.surface); + mtk::draw_separator(c, 0, y, dialog_w, theme); +} + static void render_toolbar(Canvas& c, const mtk::Theme& theme) { auto& dt = g_state; int fh = system_font_height(); @@ -98,21 +103,6 @@ static void render_toolbar(Canvas& c, const mtk::Theme& theme) { c.text(8, (TOOLBAR_H - fh) / 2, "No disks detected", theme.text); return; } - - bool has_sel = dt.selected_part >= 0; - Rect newpart = toolbar_newpart_button_rect(); - Rect format = toolbar_format_button_rect(); - Rect mount = toolbar_mount_button_rect(); - Rect refresh = toolbar_refresh_button_rect(); - - mtk::draw_button(c, newpart, "New Part", mtk::BUTTON_PRIMARY, - toolbar_button_state(newpart, true), theme); - mtk::draw_button(c, format, "Format", mtk::BUTTON_TONAL, - toolbar_button_state(format, has_sel), theme); - mtk::draw_button(c, mount, "Mount", mtk::BUTTON_TONAL, - toolbar_button_state(mount, has_sel), theme); - mtk::draw_button(c, refresh, "Refresh", mtk::BUTTON_SECONDARY, - toolbar_button_state(refresh, true), theme); } static void render_content(Canvas& c, const mtk::Theme& theme) { @@ -140,7 +130,7 @@ static void render_content(Canvas& c, const mtk::Theme& theme) { y = partition_map_y(); Rect map_rect = {MAP_PAD, y, g_win_w - MAP_PAD * 2, MAP_H}; - c.fill_rounded_rect(map_rect.x, map_rect.y, map_rect.w, map_rect.h, theme.radius_md, theme.surface_alt); + mtk::draw_rounded_frame(c, map_rect, theme.radius_md, theme.surface_alt, theme.border); int part_indices[MAX_PARTS]; int nparts = get_disk_parts(part_indices, MAX_PARTS); @@ -155,45 +145,60 @@ static void render_content(Canvas& c, const mtk::Theme& theme) { if (part_x + part_w > map_rect.x + map_rect.w) part_w = map_rect.x + map_rect.w - part_x; - Color col = part_colors[pi % NUM_PART_COLORS]; + Rect segment = {part_x, map_rect.y + 3, part_w, MAP_H - 6}; if (pi == dt.selected_part) { - col = Color::from_rgb((col.r + 255) / 2, - (col.g + 255) / 2, - (col.b + 255) / 2); + c.fill_rounded_rect(segment.x - 1, segment.y - 1, + segment.w + 2, segment.h + 2, + theme.radius_sm, theme.accent); } - c.fill_rounded_rect(part_x, map_rect.y + 2, part_w, MAP_H - 4, theme.radius_sm, col); + + Color col = part_colors[pi % NUM_PART_COLORS]; + c.fill_rounded_rect(segment.x, segment.y, segment.w, segment.h, + theme.radius_sm, col); char label[8]; snprintf(label, sizeof(label), "%d", pi); int label_w = text_width(label); if (part_w > label_w + 4) { - c.text(part_x + (part_w - label_w) / 2, + c.text(segment.x + (segment.w - label_w) / 2, map_rect.y + (MAP_H - fh) / 2, - label, WHITE); + label, theme.text_inverse); } } } y = partition_header_y(); - c.fill_rect(0, y, g_win_w, HEADER_H, theme.surface_alt); - mtk::draw_separator(c, 0, y + HEADER_H - 1, g_win_w, theme); + int list_y = partition_list_y(); + int list_bottom = g_win_h - STATUS_H; + int hovered_row = hovered_partition_row(); + mtk::TableStyle table_style = partition_table_style(theme); + mtk::TableLayout table = { + {0, y, g_win_w, HEADER_H}, + {0, list_y, g_win_w, list_bottom - list_y}, + ITEM_H + }; - int ty = y + (HEADER_H - fh) / 2; int col_idx = 12; int col_name = 40; int col_type = g_win_w / 2 - 20; int col_size = g_win_w - 160; int col_lba = g_win_w - 80; + mtk::TableColumn cols[5] = { + {"#", col_idx, col_name - col_idx - 8}, + {"Name", col_name, col_type - col_name - 10}, + {"Type", col_type, col_size - col_type - 10}, + {"Size", col_size, col_lba - col_size - 10}, + {"LBA", col_lba, g_win_w - col_lba - 12}, + }; - c.text(col_idx, ty, "#", theme.text_subtle); - c.text(col_name, ty, "Name", theme.text_subtle); - c.text(col_type, ty, "Type", theme.text_subtle); - c.text(col_size, ty, "Size", theme.text_subtle); - c.text(col_lba, ty, "LBA", theme.text_subtle); + c.fill_rect(table.header.x, table.header.y, table.header.w, table.header.h, + table_style.header_bg); + c.hline(table.header.x, table.header.y + table.header.h - 1, + table.header.w, table_style.header_border); - int list_y = partition_list_y(); - int list_bottom = g_win_h - STATUS_H; - int hovered_row = hovered_partition_row(); + int ty = table.header.y + (table.header.h - fh) / 2; + for (int i = 0; i < 5; i++) + c.text(table.header.x + cols[i].x, ty, cols[i].label, table_style.header_text); for (int pi = 0; pi < nparts; pi++) { int row_y = list_y + pi * ITEM_H - dt.scroll_y; @@ -202,11 +207,11 @@ static void render_content(Canvas& c, const mtk::Theme& theme) { Rect row_rect = {0, row_y, g_win_w, ITEM_H}; if (pi == dt.selected_part) { - mtk::draw_list_row(c, row_rect, true, false, theme); + c.fill_rect(row_rect.x, row_rect.y, row_rect.w, row_rect.h, + table_style.row_selected_bg); } else if (pi == hovered_row) { - c.fill_rect(row_rect.x, row_rect.y, row_rect.w, row_rect.h, theme.surface_hover); - } else { - mtk::draw_list_row(c, row_rect, false, (pi & 1) != 0, theme); + c.fill_rect(row_rect.x, row_rect.y, row_rect.w, row_rect.h, + table_style.row_hover_bg); } Montauk::PartInfo& p = dt.parts[part_indices[pi]]; @@ -216,17 +221,17 @@ static void render_content(Canvas& c, const mtk::Theme& theme) { int ry = row_y + (ITEM_H - fh) / 2; draw_text_fit(c, col_name, ry, p.name[0] ? p.name : "(unnamed)", - col_type - col_name - 10, theme.text); + cols[1].w, theme.text); draw_text_fit(c, col_type, ry, p.typeName, - col_size - col_type - 10, theme.text_subtle); + cols[2].w, theme.text_subtle); char sz[24]; format_disk_size(sz, sizeof(sz), p.sectorCount, disk.sectorSizeLog); - c.text(col_size, ry, sz, theme.text); + draw_text_fit(c, col_size, ry, sz, cols[3].w, theme.text); char lba_str[24]; snprintf(lba_str, sizeof(lba_str), "%lu", (unsigned)p.startLba); - c.text(col_lba, ry, lba_str, theme.text_muted); + draw_text_fit(c, col_lba, ry, lba_str, cols[4].w, theme.text_muted); } if (nparts == 0) @@ -238,13 +243,34 @@ static void render_status(Canvas& c, const mtk::Theme& theme) { int fh = system_font_height(); int sy = g_win_h - STATUS_H; - c.fill_rect(0, sy, g_win_w, STATUS_H, theme.surface_alt); + c.fill_rect(0, sy, g_win_w, STATUS_H, theme.surface); mtk::draw_separator(c, 0, sy, g_win_w, theme); + bool has_disks = dt.disk_count > 0; + bool has_sel = dt.selected_part >= 0; + int text_right = g_win_w - 16; + if (has_disks) { + Rect newpart = toolbar_newpart_button_rect(); + Rect format = toolbar_format_button_rect(); + Rect mount = toolbar_mount_button_rect(); + Rect refresh = toolbar_refresh_button_rect(); + text_right = newpart.x - 12; + + mtk::draw_button(c, newpart, "New Part", mtk::BUTTON_PRIMARY, + toolbar_button_state(newpart, true), theme); + mtk::draw_button(c, format, "Format", mtk::BUTTON_TONAL, + toolbar_button_state(format, has_sel), theme); + mtk::draw_button(c, mount, "Mount", mtk::BUTTON_TONAL, + toolbar_button_state(mount, has_sel), theme); + mtk::draw_button(c, refresh, "Refresh", mtk::BUTTON_SECONDARY, + toolbar_button_state(refresh, true), theme); + } + if (dt.status[0]) { uint64_t age = montauk::get_milliseconds() - dt.status_time; Color color = age < 5000 ? theme.text : theme.text_muted; - c.text(8, sy + (STATUS_H - fh) / 2, dt.status, color); + draw_text_fit(c, 16, sy + (STATUS_H - fh) / 2, dt.status, + text_right - 16, color); } } @@ -265,11 +291,14 @@ void render_format_window() { for (int i = 0; i < NUM_FS_TYPES; i++) { Rect option = format_option_rect(FMT_DLG_W, i); - if (i == dlg.selected_fs) - c.fill_rounded_rect(option.x, option.y, option.w, option.h, theme.radius_md, theme.accent_soft); + if (i == dlg.selected_fs) { + mtk::draw_rounded_frame(c, option, theme.radius_md, + theme.accent_soft, theme.accent_soft); + } mtk::draw_radio(c, option, g_fsTypes[i].name, i == dlg.selected_fs, theme); } + draw_dialog_footer(c, FMT_DLG_W, FMT_DLG_H, theme); mtk::draw_modal_actions(c, dialog_primary_button_rect(FMT_DLG_W, FMT_DLG_H), "Format", mtk::BUTTON_DANGER, @@ -294,11 +323,15 @@ void render_newpart_window() { Color warn_color = dlg.will_init_gpt ? theme.danger : theme.text; int warn_y = 12 + fh * 2 + 20; + Rect warn_panel = {16, warn_y - 8, NP_DLG_W - 32, fh * 2 + 20}; + Color warn_bg = dlg.will_init_gpt ? theme.danger_soft : theme.surface_alt; + mtk::draw_rounded_frame(c, warn_panel, theme.radius_md, warn_bg, warn_bg); draw_centered_text(c, NP_DLG_W, warn_y, dlg.warn_line1, warn_color); draw_centered_text(c, NP_DLG_W, warn_y + fh + 4, dlg.warn_line2, warn_color); const char* confirm_label = dlg.will_init_gpt ? "Initialize" : "Create"; mtk::ButtonVariant confirm_variant = dlg.will_init_gpt ? mtk::BUTTON_DANGER : mtk::BUTTON_PRIMARY; + draw_dialog_footer(c, NP_DLG_W, NP_DLG_H, theme); mtk::draw_modal_actions(c, dialog_primary_button_rect(NP_DLG_W, NP_DLG_H), confirm_label, confirm_variant, diff --git a/programs/src/screenshot/Makefile b/programs/src/screenshot/Makefile index 30b1d61..66b1943 100644 --- a/programs/src/screenshot/Makefile +++ b/programs/src/screenshot/Makefile @@ -18,6 +18,7 @@ endif PROJECT_ROOT := $(shell cd ../../.. && pwd) PROGRAMS := $(PROJECT_ROOT)/programs JPEGWRITE_LIB := $(PROGRAMS)/lib/libjpegwrite +LOADER_LIB := $(PROGRAMS)/lib/libloader LIBC_LIB := $(PROGRAMS)/lib/libc PROG_INC := $(PROGRAMS)/include LINK_LD := $(PROGRAMS)/link.ld @@ -66,11 +67,11 @@ LDFLAGS := \ # ---- Libraries ---- -LIBS := $(JPEGWRITE_LIB)/libjpegwrite.a $(LIBC_LIB)/liblibc.a +LIBS := $(JPEGWRITE_LIB)/libjpegwrite.a $(LOADER_LIB)/liblibloader.a $(LIBC_LIB)/liblibc.a # ---- Source files ---- -SRCS := main.cpp +SRCS := main.cpp stb_truetype_impl.cpp OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o)) # ---- Target ---- diff --git a/programs/src/screenshot/main.cpp b/programs/src/screenshot/main.cpp index 58f18af..9ae2de1 100644 --- a/programs/src/screenshot/main.cpp +++ b/programs/src/screenshot/main.cpp @@ -1,13 +1,17 @@ /* * main.cpp * MontaukOS Screenshot Tool - * Captures the screen framebuffer and saves as JPEG to the user's Pictures directory. + * Captures the screen framebuffer and saves JPEG screenshots through a GUI flow. * Copyright (c) 2026 Daniel Hammer */ #include #include #include +#include +#include +#include +#include extern "C" { #include @@ -18,26 +22,775 @@ extern "C" { #include } +using namespace gui; + // ============================================================================ -// JPEG write callback - accumulates encoded bytes into a resizable buffer +// Constants // ============================================================================ +static constexpr int INIT_W = 420; +static constexpr int INIT_H = 230; +static constexpr int PAD = 18; +static constexpr int OPTION_H = 44; +static constexpr int OPTION_GAP = 8; +static constexpr int FOOTER_H = 56; +static constexpr int BUTTON_W = 132; +static constexpr int BUTTON_H = 30; +static constexpr int MIN_AREA_SIZE = 4; + +// ============================================================================ +// App state +// ============================================================================ + +enum CaptureMode : uint8_t { + CAPTURE_SCREEN = 0, + CAPTURE_AREA = 1, +}; + +struct ScreenBuffer { + uint32_t* pixels; + int width; + int height; + int pitch; +}; + struct JpegBuffer { uint8_t* data; uint64_t size; uint64_t capacity; + bool failed; }; +struct AreaRenderCache { + uint32_t* dimmed_pixels; + int width; + int height; +}; + +static int g_win_w = INIT_W; +static int g_win_h = INIT_H; +static int g_mouse_x = -1; +static int g_mouse_y = -1; +static CaptureMode g_mode = CAPTURE_SCREEN; + +// ============================================================================ +// Small helpers +// ============================================================================ + +static void safe_copy(char* dst, int dst_len, const char* src) { + if (!dst || dst_len <= 0) return; + if (!src) src = ""; + montauk::strncpy(dst, src, dst_len); + dst[dst_len - 1] = '\0'; +} + +static bool str_ends_with_ci(const char* s, const char* suffix) { + if (!s || !suffix) return false; + int slen = montauk::slen(s); + int suflen = montauk::slen(suffix); + if (suflen > slen) return false; + + const char* tail = s + slen - suflen; + for (int i = 0; i < suflen; i++) { + char a = tail[i]; + char b = suffix[i]; + if (a >= 'A' && a <= 'Z') a += 32; + if (b >= 'A' && b <= 'Z') b += 32; + if (a != b) return false; + } + return true; +} + +static void show_error(const char* message) { + if (message && message[0]) { + montauk::print("screenshot: "); + montauk::print(message); + montauk::print("\n"); + gui::dialogs::message_box("Screenshot", message, gui::dialogs::MESSAGE_BOX_OK); + } +} + +static void free_screen(ScreenBuffer* screen) { + if (!screen) return; + if (screen->pixels) montauk::mfree(screen->pixels); + screen->pixels = nullptr; + screen->width = 0; + screen->height = 0; + screen->pitch = 0; +} + +static Rect normalized_rect(int x0, int y0, int x1, int y1) { + int left = x0 < x1 ? x0 : x1; + int top = y0 < y1 ? y0 : y1; + int right = x0 > x1 ? x0 : x1; + int bottom = y0 > y1 ? y0 : y1; + return {left, top, right - left + 1, bottom - top + 1}; +} + +static Rect clamp_rect_to_screen(const Rect& rect, int w, int h) { + Rect bounds = {0, 0, w, h}; + return rect.intersect(bounds); +} + +// ============================================================================ +// MTK picker window +// ============================================================================ + +static mtk::Theme app_theme() { + mtk::Theme theme = mtk::make_theme(); + theme.window_bg = colors::WHITE; + theme.surface = Color::from_rgb(0xF5, 0xF5, 0xF5); + theme.surface_alt = Color::from_rgb(0xFA, 0xFA, 0xFA); + theme.surface_hover = mtk::mix(theme.surface, theme.accent, 18); + theme.border = Color::from_rgb(0xD0, 0xD0, 0xD0); + theme.text = Color::from_rgb(0x2E, 0x2E, 0x2E); + theme.text_muted = Color::from_rgb(0x74, 0x74, 0x74); + theme.text_subtle = Color::from_rgb(0x88, 0x88, 0x88); + return theme; +} + +static Rect option_rect(int index) { + int top = 48 + index * (OPTION_H + OPTION_GAP); + return {PAD, top, gui_max(g_win_w - PAD * 2, 0), OPTION_H}; +} + +static Rect capture_button_rect() { + int y = g_win_h - FOOTER_H + (FOOTER_H - BUTTON_H) / 2; + int w = gui_min(BUTTON_W, gui_max((g_win_w - PAD * 3) / 2, 72)); + return {g_win_w - PAD - w, y, w, BUTTON_H}; +} + +static Rect cancel_button_rect() { + Rect capture = capture_button_rect(); + int w = capture.w; + return {capture.x - 8 - w, capture.y, w, BUTTON_H}; +} + +static bool mouse_in_rect(const Rect& rect) { + return rect.contains(g_mouse_x, g_mouse_y); +} + +static void draw_option(Canvas& c, + const Rect& rect, + CaptureMode mode, + const char* label, + const mtk::Theme& theme) { + if (rect.empty()) return; + + bool selected = g_mode == mode; + bool hovered = mouse_in_rect(rect); + if (selected) { + Color fill = hovered ? mtk::mix(theme.accent_soft, theme.accent, 14) + : theme.accent_soft; + mtk::draw_rounded_frame(c, rect, theme.radius_md, fill, fill); + } else if (hovered) { + mtk::draw_rounded_frame(c, rect, theme.radius_md, + theme.surface_hover, theme.surface_hover); + } + + Rect radio = {rect.x + 12, rect.y + (rect.h - 24) / 2, rect.w - 24, 24}; + mtk::draw_radio(c, radio, label, selected, theme); +} + +static void render_picker(Canvas& c) { + mtk::Theme theme = app_theme(); + c.fill(theme.window_bg); + + c.text(PAD, 16, "Capture mode", theme.text); + + draw_option(c, option_rect(0), CAPTURE_SCREEN, "Whole screen", theme); + draw_option(c, option_rect(1), CAPTURE_AREA, "Selected area", theme); + + int footer_y = gui_max(g_win_h - FOOTER_H, 0); + c.fill_rect(0, footer_y, g_win_w, gui_max(g_win_h - footer_y, 0), theme.surface); + mtk::draw_separator(c, 0, footer_y, g_win_w, theme); + + Rect cancel = cancel_button_rect(); + Rect capture = capture_button_rect(); + mtk::draw_button(c, cancel, "Cancel", mtk::BUTTON_SECONDARY, + mtk::widget_state(false, mouse_in_rect(cancel)), theme); + mtk::draw_button(c, capture, "Capture", mtk::BUTTON_PRIMARY, + mtk::widget_state(false, mouse_in_rect(capture)), theme); +} + +static bool run_picker(CaptureMode* out_mode) { + if (!fonts::init()) { + show_error("failed to initialize fonts"); + return false; + } + + WsWindow win; + if (!win.create("Screenshot", INIT_W, INIT_H)) { + show_error("failed to create window"); + return false; + } + + mtk::StandaloneHost host(&win); + Canvas canvas = host.canvas(); + render_picker(canvas); + host.present(); + + bool accepted = false; + while (true) { + Montauk::WinEvent ev; + int r = win.poll(&ev); + + if (r < 0) break; + if (r == 0) { + montauk::sleep_ms(16); + continue; + } + + bool redraw = false; + if (ev.type == 3) { + break; + } else if (ev.type == 2) { + g_win_w = win.width; + g_win_h = win.height; + redraw = true; + } else if (ev.type == 4) { + redraw = true; + } else if (ev.type == 0 && ev.key.pressed) { + if (ev.key.scancode == 0x01) break; + if (ev.key.ascii == '\n' || ev.key.ascii == '\r') { + accepted = true; + break; + } + if (ev.key.ascii == '1') { + g_mode = CAPTURE_SCREEN; + redraw = true; + } else if (ev.key.ascii == '2') { + g_mode = CAPTURE_AREA; + redraw = true; + } + } else if (ev.type == 1) { + g_mouse_x = ev.mouse.x; + g_mouse_y = ev.mouse.y; + redraw = true; + + bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1); + if (clicked) { + if (option_rect(0).contains(ev.mouse.x, ev.mouse.y)) { + g_mode = CAPTURE_SCREEN; + } else if (option_rect(1).contains(ev.mouse.x, ev.mouse.y)) { + g_mode = CAPTURE_AREA; + } else if (cancel_button_rect().contains(ev.mouse.x, ev.mouse.y)) { + break; + } else if (capture_button_rect().contains(ev.mouse.x, ev.mouse.y)) { + accepted = true; + break; + } + } + } + + if (redraw) { + canvas = host.canvas(); + render_picker(canvas); + host.present(); + } + } + + if (out_mode) *out_mode = g_mode; + win.destroy(); + return accepted; +} + +// ============================================================================ +// Framebuffer capture +// ============================================================================ + +static bool capture_screen(ScreenBuffer* out, char* err, int err_len) { + if (!out) return false; + out->pixels = nullptr; + out->width = 0; + out->height = 0; + out->pitch = 0; + + Montauk::FbInfo fb; + montauk::fb_info(&fb); + + int width = (int)fb.width; + int height = (int)fb.height; + int pitch = (int)fb.pitch; + if (width <= 0 || height <= 0 || pitch <= 0) { + safe_copy(err, err_len, "no framebuffer available"); + return false; + } + + uint32_t* hw_fb = (uint32_t*)montauk::fb_map(); + if (!hw_fb) { + safe_copy(err, err_len, "failed to map framebuffer"); + return false; + } + + uint64_t size = (uint64_t)width * (uint64_t)height * sizeof(uint32_t); + uint32_t* pixels = (uint32_t*)montauk::malloc(size); + if (!pixels) { + safe_copy(err, err_len, "out of memory"); + return false; + } + + for (int y = 0; y < height; y++) { + uint32_t* src = (uint32_t*)((uint8_t*)hw_fb + y * pitch); + montauk::memcpy(pixels + y * width, src, (uint64_t)width * sizeof(uint32_t)); + } + + out->pixels = pixels; + out->width = width; + out->height = height; + out->pitch = pitch; + return true; +} + +static uint32_t dim_pixel(uint32_t px) { + uint8_t r = (uint8_t)((px >> 16) & 0xFF); + uint8_t g = (uint8_t)((px >> 8) & 0xFF); + uint8_t b = (uint8_t)(px & 0xFF); + r = (uint8_t)((uint16_t)r * 3 / 5); + g = (uint8_t)((uint16_t)g * 3 / 5); + b = (uint8_t)((uint16_t)b * 3 / 5); + return (px & 0xFF000000u) | ((uint32_t)r << 16) | ((uint32_t)g << 8) | b; +} + +static void free_area_render_cache(AreaRenderCache* cache) { + if (!cache) return; + if (cache->dimmed_pixels) + montauk::mfree(cache->dimmed_pixels); + cache->dimmed_pixels = nullptr; + cache->width = 0; + cache->height = 0; +} + +static void build_area_render_cache(const ScreenBuffer& screen, AreaRenderCache* cache) { + if (!cache || !screen.pixels || screen.width <= 0 || screen.height <= 0) + return; + + uint64_t count = (uint64_t)screen.width * (uint64_t)screen.height; + uint64_t bytes = count * sizeof(uint32_t); + uint32_t* dimmed = (uint32_t*)montauk::malloc(bytes); + if (!dimmed) + return; + + for (uint64_t i = 0; i < count; i++) + dimmed[i] = dim_pixel(screen.pixels[i]); + + free_area_render_cache(cache); + cache->dimmed_pixels = dimmed; + cache->width = screen.width; + cache->height = screen.height; +} + +// ============================================================================ +// Area selection window +// ============================================================================ + +struct AreaSelectorState { + int win_w; + int win_h; + int mouse_x; + int mouse_y; + bool dragging; + bool has_selection; + bool fullscreen; + int drag_start_x; + int drag_start_y; + Rect selection_view; +}; + +static int area_initial_w(const ScreenBuffer& screen) { + int w = screen.width - 120; + return gui_clamp(w, 360, 760); +} + +static int area_initial_h(const ScreenBuffer& screen) { + int h = screen.height - 140; + return gui_clamp(h, 280, 560); +} + +static Rect area_footer_rect(const AreaSelectorState& st) { + return {0, st.win_h - FOOTER_H, st.win_w, FOOTER_H}; +} + +static Rect area_cancel_button_rect(const AreaSelectorState& st) { + Rect footer = area_footer_rect(st); + int w = gui_min(BUTTON_W, gui_max((st.win_w - PAD * 3) / 2, 72)); + int y = footer.y + (footer.h - BUTTON_H) / 2; + return {st.win_w - PAD - w * 2 - 8, y, w, BUTTON_H}; +} + +static Rect area_capture_button_rect(const AreaSelectorState& st) { + Rect footer = area_footer_rect(st); + int w = gui_min(BUTTON_W, gui_max((st.win_w - PAD * 3) / 2, 72)); + int y = footer.y + (footer.h - BUTTON_H) / 2; + return {st.win_w - PAD - w, y, w, BUTTON_H}; +} + +static Rect area_preview_rect(const ScreenBuffer& screen, const AreaSelectorState& st) { + if (st.fullscreen) { + return {0, 0, gui_max(st.win_w, 1), gui_max(st.win_h, 1)}; + } + + Rect content = {PAD, PAD, + gui_max(st.win_w - PAD * 2, 1), + gui_max(st.win_h - FOOTER_H - PAD * 2, 1)}; + if (screen.width <= 0 || screen.height <= 0) return content; + + int preview_w = content.w; + int preview_h = (int)((uint64_t)preview_w * (uint64_t)screen.height / (uint64_t)screen.width); + if (preview_h > content.h) { + preview_h = content.h; + preview_w = (int)((uint64_t)preview_h * (uint64_t)screen.width / (uint64_t)screen.height); + } + if (preview_w < 1) preview_w = 1; + if (preview_h < 1) preview_h = 1; + + return {content.x + (content.w - preview_w) / 2, + content.y + (content.h - preview_h) / 2, + preview_w, + preview_h}; +} + +static bool area_mouse_in(const AreaSelectorState& st, const Rect& rect) { + return rect.contains(st.mouse_x, st.mouse_y); +} + +static Rect clamp_rect_to_preview(const Rect& rect, const Rect& preview) { + return rect.intersect(preview); +} + +static bool draw_fullscreen_preview(Canvas& c, + const ScreenBuffer& screen, + const Rect& preview, + const Rect& selection, + bool has_selection, + const AreaRenderCache* cache) { + if (!screen.pixels || preview.x != 0 || preview.y != 0 || + preview.w != c.w || preview.h != c.h || + screen.width != c.w || screen.height != c.h) { + return false; + } + + uint64_t bytes = (uint64_t)c.w * (uint64_t)c.h * sizeof(uint32_t); + Rect sel = clamp_rect_to_preview(selection, preview); + bool draw_dimmed = has_selection && !sel.empty() && + cache && cache->dimmed_pixels && + cache->width == screen.width && + cache->height == screen.height; + + if (draw_dimmed) + montauk::memcpy(c.pixels, cache->dimmed_pixels, bytes); + else + montauk::memcpy(c.pixels, screen.pixels, bytes); + + if (draw_dimmed) { + for (int y = 0; y < sel.h; y++) { + uint32_t* dst = c.pixels + (sel.y + y) * c.w + sel.x; + uint32_t* src = screen.pixels + (sel.y + y) * screen.width + sel.x; + montauk::memcpy(dst, src, (uint64_t)sel.w * sizeof(uint32_t)); + } + } + + return true; +} + +static void draw_scaled_preview(Canvas& c, + const ScreenBuffer& screen, + const Rect& preview, + const Rect& selection, + bool has_selection, + const AreaRenderCache* cache) { + if (!screen.pixels || preview.empty()) return; + if (draw_fullscreen_preview(c, screen, preview, selection, has_selection, cache)) + return; + + Rect sel = clamp_rect_to_preview(selection, preview); + for (int y = 0; y < preview.h; y++) { + int dst_y = preview.y + y; + if (dst_y < 0 || dst_y >= c.h) continue; + int src_y = (int)((uint64_t)y * (uint64_t)screen.height / (uint64_t)preview.h); + uint32_t* src_row = screen.pixels + src_y * screen.width; + uint32_t* dst_row = c.pixels + dst_y * c.w; + + bool y_in_sel = has_selection && dst_y >= sel.y && dst_y < sel.y + sel.h; + for (int x = 0; x < preview.w; x++) { + int dst_x = preview.x + x; + if (dst_x < 0 || dst_x >= c.w) continue; + int src_x = (int)((uint64_t)x * (uint64_t)screen.width / (uint64_t)preview.w); + uint32_t px = src_row[src_x]; + bool in_sel = y_in_sel && dst_x >= sel.x && dst_x < sel.x + sel.w; + dst_row[dst_x] = (!has_selection || in_sel) ? px : dim_pixel(px); + } + } +} + +static void draw_area_selector(Canvas& c, + const ScreenBuffer& screen, + const AreaSelectorState& st, + const AreaRenderCache* cache) { + mtk::Theme theme = app_theme(); + if (!st.fullscreen) + c.fill(theme.window_bg); + + Rect preview = area_preview_rect(screen, st); + if (!st.fullscreen) + c.fill_rect(preview.x - 1, preview.y - 1, preview.w + 2, preview.h + 2, theme.border); + draw_scaled_preview(c, screen, preview, st.selection_view, st.has_selection || st.dragging, cache); + + if ((st.has_selection || st.dragging) && !st.selection_view.empty()) { + Rect sel = clamp_rect_to_preview(st.selection_view, preview); + if (!sel.empty()) { + c.rect(sel.x, sel.y, sel.w, sel.h, colors::WHITE); + if (sel.w > 2 && sel.h > 2) { + Rect inner = {sel.x + 1, sel.y + 1, sel.w - 2, sel.h - 2}; + c.rect(inner.x, inner.y, inner.w, inner.h, theme.accent); + } + } + } + + if (st.fullscreen) + return; + + Rect footer = area_footer_rect(st); + c.fill_rect(footer.x, footer.y, footer.w, footer.h, theme.surface); + mtk::draw_separator(c, 0, footer.y, st.win_w, theme); + + if (st.has_selection) { + Rect screen_sel = {0, 0, 0, 0}; + Rect sel = clamp_rect_to_preview(st.selection_view, preview); + if (!sel.empty()) { + int left = (int)((uint64_t)(sel.x - preview.x) * (uint64_t)screen.width / (uint64_t)preview.w); + int top = (int)((uint64_t)(sel.y - preview.y) * (uint64_t)screen.height / (uint64_t)preview.h); + int right = (int)((uint64_t)(sel.x + sel.w - preview.x) * (uint64_t)screen.width / (uint64_t)preview.w); + int bottom = (int)((uint64_t)(sel.y + sel.h - preview.y) * (uint64_t)screen.height / (uint64_t)preview.h); + screen_sel = clamp_rect_to_screen({left, top, right - left, bottom - top}, + screen.width, screen.height); + } + + char size_label[48]; + snprintf(size_label, sizeof(size_label), "%d x %d", screen_sel.w, screen_sel.h); + c.text(PAD, footer.y + (footer.h - system_font_height()) / 2, + size_label, theme.text_muted); + } + + Rect cancel = area_cancel_button_rect(st); + Rect capture = area_capture_button_rect(st); + mtk::draw_button(c, cancel, "Cancel", mtk::BUTTON_SECONDARY, + mtk::widget_state(false, area_mouse_in(st, cancel)), theme); + mtk::draw_button(c, capture, "Capture", mtk::BUTTON_PRIMARY, + mtk::widget_state(false, area_mouse_in(st, capture), st.has_selection), theme); +} + +static Point clamp_point_to_rect(int x, int y, const Rect& rect) { + return {gui_clamp(x, rect.x, rect.x + rect.w - 1), + gui_clamp(y, rect.y, rect.y + rect.h - 1)}; +} + +static Rect view_selection_to_screen(const ScreenBuffer& screen, + const Rect& preview, + const Rect& selection) { + Rect sel = clamp_rect_to_preview(selection, preview); + if (sel.empty()) return {0, 0, 0, 0}; + + int left = (int)((uint64_t)(sel.x - preview.x) * (uint64_t)screen.width / (uint64_t)preview.w); + int top = (int)((uint64_t)(sel.y - preview.y) * (uint64_t)screen.height / (uint64_t)preview.h); + int right = (int)((uint64_t)(sel.x + sel.w - preview.x) * (uint64_t)screen.width / (uint64_t)preview.w); + int bottom = (int)((uint64_t)(sel.y + sel.h - preview.y) * (uint64_t)screen.height / (uint64_t)preview.h); + if (right <= left) right = left + 1; + if (bottom <= top) bottom = top + 1; + + return clamp_rect_to_screen({left, top, right - left, bottom - top}, + screen.width, screen.height); +} + +enum AreaEventResult : uint8_t { + AREA_EVENT_CONTINUE = 0, + AREA_EVENT_ACCEPT, + AREA_EVENT_CANCEL, +}; + +static AreaEventResult handle_area_selector_event(WsWindow& win, + const ScreenBuffer& screen, + AreaSelectorState& st, + const Montauk::WinEvent& ev, + bool* redraw) { + if (!redraw) return AREA_EVENT_CONTINUE; + + if (ev.type == 3) { + return AREA_EVENT_CANCEL; + } + + if (ev.type == 2) { + st.win_w = win.width; + st.win_h = win.height; + st.selection_view = clamp_rect_to_preview(st.selection_view, + area_preview_rect(screen, st)); + *redraw = true; + return AREA_EVENT_CONTINUE; + } + + if (ev.type == 4) { + *redraw = true; + return AREA_EVENT_CONTINUE; + } + + if (ev.type == 0 && ev.key.pressed) { + if (ev.key.scancode == 0x01) + return AREA_EVENT_CANCEL; + if ((ev.key.ascii == '\n' || ev.key.ascii == '\r') && st.has_selection) + return AREA_EVENT_ACCEPT; + return AREA_EVENT_CONTINUE; + } + + if (ev.type != 1) + return AREA_EVENT_CONTINUE; + + int old_x = st.mouse_x; + int old_y = st.mouse_y; + st.mouse_x = ev.mouse.x; + st.mouse_y = ev.mouse.y; + if (old_x != st.mouse_x || old_y != st.mouse_y) + *redraw = true; + + Rect preview = area_preview_rect(screen, st); + bool left_pressed = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1); + bool left_held = (ev.mouse.buttons & 1) != 0; + bool left_released = !(ev.mouse.buttons & 1) && (ev.mouse.prev_buttons & 1); + + if (left_pressed) { + if (!st.fullscreen) { + Rect cancel = area_cancel_button_rect(st); + Rect capture = area_capture_button_rect(st); + if (cancel.contains(ev.mouse.x, ev.mouse.y)) + return AREA_EVENT_CANCEL; + if (capture.contains(ev.mouse.x, ev.mouse.y) && st.has_selection) + return AREA_EVENT_ACCEPT; + } + if (preview.contains(ev.mouse.x, ev.mouse.y)) { + Point p = clamp_point_to_rect(ev.mouse.x, ev.mouse.y, preview); + st.dragging = true; + st.has_selection = false; + st.drag_start_x = p.x; + st.drag_start_y = p.y; + st.selection_view = {p.x, p.y, 1, 1}; + *redraw = true; + } + } else if (st.dragging && left_held) { + Point p = clamp_point_to_rect(ev.mouse.x, ev.mouse.y, preview); + st.selection_view = clamp_rect_to_preview( + normalized_rect(st.drag_start_x, st.drag_start_y, p.x, p.y), + preview); + *redraw = true; + } else if (st.dragging && left_released) { + Point p = clamp_point_to_rect(ev.mouse.x, ev.mouse.y, preview); + st.selection_view = clamp_rect_to_preview( + normalized_rect(st.drag_start_x, st.drag_start_y, p.x, p.y), + preview); + st.dragging = false; + st.has_selection = st.selection_view.w >= MIN_AREA_SIZE + && st.selection_view.h >= MIN_AREA_SIZE; + if (st.fullscreen && st.has_selection) + return AREA_EVENT_ACCEPT; + *redraw = true; + } + + return AREA_EVENT_CONTINUE; +} + +static bool run_area_selector(const ScreenBuffer& screen, Rect* out_area) { + if (!screen.pixels || !out_area) return false; + + AreaSelectorState st = {}; + st.fullscreen = true; + st.win_w = st.fullscreen ? screen.width : area_initial_w(screen); + st.win_h = st.fullscreen ? screen.height : area_initial_h(screen); + st.mouse_x = -1; + st.mouse_y = -1; + + WsWindow win; + if (!win.create("Select Area", st.win_w, st.win_h)) { + show_error("failed to create area selection window"); + return false; + } + if (st.fullscreen) + win.set_flags(Montauk::WIN_FLAG_FULLSCREEN); + + mtk::StandaloneHost host(&win); + AreaRenderCache cache = {}; + Canvas canvas = host.canvas(); + draw_area_selector(canvas, screen, st, &cache); + host.present(); + if (st.fullscreen) + build_area_render_cache(screen, &cache); + + bool accepted = false; + while (true) { + Montauk::WinEvent ev; + int r = win.poll(&ev); + + if (r < 0) break; + if (r == 0) { + montauk::sleep_ms(16); + continue; + } + + bool redraw = false; + AreaEventResult result = handle_area_selector_event(win, screen, st, ev, &redraw); + + for (int drain = 0; result == AREA_EVENT_CONTINUE && drain < 64; drain++) { + Montauk::WinEvent queued; + int qr = win.poll(&queued); + if (qr < 0) { + result = AREA_EVENT_CANCEL; + break; + } + if (qr == 0) + break; + result = handle_area_selector_event(win, screen, st, queued, &redraw); + } + + if (result == AREA_EVENT_ACCEPT) { + accepted = true; + break; + } + if (result == AREA_EVENT_CANCEL) + break; + + if (redraw) { + canvas = host.canvas(); + draw_area_selector(canvas, screen, st, &cache); + host.present(); + } + } + + if (accepted) { + Rect preview = area_preview_rect(screen, st); + *out_area = view_selection_to_screen(screen, preview, st.selection_view); + if (out_area->empty()) + accepted = false; + } + + win.destroy(); + free_area_render_cache(&cache); + return accepted; +} + +// ============================================================================ +// JPEG encoding and saving +// ============================================================================ + static void jpeg_write_callback(void* context, void* data, int size) { JpegBuffer* buf = (JpegBuffer*)context; - if (size <= 0) return; + if (!buf || size <= 0 || buf->failed) return; uint64_t needed = buf->size + (uint64_t)size; if (needed > buf->capacity) { uint64_t new_cap = buf->capacity * 2; if (new_cap < needed) new_cap = needed; uint8_t* new_data = (uint8_t*)montauk::realloc(buf->data, new_cap); - if (!new_data) return; + if (!new_data) { + buf->failed = true; + return; + } buf->data = new_data; buf->capacity = new_cap; } @@ -45,116 +798,194 @@ static void jpeg_write_callback(void* context, void* data, int size) { buf->size += (uint64_t)size; } +static bool encode_jpeg(const ScreenBuffer& screen, + const Rect& area, + JpegBuffer* out, + char* err, + int err_len) { + if (!screen.pixels || !out) { + safe_copy(err, err_len, "nothing to encode"); + return false; + } + + Rect rect = clamp_rect_to_screen(area, screen.width, screen.height); + if (rect.empty()) { + safe_copy(err, err_len, "empty screenshot area"); + return false; + } + + uint64_t rgb_size = (uint64_t)rect.w * (uint64_t)rect.h * 3; + uint8_t* rgb = (uint8_t*)montauk::malloc(rgb_size); + if (!rgb) { + safe_copy(err, err_len, "out of memory"); + return false; + } + + for (int y = 0; y < rect.h; y++) { + uint32_t* src_row = screen.pixels + (rect.y + y) * screen.width + rect.x; + uint8_t* dst_row = rgb + (uint64_t)y * rect.w * 3; + for (int x = 0; x < rect.w; x++) { + uint32_t px = src_row[x]; + dst_row[x * 3 + 0] = (uint8_t)((px >> 16) & 0xFF); + dst_row[x * 3 + 1] = (uint8_t)((px >> 8) & 0xFF); + dst_row[x * 3 + 2] = (uint8_t)(px & 0xFF); + } + } + + out->data = (uint8_t*)montauk::malloc(256 * 1024); + out->size = 0; + out->capacity = 256 * 1024; + out->failed = false; + if (!out->data) { + montauk::mfree(rgb); + safe_copy(err, err_len, "out of memory for JPEG buffer"); + return false; + } + + int ok = stbi_write_jpg_to_func(jpeg_write_callback, out, + rect.w, rect.h, 3, rgb, 100); + montauk::mfree(rgb); + + if (!ok || out->failed || out->size == 0) { + if (out->data) montauk::mfree(out->data); + out->data = nullptr; + out->size = 0; + out->capacity = 0; + safe_copy(err, err_len, "JPEG encoding failed"); + return false; + } + + return true; +} + +static void build_default_save_dir(char* out, int out_len) { + char username[32] = {}; + if (montauk::getuser(username, sizeof(username)) > 0 && username[0]) { + char home[128]; + snprintf(home, sizeof(home), "0:/users/%s", username); + snprintf(out, out_len, "%s/Pictures", home); + montauk::fmkdir(out); + return; + } + safe_copy(out, out_len, "0:/"); +} + +static void build_suggested_name(char* out, int out_len) { + Montauk::DateTime dt; + montauk::gettime(&dt); + snprintf(out, out_len, + "screenshot_%04d%02d%02d_%02d%02d%02d.jpg", + (int)dt.Year, (int)dt.Month, (int)dt.Day, + (int)dt.Hour, (int)dt.Minute, (int)dt.Second); +} + +static bool write_jpeg_file(const char* path, const JpegBuffer& jpeg, char* err, int err_len) { + if (!path || !path[0]) { + safe_copy(err, err_len, "no destination selected"); + return false; + } + if (!jpeg.data || jpeg.size == 0) { + safe_copy(err, err_len, "no screenshot data to save"); + return false; + } + + int fd = montauk::fcreate(path); + if (fd < 0) { + safe_copy(err, err_len, "failed to create file"); + return false; + } + + int wrote = montauk::fwrite(fd, jpeg.data, 0, jpeg.size); + montauk::close(fd); + if (wrote < 0 || (uint64_t)wrote != jpeg.size) { + safe_copy(err, err_len, "failed to write file"); + return false; + } + return true; +} + +static bool save_with_dialog(const JpegBuffer& jpeg) { + char save_dir[192]; + char suggested[128]; + char path[256]; + char message[160]; + char err[160]; + + build_default_save_dir(save_dir, sizeof(save_dir)); + build_suggested_name(suggested, sizeof(suggested)); + path[0] = '\0'; + message[0] = '\0'; + err[0] = '\0'; + + bool ok = gui::dialogs::save_file("Save Screenshot", save_dir, suggested, + path, sizeof(path), message, sizeof(message)); + if (!ok) { + if (message[0]) show_error(message); + return false; + } + + if (!str_ends_with_ci(path, ".jpg") && !str_ends_with_ci(path, ".jpeg")) { + int len = montauk::slen(path); + if (len + 4 < (int)sizeof(path)) { + path[len + 0] = '.'; + path[len + 1] = 'j'; + path[len + 2] = 'p'; + path[len + 3] = 'g'; + path[len + 4] = '\0'; + } + } + + if (!write_jpeg_file(path, jpeg, err, sizeof(err))) { + show_error(err); + return false; + } + + char msg[320]; + snprintf(msg, sizeof(msg), "Screenshot saved: %s (%llu bytes)\n", + path, (unsigned long long)jpeg.size); + montauk::print(msg); + return true; +} + // ============================================================================ // Entry point // ============================================================================ extern "C" void _start() { - // Get framebuffer info before doing anything else - Montauk::FbInfo fb; - montauk::fb_info(&fb); + CaptureMode mode = CAPTURE_SCREEN; + if (!run_picker(&mode)) + montauk::exit(0); - int width = (int)fb.width; - int height = (int)fb.height; - int pitch = (int)fb.pitch; + // Let the desktop remove the picker window before reading the screen. + montauk::sleep_ms(140); - if (width <= 0 || height <= 0) { - montauk::print("screenshot: no framebuffer available\n"); + ScreenBuffer screen = {}; + char err[160]; + err[0] = '\0'; + if (!capture_screen(&screen, err, sizeof(err))) { + show_error(err); montauk::exit(1); } - // Map the hardware framebuffer (read-only access to screen pixels) - uint32_t* hw_fb = (uint32_t*)montauk::fb_map(); - if (!hw_fb) { - montauk::print("screenshot: failed to map framebuffer\n"); - montauk::exit(1); - } - - // Copy framebuffer and convert from BGRA (0xAARRGGBB) to packed RGB - uint64_t rgb_size = (uint64_t)width * height * 3; - uint8_t* rgb = (uint8_t*)montauk::malloc(rgb_size); - if (!rgb) { - montauk::print("screenshot: out of memory\n"); - montauk::exit(1); - } - - for (int y = 0; y < height; y++) { - uint32_t* src_row = (uint32_t*)((uint8_t*)hw_fb + y * pitch); - uint8_t* dst_row = rgb + y * width * 3; - for (int x = 0; x < width; x++) { - uint32_t px = src_row[x]; - dst_row[x * 3 + 0] = (px >> 16) & 0xFF; // R - dst_row[x * 3 + 1] = (px >> 8) & 0xFF; // G - dst_row[x * 3 + 2] = px & 0xFF; // B + Rect area = {0, 0, screen.width, screen.height}; + if (mode == CAPTURE_AREA) { + if (!run_area_selector(screen, &area)) { + free_screen(&screen); + montauk::exit(0); } + // Let the desktop remove the selector window before opening the save dialog. + montauk::sleep_ms(80); } - // Encode as JPEG (quality 100 - maximum quality) - JpegBuffer jpeg; - jpeg.data = (uint8_t*)montauk::malloc(256 * 1024); - jpeg.size = 0; - jpeg.capacity = 256 * 1024; - - if (!jpeg.data) { - montauk::mfree(rgb); - montauk::print("screenshot: out of memory for JPEG buffer\n"); + JpegBuffer jpeg = {}; + if (!encode_jpeg(screen, area, &jpeg, err, sizeof(err))) { + free_screen(&screen); + show_error(err); montauk::exit(1); } + free_screen(&screen); - int ok = stbi_write_jpg_to_func(jpeg_write_callback, &jpeg, - width, height, 3, rgb, 100); - montauk::mfree(rgb); - - if (!ok || jpeg.size == 0) { - montauk::mfree(jpeg.data); - montauk::print("screenshot: JPEG encoding failed\n"); - montauk::exit(1); - } - - // Build output path: 0:/users//Pictures/screenshot_YYYYMMDD_HHMMSS.jpg - char username[32] = {}; - if (montauk::getuser(username, sizeof(username)) <= 0 || username[0] == '\0') { - montauk::mfree(jpeg.data); - montauk::print("screenshot: no user session\n"); - montauk::exit(1); - } - - char home[128]; - snprintf(home, sizeof(home), "0:/users/%s", username); - - // Ensure Pictures directory exists - char pictures_dir[192]; - snprintf(pictures_dir, sizeof(pictures_dir), "%s/Pictures", home); - montauk::fmkdir(pictures_dir); - - // Build filename with timestamp - Montauk::DateTime dt; - montauk::gettime(&dt); - - char filepath[256]; - snprintf(filepath, sizeof(filepath), - "%s/screenshot_%04d%02d%02d_%02d%02d%02d.jpg", - pictures_dir, - (int)dt.Year, (int)dt.Month, (int)dt.Day, - (int)dt.Hour, (int)dt.Minute, (int)dt.Second); - - // Write JPEG file - int fd = montauk::fcreate(filepath); - if (fd < 0) { - montauk::mfree(jpeg.data); - montauk::print("screenshot: failed to create file\n"); - montauk::exit(1); - } - - montauk::fwrite(fd, jpeg.data, 0, jpeg.size); - montauk::close(fd); - montauk::mfree(jpeg.data); - - // Print confirmation - char msg[320]; - snprintf(msg, sizeof(msg), "Screenshot saved: %s (%dx%d, %llu bytes)\n", - filepath, width, height, (unsigned long long)jpeg.size); - montauk::print(msg); - + save_with_dialog(jpeg); + if (jpeg.data) montauk::mfree(jpeg.data); montauk::exit(0); } diff --git a/programs/src/screenshot/stb_truetype_impl.cpp b/programs/src/screenshot/stb_truetype_impl.cpp new file mode 100644 index 0000000..dd91785 --- /dev/null +++ b/programs/src/screenshot/stb_truetype_impl.cpp @@ -0,0 +1,33 @@ +/* + * stb_truetype_impl.cpp + * Single compilation unit for stb_truetype in MontaukOS freestanding environment + * Copyright (c) 2026 Daniel Hammer + */ + +#include +#include +#include +#include +#include + +#define STBTT_ifloor(x) ((int) stb_floor(x)) +#define STBTT_iceil(x) ((int) stb_ceil(x)) +#define STBTT_sqrt(x) stb_sqrt(x) +#define STBTT_pow(x,y) stb_pow(x,y) +#define STBTT_fmod(x,y) stb_fmod(x,y) +#define STBTT_cos(x) stb_cos(x) +#define STBTT_acos(x) stb_acos(x) +#define STBTT_fabs(x) stb_fabs(x) + +#define STBTT_malloc(x,u) ((void)(u), montauk::malloc(x)) +#define STBTT_free(x,u) ((void)(u), montauk::mfree(x)) + +#define STBTT_memcpy(d,s,n) montauk::memcpy(d,s,n) +#define STBTT_memset(d,v,n) montauk::memset(d,v,n) + +#define STBTT_strlen(x) montauk::slen(x) + +#define STBTT_assert(x) ((void)(x)) + +#define STB_TRUETYPE_IMPLEMENTATION +#include diff --git a/template/sysroot/include/Api/Syscall.hpp b/template/sysroot/include/Api/Syscall.hpp index 581d864..88f3ac3 100644 --- a/template/sysroot/include/Api/Syscall.hpp +++ b/template/sysroot/include/Api/Syscall.hpp @@ -83,6 +83,7 @@ namespace Montauk { static constexpr uint64_t SYS_WINSETSCALE = 65; static constexpr uint64_t SYS_WINGETSCALE = 66; static constexpr uint64_t SYS_WINSETCURSOR = 68; + static constexpr uint64_t SYS_WINSETFLAGS = 126; // Process management syscalls static constexpr uint64_t SYS_PROCLIST = 61; @@ -213,8 +214,11 @@ namespace Montauk { uint8_t dirty; uint8_t cursor; // 0=arrow, 1=resize_h, 2=resize_v uint8_t _pad2[2]; + uint32_t flags; }; + static constexpr uint32_t WIN_FLAG_FULLSCREEN = 1u << 0; + struct WinCreateResult { int32_t id; // -1 on failure uint32_t _pad; diff --git a/template/sysroot/include/gui/standalone.hpp b/template/sysroot/include/gui/standalone.hpp index 36e090e..2a2fcd4 100644 --- a/template/sysroot/include/gui/standalone.hpp +++ b/template/sysroot/include/gui/standalone.hpp @@ -70,6 +70,11 @@ struct WsWindow { montauk::win_setcursor(id, cursor); } + void set_flags(uint32_t flags) const { + if (id >= 0) + montauk::win_setflags(id, flags); + } + void destroy() { if (id >= 0) montauk::win_destroy(id); diff --git a/template/sysroot/include/gui/window.hpp b/template/sysroot/include/gui/window.hpp index 7c67c1d..fa16561 100644 --- a/template/sysroot/include/gui/window.hpp +++ b/template/sysroot/include/gui/window.hpp @@ -67,6 +67,7 @@ struct Window { bool external; // true = shared-memory window from external process int ext_win_id; // window server ID (valid when external == true) uint8_t ext_cursor; // cursor style requested by external app (0=arrow, 1=resize_h, 2=resize_v) + uint32_t ext_flags; Rect titlebar_rect() const { return {frame.x, frame.y, frame.w, TITLEBAR_HEIGHT}; diff --git a/template/sysroot/include/libc/montauk.h b/template/sysroot/include/libc/montauk.h index 177d4dd..967c0d1 100644 --- a/template/sysroot/include/libc/montauk.h +++ b/template/sysroot/include/libc/montauk.h @@ -87,6 +87,7 @@ extern "C" { #define MTK_SYS_WINGETSCALE 66 #define MTK_SYS_MEMSTATS 67 #define MTK_SYS_WINSETCURSOR 68 +#define MTK_SYS_WINSETFLAGS 126 #define MTK_SYS_FDELETE 77 #define MTK_SYS_FMKDIR 78 #define MTK_SYS_FRENAME 94 @@ -111,6 +112,8 @@ extern "C" { #define MTK_EVENT_CLOSE 3 #define MTK_EVENT_SCALE 4 +#define MTK_WIN_FLAG_FULLSCREEN (1u << 0) + /* Audio control commands */ #define MTK_AUDIO_SET_VOLUME 0 #define MTK_AUDIO_GET_VOLUME 1 @@ -162,6 +165,7 @@ typedef struct { uint8_t dirty; uint8_t cursor; uint8_t _pad2[2]; + uint32_t flags; } mtk_win_info; typedef struct { @@ -492,6 +496,10 @@ static inline int mtk_win_setcursor(int id, int cursor) { return (int)_mtk_syscall2(MTK_SYS_WINSETCURSOR, (long)id, (long)cursor); } +static inline int mtk_win_setflags(int id, uint32_t flags) { + return (int)_mtk_syscall2(MTK_SYS_WINSETFLAGS, (long)id, (long)flags); +} + static inline int mtk_win_setscale(int scale) { return (int)_mtk_syscall1(MTK_SYS_WINSETSCALE, (long)scale); } diff --git a/template/sysroot/include/montauk/syscall.h b/template/sysroot/include/montauk/syscall.h index eb0588a..ae0eba1 100644 --- a/template/sysroot/include/montauk/syscall.h +++ b/template/sysroot/include/montauk/syscall.h @@ -436,5 +436,8 @@ namespace montauk { inline int win_setcursor(int id, int cursor) { return (int)syscall2(Montauk::SYS_WINSETCURSOR, (uint64_t)id, (uint64_t)cursor); } + inline int win_setflags(int id, uint32_t flags) { + return (int)syscall2(Montauk::SYS_WINSETFLAGS, (uint64_t)id, (uint64_t)flags); + } }