diff --git a/src/dlangui/core/events.d b/src/dlangui/core/events.d index 108c4c48..7ffadeab 100644 --- a/src/dlangui/core/events.d +++ b/src/dlangui/core/events.d @@ -3,13 +3,14 @@ module dlangui.core.events; import std.conv; enum MouseAction : ubyte { - Cancel, + Cancel, // button down handling is cancelled ButtonDown, // button is down ButtonUp, // button is up - Move, // mouse pointer is moving - Wheel, - FocusIn, - FocusOut + Move, // mouse pointer is moving + FocusIn, // pointer moved outside of widget while button was down + FocusOut, // pointer is back inside widget while button is down after FocusIn + Wheel, // scroll wheel movement + Leave // pointer left widget which has before processed Move message, while button was not down } enum MouseFlag : ushort { @@ -77,6 +78,7 @@ class MouseEvent { protected short _x; protected short _y; protected ushort _flags; + protected short _wheelDelta; protected ButtonDetails _lbutton; protected ButtonDetails _mbutton; protected ButtonDetails _rbutton; @@ -85,15 +87,30 @@ class MouseEvent { @property ref ButtonDetails mbutton() { return _mbutton; } @property MouseButton button() { return _button; } @property MouseAction action() { return _action; } + void changeAction(MouseAction a) { _action = a; } @property ushort flags() { return _flags; } + @property short wheelDelta() { return _wheelDelta; } @property short x() { return _x; } @property short y() { return _y; } - this (MouseAction a, MouseButton b, ushort f, short x, short y) { + this (MouseEvent e) { + _eventTimestamp = e._eventTimestamp; + _action = e._action; + _button = e._button; + _flags = e._flags; + _x = e._x; + _y = e._y; + _lbutton = e._lbutton; + _rbutton = e._rbutton; + _mbutton = e._mbutton; + _wheelDelta = e._wheelDelta; + } + this (MouseAction a, MouseButton b, ushort f, short x, short y, short wheelDelta = 0) { _eventTimestamp = std.datetime.Clock.currStdTime; _action = a; _button = b; _flags = f; _x = x; _y = y; + _wheelDelta = wheelDelta; } } diff --git a/src/dlangui/platforms/common/platform.d b/src/dlangui/platforms/common/platform.d index 44910b7e..82d15d61 100644 --- a/src/dlangui/platforms/common/platform.d +++ b/src/dlangui/platforms/common/platform.d @@ -33,12 +33,179 @@ class Window { _mainWidget.layout(Rect(0, 0, _dx, _dy)); } } + + long lastDrawTs; + + private void animate(Widget root, long interval) { + if (root.visibility != Visibility.Visible) + return; + for (int i = 0; i < root.childCount; i++) + animate(root.child(i), interval); + if (root.animating) + root.animate(interval); + } + void onDraw(DrawBuf buf) { if (_mainWidget !is null) { + bool needDraw = false; + bool needLayout = false; + bool animationActive = false; + checkUpdateNeeded(needDraw, needLayout, animationActive); + if (needLayout || animationActive) + needDraw = true; + long ts = std.datetime.Clock.currStdTime; + if (animationActive && lastDrawTs != 0) { + animate(_mainWidget, ts - lastDrawTs); + // layout required flag could be changed during animate - check again + checkUpdateNeeded(needDraw, needLayout, animationActive); + } + if (needLayout) { + _mainWidget.measure(_dx, _dy); + _mainWidget.layout(Rect(0, 0, _dx, _dy)); + } _mainWidget.onDraw(buf); + lastDrawTs = ts; + if (animationActive) + scheduleAnimation(); } } - abstract bool onMouseEvent(MouseEvent event); + + /// after drawing, call to schedule redraw if animation is active + void scheduleAnimation() { + // override if necessary + } + + protected bool dispatchMouseEvent(Widget root, MouseEvent event) { + // only route mouse events to visible widgets + if (root.visibility != Visibility.Visible) + return false; + if (!root.isPointInside(event.x, event.y)) + return false; + // offer event to children first + for (int i = 0; i < root.childCount; i++) { + Widget child = root.child(i); + if (dispatchMouseEvent(child, event)) + return true; + } + // if not processed by children, offer event to root + if (root.onMouseEvent(event)) { + Log.d("MouseEvent is processed"); + if (event.action == MouseAction.ButtonDown && _mouseCaptureWidget is null) { + Log.d("Setting active widget"); + _mouseCaptureWidget = root; + } else if (event.action == MouseAction.Move && _mouseTrackingWidget is null) { + Log.d("Setting tracking widget"); + _mouseTrackingWidget = root; + } + return true; + } + return false; + } + + /// widget which tracks Move events + protected Widget _mouseTrackingWidget; + /// widget which tracks all events after processed ButtonDown + protected Widget _mouseCaptureWidget; + protected bool _mouseCaptureFocusedOut; + /// dispatch mouse event to window content widgets + bool dispatchMouseEvent(MouseEvent event) { + // ignore events if there is no root + if (_mainWidget is null) + return false; + + // check if _mouseCaptureWidget and _mouseTrackingWidget still exist in child of root widget + if (_mouseCaptureWidget !is null && !_mainWidget.isChild(_mouseCaptureWidget)) + _mouseCaptureWidget = null; + if (_mouseTrackingWidget !is null && !_mainWidget.isChild(_mouseTrackingWidget)) + _mouseTrackingWidget = null; + + bool res = false; + if (_mouseCaptureWidget !is null) { + // try to forward message directly to active widget + if (event.action == MouseAction.Move) { + if (!_mouseCaptureWidget.isPointInside(event.x, event.y)) { + // point is no more inside of captured widget + if (!_mouseCaptureFocusedOut) { + // sending FocusOut message + event.changeAction(MouseAction.FocusOut); + _mouseCaptureFocusedOut = true; + return _mouseCaptureWidget.onMouseEvent(event); + } + return true; + } else { + // point is inside widget + if (_mouseCaptureFocusedOut) { + event.changeAction(MouseAction.FocusIn); // back in after focus out + _mouseCaptureFocusedOut = false; + } + return _mouseCaptureWidget.onMouseEvent(event); + } + } else if (event.action == MouseAction.Leave) { + if (!_mouseCaptureFocusedOut) { + // sending FocusOut message + event.changeAction(MouseAction.FocusOut); + _mouseCaptureFocusedOut = true; + return _mouseCaptureWidget.onMouseEvent(event); + } + return true; + } + // other messages + res = _mouseCaptureWidget.onMouseEvent(event); + if ((event.flags & (MouseFlag.LButton | MouseFlag.MButton | MouseFlag.RButton)) == 0) { + // usable capturing - no more buttons pressed + Log.d("unsetting active widget"); + _mouseCaptureWidget = null; + } + return res; + } + if (event.action == MouseAction.Move && _mouseTrackingWidget !is null) { + if (!_mouseTrackingWidget.isPointInside(event.x, event.y)) { + // send Leave message + MouseEvent leaveEvent = new MouseEvent(event); + leaveEvent.changeAction(MouseAction.Leave); + _mouseCaptureWidget.onMouseEvent(event); + // stop tracking + _mouseTrackingWidget = null; + } + } + if (!res) { + res = dispatchMouseEvent(_mainWidget, event); + } + return res; + } + /// checks content widgets for necessary redraw and/or layout + protected void checkUpdateNeeded(Widget root, ref bool needDraw, ref bool needLayout, ref bool animationActive) { + if (!root.visibility == Visibility.Visible) + return; + needDraw = root.needDraw || needDraw; + needLayout = root.needLayout || needLayout; + animationActive = root.animating || animationActive; + for (int i = 0; i < root.childCount; i++) + checkUpdateNeeded(root.child(i), needDraw, needLayout, animationActive); + } + /// checks content widgets for necessary redraw and/or layout + bool checkUpdateNeeded(ref bool needDraw, ref bool needLayout, ref bool animationActive) { + needDraw = needLayout = animationActive = false; + if (_mainWidget is null) + return false; + checkUpdateNeeded(_mainWidget, needDraw, needLayout, animationActive); + return needDraw || needLayout || animationActive; + } + /// requests update for window (unless force is true, update will be performed only if layout, redraw or animation is required). + void update(bool force = false) { + if (_mainWidget is null) + return; + bool needDraw = false; + bool needLayout = false; + bool animationActive = false; + if (checkUpdateNeeded(needDraw, needLayout, animationActive) || force) { + Log.d("Requesting update"); + invalidate(); + } + Log.d("checkUpdateNeeded returned needDraw=", needDraw, " needLayout=", needLayout, " animationActive=", animationActive); + } + /// request window redraw + abstract void invalidate(); } class Platform { diff --git a/src/dlangui/platforms/windows/winapp.d b/src/dlangui/platforms/windows/winapp.d index 3283f26b..18d221a0 100644 --- a/src/dlangui/platforms/windows/winapp.d +++ b/src/dlangui/platforms/windows/winapp.d @@ -318,67 +318,15 @@ class Win32Window : Window { protected ButtonDetails _mbutton; protected ButtonDetails _rbutton; - override bool onMouseEvent(MouseEvent event) { - return false; - } - protected bool dispatchMouseEvent(Widget root, MouseEvent event) { - // only route mouse events to visible widgets - if (root.visibility != Visibility.Visible) - return false; - // offer event to children first - for (int i = 0; i < root.childCount; i++) { - Widget child = root.child(i); - if (dispatchMouseEvent(child, event)) - return true; - } - // if not processed by children, offer event to root - if (root.onMouseEvent(event)) { - Log.d("MouseEvent is processed"); - if (event.action == MouseAction.ButtonDown && _mouseCaptureWidget is null) { - Log.d("Setting active widget"); - _mouseCaptureWidget = root; - } - return true; - } - return false; - } - protected Widget _mouseCaptureWidget; - bool dispatchMouseEvent(MouseEvent event) { - // ignore events if there is no root - if (_mainWidget is null) - return false; - // check if _mouseCaptureWidget still exists in child of root widget - if (_mouseCaptureWidget !is null && !_mainWidget.isChild(_mouseCaptureWidget)) - _mouseCaptureWidget = null; - bool res = false; - if (_mouseCaptureWidget !is null) { - // try to forward message directly to active widget - res = _mouseCaptureWidget.onMouseEvent(event); - } - if (_mouseCaptureWidget !is null && (event.flags & (MouseFlag.LButton | MouseFlag.MButton | MouseFlag.RButton)) == 0) { - // usable capturing - no more buttons pressed - Log.d("unsetting active widget"); - _mouseCaptureWidget = null; - } - if (res) - return res; - if (!res) { - res = dispatchMouseEvent(_mainWidget, event); - } - return res; - } - - void requestUpdate() { - InvalidateRect(_hwnd, null, FALSE); - } - - bool onMouse(uint message, ushort flags, short x, short y) { + bool _mouseTracking; + bool onMouse(uint message, uint flags, short x, short y) { Log.d("Win32 Mouse Message ", message, " flags=", flags, " x=", x, " y=", y); MouseButton button = MouseButton.None; MouseAction action = MouseAction.ButtonDown; ButtonDetails * pbuttonDetails = null; + short wheelDelta = 0; switch (message) { case WM_MOUSEMOVE: action = MouseAction.Move; @@ -413,24 +361,59 @@ class Win32Window : Window { button = MouseButton.Middle; pbuttonDetails = &_mbutton; break; + case WM_MOUSELEAVE: + action = MouseAction.Leave; + if (_mouseTracking) { + _mouseTracking = false; + ReleaseCapture(); + } + case WM_MOUSEWHEEL: + { + action = MouseAction.Wheel; + wheelDelta = (cast(short)(flags >> 16)) / 120; + POINT pt; + pt.x = x; + pt.y = y; + ScreenToClient(_hwnd, &pt); + x = cast(short)pt.x; + y = cast(short)pt.y; + } + break; default: // unsupported event return false; } if (action == MouseAction.ButtonDown) { - pbuttonDetails.down(x, y, flags); + pbuttonDetails.down(x, y, cast(ushort)flags); } else if (action == MouseAction.ButtonDown) { - pbuttonDetails.up(x, y, flags); + pbuttonDetails.up(x, y, cast(ushort)flags); } - MouseEvent event = new MouseEvent(action, button, flags, x, y); + if (message != WM_MOUSELEAVE && !_mouseTracking) { + _mouseTracking = true; + SetCapture(_hwnd); + } + MouseEvent event = new MouseEvent(action, button, cast(ushort)flags, x, y, wheelDelta); event.lbutton = _lbutton; event.rbutton = _rbutton; event.mbutton = _mbutton; bool res = dispatchMouseEvent(event); - if (res) - requestUpdate(); + if (res) { + Log.d("Calling update() after mouse event"); + update(); + } return res; } + + /// request window redraw + override void invalidate() { + InvalidateRect(_hwnd, null, FALSE); + } + + /// after drawing, call to schedule redraw if animation is active + override void scheduleAnimation() { + invalidate(); + } + } class Win32Platform : Platform { @@ -687,6 +670,7 @@ LRESULT WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) window.onPaint(); } return 0; // processed + case WM_MOUSELEAVE: case WM_MOUSEMOVE: case WM_LBUTTONDOWN: case WM_MBUTTONDOWN: @@ -694,6 +678,7 @@ LRESULT WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) case WM_LBUTTONUP: case WM_MBUTTONUP: case WM_RBUTTONUP: + case WM_MOUSEWHEEL: if (window !is null) window.onMouse(message, cast(ushort)wParam, cast(short)(lParam & 0xFFFF), cast(short)((lParam >> 16) & 0xFFFF)); return 0; // processed diff --git a/src/dlangui/widgets/controls.d b/src/dlangui/widgets/controls.d index 74440a53..7819bc32 100644 --- a/src/dlangui/widgets/controls.d +++ b/src/dlangui/widgets/controls.d @@ -145,6 +145,14 @@ class Button : Widget { Log.d("Button state: ", state); return true; } + if (event.action == MouseAction.FocusOut) { + resetState(State.Pressed); + return true; + } + if (event.action == MouseAction.FocusIn) { + setState(State.Pressed); + return true; + } return false; } diff --git a/src/dlangui/widgets/widget.d b/src/dlangui/widgets/widget.d index 8f8c7c7f..c74f4ad7 100644 --- a/src/dlangui/widgets/widget.d +++ b/src/dlangui/widgets/widget.d @@ -119,6 +119,7 @@ class Widget { } + //====================================================== // Style related properties @@ -201,6 +202,11 @@ class Widget { @property bool needLayout() { return _needLayout; } /// returns true if redraw is required for widget and its children @property bool needDraw() { return _needDraw; } + /// returns true is widget is being animated - need to call animate() and redraw + @property bool animating() { return false; } + /// animates window; interval is time left from previous draw, in hnsecs (1/10000 of second) + void animate(long interval) { + } /// returns measured width (calculated during measure() call) @property measuredWidth() { return _measuredWidth; } /// returns measured height (calculated during measure() call)