libvwad: updated -- vwadwrite: free file buffers on close (otherwise archive creation...
[k8vavoom.git] / source / widgets / ui_root.cpp
blob5c4c5590ed1f92dabbc6f647391509d1d6f9a87d
1 //**************************************************************************
2 //**
3 //** ## ## ## ## ## #### #### ### ###
4 //** ## ## ## ## ## ## ## ## ## ## #### ####
5 //** ## ## ## ## ## ## ## ## ## ## ## ## ## ##
6 //** ## ## ######## ## ## ## ## ## ## ## ### ##
7 //** ### ## ## ### ## ## ## ## ## ##
8 //** # ## ## # #### #### ## ##
9 //**
10 //** Copyright (C) 1999-2006 Jānis Legzdiņš
11 //** Copyright (C) 2018-2023 Ketmar Dark
12 //**
13 //** This program is free software: you can redistribute it and/or modify
14 //** it under the terms of the GNU General Public License as published by
15 //** the Free Software Foundation, version 3 of the License ONLY.
16 //**
17 //** This program is distributed in the hope that it will be useful,
18 //** but WITHOUT ANY WARRANTY; without even the implied warranty of
19 //** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 //** GNU General Public License for more details.
21 //**
22 //** You should have received a copy of the GNU General Public License
23 //** along with this program. If not, see <http://www.gnu.org/licenses/>.
24 //**
25 //**************************************************************************
26 #include "../gamedefs.h"
27 #include "../screen.h"
28 #include "../client/cl_local.h"
29 #include "../input.h"
30 #include "ui.h"
33 static VCvarI ui_click_threshold("ui_click_threshold", "6", "Amount of pixels mouse cursor can move before aborting a click event.", CVAR_Archive|CVAR_NoShadow);
34 static VCvarF ui_click_timeout("ui_click_timeout", "0.3", "Click timeout in seconds.", CVAR_Archive|CVAR_NoShadow);
36 extern VCvarB ui_mouse_forced;
39 IMPLEMENT_CLASS(V, RootWidget);
41 VRootWidget *GRoot;
44 //==========================================================================
46 // VRootWidget::Init
48 //==========================================================================
49 void VRootWidget::Init () {
50 Super::Init(nullptr);
51 SetSize(640, 480);
53 //MouseCursorPic = GTextureManager.AddPatch("mc_arrow", TEXTYPE_Pic, true); // silent
54 MouseCursorPic = GTextureManager.AddFileTextureChecked("graphics/ui/default_cursor.png", TEXTYPE_Pic);
58 //==========================================================================
60 // VRootWidget::UpdateMouseForced
62 //==========================================================================
63 void VRootWidget::UpdateMouseForced () {
64 //FIXME: make this faster!
65 if (IsWantMouseInput()) RootFlags |= RWF_MouseForced; else RootFlags &= ~RWF_MouseForced;
66 ui_mouse_forced = !!(RootFlags&RWF_MouseForced);
70 //==========================================================================
72 // VRootWidget::DrawWidgets
74 //==========================================================================
75 void VRootWidget::DrawWidgets () {
76 CleanupWidgets();
77 if (IsGoingToDie()) return;
78 DrawTree();
79 // draw message box
80 if (GClGame) GClGame->eventMessageBoxDrawer();
81 // draw mouse cursor
82 UpdateMouseForced();
83 if (MouseCursorPic > 0) {
84 if (RootFlags&(RWF_MouseEnabled|RWF_MouseForced)) DrawPic(MouseX, MouseY, MouseCursorPic);
89 //==========================================================================
91 // VRootWidget::RefreshScale
93 //==========================================================================
94 void VRootWidget::RefreshScale () {
95 //fprintf(stderr, "new scale: %g, %g\n", (double)fScaleX, (double)fScaleY);
96 SizeScaleX = fScaleX;
97 SizeScaleY = fScaleY;
98 SizeWidth = VirtualWidth;
99 SizeHeight = VirtualHeight;
100 ClipRect.ClipX2 = VirtualWidth-1;
101 ClipRect.ClipY2 = VirtualHeight-1;
102 ClipTree();
106 //==========================================================================
108 // VRootWidget::TickWidgets
110 //==========================================================================
111 void VRootWidget::TickWidgets (float DeltaTime) {
112 CleanupWidgets();
113 if (IsGoingToDie()) return;
114 if (SizeScaleX != fScaleX || SizeScaleY != fScaleY) RefreshScale();
115 TickTree(DeltaTime);
119 //==========================================================================
121 // VRootWidget::UpdateMousePosition
123 //==========================================================================
124 void VRootWidget::UpdateMousePosition (int NewX, int NewY) noexcept {
125 // update and clip mouse coordinates against window boundaries
126 MouseX = clampval(NewX, 0, SizeWidth-1);
127 MouseY = clampval(NewY, 0, SizeHeight-1);
131 //==========================================================================
133 // VRootWidget::GetWidgetAtScreenXY
135 //==========================================================================
136 VWidget *VRootWidget::GetWidgetAtScreenXY (int x, int y, bool allowDisabled) noexcept {
137 return GetWidgetAt(x*SizeScaleX, y*SizeScaleY, allowDisabled);
141 //==========================================================================
143 // VRootWidget::BuildEventPath
145 //==========================================================================
146 void VRootWidget::BuildEventPath (VWidget *lastOne) noexcept {
147 if (lastOne && (lastOne->IsGoingToDie() || !lastOne->IsEnabled())) lastOne = nullptr;
148 EventPath.resetNoDtor();
149 for (VWidget *w = CurrentFocusChild; w; w = w->CurrentFocusChild) {
150 if (w->IsGoingToDie() || !w->IsEnabled(false)) break;
151 EventPath.append(w);
153 if (lastOne && (EventPath.length() == 0 || EventPath[EventPath.length()-1] != lastOne)) {
154 EventPath.append(lastOne);
159 //==========================================================================
161 // VRootWidget::FixEventCoords
163 //==========================================================================
164 void VRootWidget::FixEventCoords (VWidget *w, event_t *evt, SavedEventParts &svparts) noexcept {
165 svparts.type = 0;
166 if (!evt || !evt->isAnyMouseEvent()) return;
167 if (!w) w = this;
168 if (evt->type != ev_mouse) {
169 svparts.type = 1;
170 svparts.x = evt->x;
171 svparts.y = evt->y;
172 evt->x = (int)w->ScaledXToLocal(MouseX*SizeScaleX);
173 evt->y = (int)w->ScaledYToLocal(MouseY*SizeScaleY);
174 } else {
175 svparts.type = 2;
176 svparts.dx = evt->dx;
177 svparts.dy = evt->dy;
178 svparts.msx = evt->msx;
179 svparts.msy = evt->msy;
180 evt->dx = (int)(evt->dx*SizeScaleX/w->ClipRect.ScaleX);
181 evt->dy = (int)(evt->dy*SizeScaleY/w->ClipRect.ScaleY);
182 evt->msx = (int)w->ScaledXToLocal(MouseX*SizeScaleX);
183 evt->msy = (int)w->ScaledYToLocal(MouseY*SizeScaleY);
188 //==========================================================================
190 // VRootWidget::RestoreEventCoords
192 //==========================================================================
193 void VRootWidget::RestoreEventCoords (event_t *evt, const SavedEventParts &svparts) noexcept {
194 if (!evt) return;
195 switch (svparts.type) {
196 case 1: // ev_mouse
197 evt->x = svparts.x;
198 evt->y = svparts.y;
199 break;
200 case 2: // any mouse event except `ev_mouse`
201 evt->dx = svparts.dx;
202 evt->dy = svparts.dy;
203 evt->msx = svparts.msx;
204 evt->msy = svparts.msy;
205 break;
210 //==========================================================================
212 // VRootWidget::DispatchEvent
214 //==========================================================================
215 bool VRootWidget::DispatchEvent (event_t *evt) {
216 if (!evt || evt->isEatenOrCancelled() || EventPath.length() == 0) return false;
218 SavedEventParts svparts;
220 // sink down to the destination parent, then bubble up from the destination
221 // i.e. destination widget will get only "bubbling" event
223 int dir = 1; // 1: sinking; -1: bubbling
224 int widx = 0;
225 while (widx >= 0 && widx < EventPath.length()) {
226 if (dir == 1 && widx == EventPath.length()-1) dir = -1; // stop sinking, start bubbling
227 VWidget *w = EventPath[widx];
228 if (w->IsGoingToDie()) {
229 // if sinking, don't sink further
230 // if bubbling, do nothing
231 dir = -1;
232 } else {
233 // protect from accidental changes
234 if (dir == 1) evt->setSinking(); else evt->setBubbling();
235 // restore destination
236 evt->dest = EventPath[EventPath.length()-1];
237 // fix mouse coords
238 FixEventCoords(w, evt, svparts);
239 // call event handler
240 if (dir == 1) {
241 // sinking
242 if (w->OnEventSink(evt)) evt->setEaten();
243 } else {
244 // bubbling
245 if (w->OnEvent(evt)) evt->setEaten();
247 // restore modified event fields
248 RestoreEventCoords(evt, svparts);
249 // get out if event is consumed or cancelled
250 if (evt->isEatenOrCancelled()) return true;
252 widx += dir;
255 return false;
259 //==========================================================================
261 // VRootWidget::MouseMoveEvent
263 //==========================================================================
264 void VRootWidget::MouseMoveEvent (const event_t *evt, int OldMouseX, int OldMouseY) {
265 if (IsGoingToDie()) return;
267 // check if this is a mouse movement event
268 if (evt->type != ev_mouse) return;
270 // check if mouse really moved
271 if (OldMouseX == MouseX && OldMouseY == MouseY) return;
273 // find widgets under old and new positions
274 VWidget *OldFocus = GetWidgetAtScreenXY(OldMouseX, OldMouseY);
275 VWidget *NewFocus = GetWidgetAtScreenXY(MouseX, MouseY);
277 //GCon->Logf(NAME_Debug, "mmoved");
279 if (OldFocus == NewFocus) {
280 //GCon->Logf(NAME_Debug, "mmoved; old=(%d,%d); new=(%d,%d)", OldMouseX, OldMouseY, MouseX, MouseY);
281 //if (OldFocus) GCon->Logf(NAME_Debug, " ow:%u: size=(%d,%d)", OldFocus->GetUniqueId(), OldFocus->SizeWidth, OldFocus->SizeHeight);
282 return;
285 if (OldFocus && !OldFocus->IsGoingToDie()) {
286 //OldFocus->OnMouseLeave();
287 // generate leave event
288 event_t cev;
289 cev.clear();
290 cev.type = ev_leave;
291 cev.dest = OldFocus;
292 // the event is dispatched through the whole chain down to the current focused widget, and then to the leaved one
293 BuildEventPath(OldFocus);
294 // dispatch it
295 DispatchEvent(&cev);
298 if (NewFocus && !NewFocus->IsGoingToDie()) {
299 //NewFocus->OnMouseEnter();
300 // generate enter event
301 event_t cev;
302 cev.clear();
303 cev.type = ev_enter;
304 cev.dest = NewFocus;
305 // the event is dispatched through the whole chain down to the current focused widget, and then to the entered one
306 BuildEventPath(NewFocus);
307 // dispatch it
308 DispatchEvent(&cev);
313 //==========================================================================
315 // VRootWidget::MouseClickEvent
317 //==========================================================================
318 void VRootWidget::MouseClickEvent (const event_t *evt) {
319 if (IsGoingToDie()) return;
321 // check if this is a mouse button event
322 if (evt->type != ev_keydown && evt->type != ev_keyup) return;
323 if (evt->keycode < K_MOUSE1 || evt->keycode > K_MOUSE9) return;
325 // find widget under mouse
326 VWidget *Focus = GetWidgetAtScreenXY(MouseX, MouseY);
328 if (!Focus) return; // oopsie
330 const float ct = Sys_Time();
331 const unsigned mnum = (unsigned)(evt->keycode-K_MOUSE1);
332 if (evt->type == ev_keydown) {
333 Focus->MouseDownState[mnum].time = ct;
334 Focus->MouseDownState[mnum].x = MouseX;
335 Focus->MouseDownState[mnum].y = MouseY;
336 Focus->MouseDownState[mnum].localx = Focus->ScaledXToLocal(MouseX*SizeScaleX);
337 Focus->MouseDownState[mnum].localy = Focus->ScaledYToLocal(MouseY*SizeScaleY);
338 } else {
339 const float otime = Focus->MouseDownState[mnum].time;
340 Focus->MouseDownState[mnum].time = 0;
341 if (otime > 0 && (ui_click_timeout.asFloat() <= 0 || ct-otime < ui_click_timeout.asFloat())) {
342 const int th = ui_click_threshold.asInt();
343 if (th < 0 || (abs(Focus->MouseDownState[mnum].x-MouseX) <= th && abs(Focus->MouseDownState[mnum].y-MouseY) <= th)) {
344 //Focus->OnMouseClick((int)msx, (int)msy, evt->keycode, Focus);
345 // generate click event
346 event_t cev;
347 cev.clear();
348 cev.type = ev_click;
349 cev.keycode = evt->keycode;
350 cev.clickcnt = 1;
351 cev.dest = Focus;
352 // the event is dispatched through the whole chain down to the current focused widget, and then to the clicked one
353 BuildEventPath(Focus);
354 // dispatch it
355 DispatchEvent(&cev);
362 //==========================================================================
364 // VRootWidget::InternalResponder
366 // this is called by the engine to dispatch the event
368 //==========================================================================
369 bool VRootWidget::InternalResponder (event_t *evt) {
370 if (IsGoingToDie()) return false;
371 if (!IsEnabled(false)) return false;
372 if (evt->isEatenOrCancelled()) return evt->isEaten();
374 // process broadcast event
375 if (evt->type == ev_broadcast) {
376 BroadcastEvent(evt);
377 return false;
380 UpdateMouseForced();
382 // remember old mouse coordinates (we may need it for enter/leave events)
383 const int oldMouseX = MouseX;
384 const int oldMouseY = MouseY;
386 // update mouse position
387 if (evt->type == ev_mouse) UpdateMousePosition(MouseX+evt->dx, MouseY-evt->dy);
389 const bool mouseAllowed = (RootFlags&(RWF_MouseEnabled|RWF_MouseForced));
391 // mouse down/up should still be processed to generate click events
392 // also, "click" event should be delivered *before* "mouse up"
393 // do it here and now, why not?
394 if (mouseAllowed) {
395 // those method will take care of everything
396 MouseMoveEvent(evt, oldMouseX, oldMouseY);
397 MouseClickEvent(evt);
400 // do not send mouse events if UI mouse is disabled
401 if (mouseAllowed || !evt->isAnyMouseEvent()) {
402 VWidget *Focus = nullptr;
403 // special processing for mouse events
404 if (evt->type == ev_mouse ||
405 evt->type == ev_uimouse ||
406 ((evt->type == ev_keydown || evt->type == ev_keyup) && (evt->keycode >= K_MOUSE_FIRST && evt->keycode <= K_MOUSE_LAST)))
408 // find widget under mouse
409 Focus = GetWidgetAtScreenXY(MouseX, MouseY);
411 BuildEventPath(Focus);
412 DispatchEvent(evt);
415 return false;
419 //==========================================================================
421 // VRootWidget::Responder
423 // this is called by the engine to dispatch the event
425 //==========================================================================
426 bool VRootWidget::Responder (event_t *evt) {
427 const bool res = (evt ? InternalResponder(evt) : false);
428 CleanupWidgets();
429 return res;
433 //==========================================================================
435 // VRootWidget::SetMouse
437 //==========================================================================
438 void VRootWidget::SetMouse (bool MouseOn) {
439 if (MouseOn) RootFlags |= RWF_MouseEnabled; else RootFlags &= ~RWF_MouseEnabled;
440 UpdateMouseForced();
441 if (GInput && (RootFlags&(RWF_MouseEnabled|RWF_MouseForced))) GInput->RegrabMouse();
445 //==========================================================================
447 // VRootWidget::StaticInit
449 //==========================================================================
450 void VRootWidget::StaticInit () {
451 GRoot = SpawnWithReplace<VRootWidget>();
452 GRoot->Init();
453 GClGame->GRoot = GRoot;
457 //==========================================================================
459 // Natives
461 //==========================================================================
462 IMPLEMENT_FUNCTION(VRootWidget, SetMouse) {
463 P_GET_BOOL(MouseOn);
464 P_GET_SELF;
465 Self->SetMouse(MouseOn);
468 // native final Widget GetWidgetAtScreenXY (int x, int y, optional bool allowDisabled/*=false*/);
469 IMPLEMENT_FUNCTION(VRootWidget, GetWidgetAtScreenXY) {
470 int x, y;
471 VOptParamBool allowDisabled(false);
472 vobjGetParamSelf(x, y, allowDisabled);
473 RET_REF(Self ? Self->GetWidgetAtScreenXY(x, y, allowDisabled) : nullptr);