Skip to content

Commit

Permalink
feat: Add snap-to-box behaviour for draggable boxes. (#87)
Browse files Browse the repository at this point in the history
Closes #26.
  • Loading branch information
MartinHowarth authored Feb 7, 2020
1 parent 4321f7d commit 9a4dfff
Show file tree
Hide file tree
Showing 7 changed files with 248 additions and 95 deletions.
62 changes: 38 additions & 24 deletions shimmer/display/components/box.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,31 +259,16 @@ def get_coordinates_of_anchor(self, anchor: PositionalAnchor) -> cocos.draw.Poin
"""
return anchor.get_coord_in_rect(self._width, self._height)

def align_anchor_with_other_anchor(
def vector_between_anchors(
self,
other: "Box",
other_anchor: PositionalAnchor = CenterCenter,
self_anchor: Optional[PositionalAnchor] = None,
spacing: Union[int, Tuple[int, int], cocos.draw.Point2] = 0,
) -> None:
other_anchor: PositionalAnchor,
self_anchor: PositionalAnchor,
) -> cocos.draw.Vector2:
"""
Set an anchor of this Box to be aligned an anchor of the other Box.
For example, to align the right edge of this box with the left edge of the other:
self.align_anchor_with_other_anchor(other, RightCenter, LeftCenter)
Get the vector between the anchor of this box, and the anchor of the other box.
:param other: The Box to align this one with.
:param other_anchor: The anchor point of the other Box to align.
Defaults to CenterCenter.
:param self_anchor: The anchor point of this Box to align.
Defaults to match `other_anchor`.
:param spacing: Pixels to leave between the anchors.
If an `int` is given, the direction of the spacing the vector defined between the
given self_anchor and the CenterCenter of this Box.
Positive integers serve to make more space between the edges of the two Boxes,
while negative integers serve to cause the Box edges to overlap.
If a Tuple[int, int] is given, then it is treated as an exact (x, y) offset.
See `align_anchor_with_other_anchor` for argument descriptions.
"""
if not self.is_running:
# We need to work in a common coordinate space. The easiest choice is the director.
Expand All @@ -298,9 +283,6 @@ def align_anchor_with_other_anchor(
f"so setting relative positioning may by unstable. {self=}, {other=}"
)

if self_anchor is None:
self_anchor = other_anchor

# Get the anchor coordinates in each Boxes coordinate space.
self_point = self.get_coordinates_of_anchor(self_anchor)
other_point = other.get_coordinates_of_anchor(other_anchor)
Expand All @@ -313,6 +295,38 @@ def align_anchor_with_other_anchor(
# Get the difference between those two points in world space, which is the amount we need
# to translate this Box by to align the two anchors.
anchor_vector = other_world_point - self_world_point
return anchor_vector

def align_anchor_with_other_anchor(
self,
other: "Box",
other_anchor: PositionalAnchor = CenterCenter,
self_anchor: Optional[PositionalAnchor] = None,
spacing: Union[int, Tuple[int, int], cocos.draw.Point2] = 0,
) -> None:
"""
Set an anchor of this Box to be aligned an anchor of the other Box.
For example, to align the right edge of this box with the left edge of the other:
self.align_anchor_with_other_anchor(other, RightCenter, LeftCenter)
:param other: The Box to align this one with.
:param other_anchor: The anchor point of the other Box to align.
Defaults to CenterCenter.
:param self_anchor: The anchor point of this Box to align.
Defaults to match `other_anchor`.
:param spacing: Pixels to leave between the anchors.
If an `int` is given, the direction of the spacing the vector defined between the
given self_anchor and the CenterCenter of this Box.
Positive integers serve to make more space between the edges of the two Boxes,
while negative integers serve to cause the Box edges to overlap.
If a Tuple[int, int] is given, then it is treated as an exact (x, y) offset.
"""
if self_anchor is None:
self_anchor = other_anchor

anchor_vector = self.vector_between_anchors(other, other_anchor, self_anchor)

# Now account for spacing.
# If the spacing is an integer, then it is applied in the direction defined from
Expand Down
51 changes: 0 additions & 51 deletions shimmer/display/components/draggable_anchor.py

This file was deleted.

106 changes: 106 additions & 0 deletions shimmer/display/components/draggable_box.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""Box that can be dragged, changing the position of it's parent as well."""

import logging
from dataclasses import dataclass, replace
from typing import Optional, Iterable

import cocos
from shimmer.display.alignment import CenterCenter
from shimmer.display.components.box import Box
from shimmer.display.components.mouse_box import (
MouseBox,
MouseBoxDefinition,
EVENT_HANDLED,
)

log = logging.getLogger(__name__)


@dataclass(frozen=True)
class DraggableBoxDefinition(MouseBoxDefinition):
"""
Definition of a draggable box.
MouseBox actions will be overwritten when initialising the DraggableBox
:param snap_boxes: List of Boxes which the draggable anchor will snap to.
Snapping means when the anchor is dragged over a snap box, their centers will be aligned.
"""

snap_boxes: Optional[Iterable[Box]] = None


class DraggableBox(MouseBox):
"""Box that can be dragged, changing the position of it's parent as well."""

def __init__(self, definition: DraggableBoxDefinition):
"""
Creates a new DraggableBox.
:param definition: DraggableBoxDefinition defining the shape of the draggable box.
"""
defn = replace(
definition,
on_press=self.start_dragging,
on_release=self.stop_dragging,
on_drag=self.handle_drag,
)
super(DraggableBox, self).__init__(defn)
self.definition: DraggableBoxDefinition = self.definition
self._currently_snapped: bool = False
self._snap_drag_record: cocos.draw.Vector2 = cocos.draw.Vector2(0, 0)

def _should_handle_mouse_press(self, buttons: int) -> bool:
"""Should only handle events if this box is attached to something."""
return self.parent is not None

def _should_handle_mouse_release(self, buttons: int) -> bool:
"""Should only handle events if this box is attached to something."""
return self.parent is not None

def _should_handle_mouse_drag(self) -> bool:
"""Should only handle drag is this box currently is being dragged."""
return self._currently_dragging

def start_dragging(
self, box: "MouseBox", x: int, y: int, buttons: int, modifiers: int,
) -> bool:
"""Set this box as being dragged, and reset previous drag knowledge."""
self._snap_drag_record = cocos.draw.Vector2(0, 0)
return super(DraggableBox, self).start_dragging(box, x, y, buttons, modifiers)

def handle_drag(
self, box: Box, x: int, y: int, dx: int, dy: int, buttons: int, modifiers: int
) -> Optional[bool]:
"""While the mouse is pressed on the area, keep updating the position."""
self._snap_drag_record += (dx, dy)
self.parent.position += self._snap_drag_record

if self.definition.snap_boxes is not None:
# Move the parent by the snap record so we can test if we still intersect a snap point
# It will get moved back it we still intersect the current snap point.
# If we don't intersect with any snap point, then we will have just moved the parent
# by the correct displacement anyway.
for snap_box in self.definition.snap_boxes:
if self.world_rect.intersects(snap_box.world_rect):
self._align_with_snap_box(snap_box)
break
else:
# If no longer intersecting any snap boxes, then stop being snapped
self._snap_drag_record = cocos.draw.Vector2(0, 0)
self._currently_snapped = False

return EVENT_HANDLED

def _align_with_snap_box(self, snap_box: Box) -> None:
"""Align the parent of this DraggableBox with the given snap box."""
log.debug(f"Aligning {self} with snap box {snap_box}.")
alignment_required = self.vector_between_anchors(
snap_box, CenterCenter, CenterCenter
)
self.parent.position += alignment_required
if not self._currently_snapped:
# Set the drag record to be the inverse motion. This allows the user to drag on/off
# the snap point repeatedly right at the edge of the snap box.
self._snap_drag_record = -alignment_required
self._currently_snapped = True
2 changes: 1 addition & 1 deletion shimmer/display/components/mouse_box.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ class MouseBox(ActiveBox):
this is because it is impossible to define a generic behaviour that works for all situations.
Therefore, you should call `start_dragging` and `stop_dragging` respectively when required.
See `DraggableAnchor` for an example.
See `DraggableBox` for an example.
"""

definition_type = MouseBoxDefinition
Expand Down
12 changes: 7 additions & 5 deletions shimmer/display/programmable/instruction.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
from typing import Optional

import cocos
from shimmer.display.components.box import BoxDefinition
from shimmer.display.components.draggable_anchor import DraggableAnchor
from shimmer.display.components.draggable_box import (
DraggableBox,
DraggableBoxDefinition,
)
from shimmer.display.data_structures import Color, ActiveGreen
from shimmer.display.primitives import create_color_rect
from shimmer.display.widgets.button import ButtonDefinition, Button
Expand Down Expand Up @@ -59,7 +61,7 @@ def __init__(
self.instruction.on_execute_start = self.show_mask
self.instruction.on_execute_complete = self.hide_mask

self.drag_anchor: Optional[DraggableAnchor] = None
self.drag_anchor: Optional[DraggableBox] = None
self.executing_mask: Optional[cocos.layer.ColorLayer] = None

super(InstructionDisplay, self).__init__(definition)
Expand Down Expand Up @@ -95,8 +97,8 @@ def update_draggable_anchor(self):
self.drag_anchor = None
return

self.drag_anchor = DraggableAnchor(
BoxDefinition(
self.drag_anchor = DraggableBox(
DraggableBoxDefinition(
width=self.definition.draggable_width, height=self.rect.height
)
)
Expand Down
6 changes: 3 additions & 3 deletions shimmer/display/widgets/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
CenterTop,
)
from ..components.box import Box, BoxDefinition, ActiveBox
from ..components.draggable_anchor import DraggableAnchor
from ..components.draggable_box import DraggableBox, DraggableBoxDefinition
from ..components.focus import make_focusable
from ..components.font import FontDefinition, Calibri
from ..components.mouse_box import (
Expand Down Expand Up @@ -225,11 +225,11 @@ def _update_drag_zone(self):
This creates a draggable area of the window covering the entire title bar to the left of
the leftmost title bar button.
"""
drag_anchor_definition = BoxDefinition(
drag_box_definition = DraggableBoxDefinition(
width=self.rect.width - self._leftmost_title_bar_button_position,
height=self.title_bar_height,
)
self._update_title_bar_box("drag", DraggableAnchor(drag_anchor_definition))
self._update_title_bar_box("drag", DraggableBox(drag_box_definition))
self._title_boxes["drag"].position = 0, self.rect.height - self.title_bar_height

def _update_title_bar_box(self, name: str, box: Box) -> None:
Expand Down
Loading

0 comments on commit 9a4dfff

Please sign in to comment.