1 //**************************************************************************
3 //** ## ## ## ## ## #### #### ### ###
4 //** ## ## ## ## ## ## ## ## ## ## #### ####
5 //** ## ## ## ## ## ## ## ## ## ## ## ## ## ##
6 //** ## ## ######## ## ## ## ## ## ## ## ### ##
7 //** ### ## ## ### ## ## ## ## ## ##
8 //** # ## ## # #### #### ## ##
10 //** Copyright (C) 1999-2006 Jānis Legzdiņš
11 //** Copyright (C) 2018-2023 Ketmar Dark
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.
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.
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/>.
25 //**************************************************************************
26 #include "../gamedefs.h"
27 #include "../screen.h"
28 #include "../client/cl_local.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
);
44 //==========================================================================
48 //==========================================================================
49 void VRootWidget::Init () {
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 () {
77 if (IsGoingToDie()) return;
80 if (GClGame
) GClGame
->eventMessageBoxDrawer();
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);
98 SizeWidth
= VirtualWidth
;
99 SizeHeight
= VirtualHeight
;
100 ClipRect
.ClipX2
= VirtualWidth
-1;
101 ClipRect
.ClipY2
= VirtualHeight
-1;
106 //==========================================================================
108 // VRootWidget::TickWidgets
110 //==========================================================================
111 void VRootWidget::TickWidgets (float DeltaTime
) {
113 if (IsGoingToDie()) return;
114 if (SizeScaleX
!= fScaleX
|| SizeScaleY
!= fScaleY
) RefreshScale();
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;
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
{
166 if (!evt
|| !evt
->isAnyMouseEvent()) return;
168 if (evt
->type
!= ev_mouse
) {
172 evt
->x
= (int)w
->ScaledXToLocal(MouseX
*SizeScaleX
);
173 evt
->y
= (int)w
->ScaledYToLocal(MouseY
*SizeScaleY
);
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
{
195 switch (svparts
.type
) {
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
;
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
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
233 // protect from accidental changes
234 if (dir
== 1) evt
->setSinking(); else evt
->setBubbling();
235 // restore destination
236 evt
->dest
= EventPath
[EventPath
.length()-1];
238 FixEventCoords(w
, evt
, svparts
);
239 // call event handler
242 if (w
->OnEventSink(evt
)) evt
->setEaten();
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;
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);
285 if (OldFocus
&& !OldFocus
->IsGoingToDie()) {
286 //OldFocus->OnMouseLeave();
287 // generate leave event
292 // the event is dispatched through the whole chain down to the current focused widget, and then to the leaved one
293 BuildEventPath(OldFocus
);
298 if (NewFocus
&& !NewFocus
->IsGoingToDie()) {
299 //NewFocus->OnMouseEnter();
300 // generate enter event
305 // the event is dispatched through the whole chain down to the current focused widget, and then to the entered one
306 BuildEventPath(NewFocus
);
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
);
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
349 cev
.keycode
= evt
->keycode
;
352 // the event is dispatched through the whole chain down to the current focused widget, and then to the clicked one
353 BuildEventPath(Focus
);
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
) {
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?
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
);
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);
433 //==========================================================================
435 // VRootWidget::SetMouse
437 //==========================================================================
438 void VRootWidget::SetMouse (bool MouseOn
) {
439 if (MouseOn
) RootFlags
|= RWF_MouseEnabled
; else RootFlags
&= ~RWF_MouseEnabled
;
441 if (GInput
&& (RootFlags
&(RWF_MouseEnabled
|RWF_MouseForced
))) GInput
->RegrabMouse();
445 //==========================================================================
447 // VRootWidget::StaticInit
449 //==========================================================================
450 void VRootWidget::StaticInit () {
451 GRoot
= SpawnWithReplace
<VRootWidget
>();
453 GClGame
->GRoot
= GRoot
;
457 //==========================================================================
461 //==========================================================================
462 IMPLEMENT_FUNCTION(VRootWidget
, SetMouse
) {
465 Self
->SetMouse(MouseOn
);
468 // native final Widget GetWidgetAtScreenXY (int x, int y, optional bool allowDisabled/*=false*/);
469 IMPLEMENT_FUNCTION(VRootWidget
, GetWidgetAtScreenXY
) {
471 VOptParamBool
allowDisabled(false);
472 vobjGetParamSelf(x
, y
, allowDisabled
);
473 RET_REF(Self
? Self
->GetWidgetAtScreenXY(x
, y
, allowDisabled
) : nullptr);