def test_drag_overlapping(self): bottom = Frame(self.context) bottom.draggable = True bottom.bounds = Bounds(10, 10, 50, 50) bottom.set_color(StyleKeys.Background, RGBA(1, 0, 0, 1)) top = Frame(self.context) top.draggable = True top.bounds = Bounds(20, 20, 50, 50) top.set_color(StyleKeys.Background, RGBA(0, 0, 1, 1)) self.mouse.move_to(Point(30, 30)) self.mouse.press(MouseButton.LEFT) self.mouse.move_to(Point(50, 50)) self.mouse.release(MouseButton.LEFT) self.context.process() self.assertImage("drag_overlapping_top", self.context) self.mouse.move_to(Point(20, 20)) self.mouse.press(MouseButton.LEFT) self.mouse.move_to(Point(40, 40)) self.mouse.release(MouseButton.LEFT) self.context.process() self.assertImage("drag_overlapping_bottom", self.context)
def test_drag(self): window = Frame(self.context) window.draggable = True window.resizable = True window.bounds = Bounds(10, 10, 50, 50) self.mouse.move_to(Point(30, 30)) self.mouse.press(MouseButton.RIGHT) self.mouse.move_to(Point(40, 40)) self.mouse.release(MouseButton.RIGHT) self.context.process() self.assertImage("drag_with_right_button", self.context) window.bounds = Bounds(10, 10, 50, 50) self.mouse.move_to(Point(20, 20)) self.mouse.press(MouseButton.LEFT) self.mouse.move_to(Point(30, 40)) self.mouse.release(MouseButton.LEFT) self.context.process() self.assertImage("drag_with_left_button", self.context) window.bounds = Bounds(10, 10, 50, 50) self.mouse.move_to(Point(10, 10)) self.mouse.press(MouseButton.LEFT) self.mouse.press(MouseButton.MIDDLE) self.mouse.move_to(Point(40, 30)) self.mouse.release(MouseButton.MIDDLE) self.mouse.move_to(Point(20, 50)) self.context.process() self.assertImage("drag_with_2_buttons", self.context) self.mouse.release(MouseButton.LEFT) window.bounds = Bounds(10, 10, 50, 50) window.draggable = False self.mouse.move_to(Point(30, 30)) self.mouse.press(MouseButton.LEFT) self.mouse.move_to(Point(50, 30)) self.mouse.release(MouseButton.LEFT) self.context.process() self.assertImage("drag_non_draggable", self.context)
def test_location(self): locations = [] rv.observe(self.fixture.location).subscribe(locations.append) self.assertEqual(Point(0, 0), self.fixture.location) self.assertEqual([Point(0, 0)], locations) self.fixture.bounds = Bounds(10, -30, 200, 100) self.assertEqual(Point(10, -30), self.fixture.location) self.assertEqual([Point(0, 0), Point(10, -30)], locations)
def test_propagation(self): component = Component(self.context) event = MouseMoveEvent(component, Point(10, 10)) self.assertFalse(event.propagation_stopped) event.stop_propagation() self.assertTrue(event.propagation_stopped)
class FakeMouseInput(MouseInput): _position: RP[Point] = rv.from_value(Point(0, 0)) _buttons: RP[int] = rv.from_value(0) position: RV[Point] = _position.as_view() buttons: RV[int] = _buttons.as_view() def __init__(self, context: Context): super().__init__(context) self._scroll = Subject() @property def on_mouse_wheel(self) -> Observable: return self._scroll def pressed(self, button: MouseButton) -> bool: if button is None: raise ValueError("Argument 'button' is required.") return self._buttons & button == button def press(self, button: MouseButton) -> None: if button is None: raise ValueError("Argument 'button' is required.") self._buttons = self._buttons | button def release(self, button: MouseButton) -> None: if button is None: raise ValueError("Argument 'button' is required.") self._buttons = self._buttons ^ button def click(self, button: MouseButton) -> None: self.press(button) self.release(button) def move_to(self, location: Point) -> None: if location is None: raise ValueError("Argument 'location' is required.") # noinspection PyTypeChecker self._position = location def scroll(self, lines: int) -> None: self._scroll.on_next(lines) def dispose(self) -> None: self.execute_safely(self._scroll.dispose) super().dispose()
def test_hover(self): window = Window(self.context) window.bounds = Bounds(0, 0, 100, 60) button = LabelButton(self.context) button.text = "AlleyCat" button.text_size = 14 button.bounds = Bounds(10, 10, 80, 40) window.add(button) values = [] rv.observe(button, "hover").subscribe(values.append) self.assertFalse(button.hover) self.assertEqual([False], values) self.mouse.move_to(Point(50, 30)) self.context.process() self.assertTrue(button.hover) self.assertEqual([False, True], values) self.assertImage("hover_mouse_over", self.context, tolerance=Tolerance) self.mouse.move_to(Point(0, 0)) self.context.process() self.assertFalse(button.hover) self.assertEqual([False, True, False], values) self.assertImage("hover_mouse_out", self.context, tolerance=Tolerance) self.mouse.move_to(Point(10, 10)) self.context.process() self.assertTrue(button.hover) self.assertEqual([False, True, False, True], values) self.assertImage("hover_mouse_over2", self.context, tolerance=Tolerance)
def test_window_at(self): manager = self.context.window_manager bottom = Frame(self.context) bottom.bounds = Bounds(0, 0, 100, 100) middle = Frame(self.context) middle.bounds = Bounds(100, 100, 100, 100) top = Frame(self.context) top.bounds = Bounds(50, 50, 100, 100) self.assertEqual(Some(bottom), manager.window_at(Point(0, 0))) self.assertEqual(Some(bottom), manager.window_at(Point(100, 0))) self.assertEqual(Some(bottom), manager.window_at(Point(0, 100))) self.assertEqual(Some(middle), manager.window_at(Point(200, 100))) self.assertEqual(Some(middle), manager.window_at(Point(200, 200))) self.assertEqual(Some(middle), manager.window_at(Point(100, 200))) self.assertEqual(Some(top), manager.window_at(Point(100, 100))) self.assertEqual(Some(top), manager.window_at(Point(150, 150))) self.assertEqual(Some(top), manager.window_at(Point(150, 50))) self.assertEqual(Some(top), manager.window_at(Point(50, 150)))
def draw(self, g: Graphics) -> None: if self.visible: g.save() (dx, dy) = self.parent.map(lambda p: p.location).value_or(Point(0, 0)) (cx, cy, cw, ch) = self.ui.clip_bounds(self).tuple g.translate(dx, dy) g.rectangle(cx, cy, cw, ch) g.clip() try: self.draw_component(g) except BaseException as e: self.error_handler(e) g.restore()
def test_point_to_tuple(self): self.assertEqual((3, 2), Point(3, 2).tuple)
def test_bounds_contains(self): self.assertTrue(Bounds(10, 20, 100, 50).contains(Point(60, 40))) self.assertFalse(Bounds(10, 20, 100, 50).contains(Point(0, 40))) self.assertTrue(Bounds(-50, -40, 100, 80).contains(Point(50, 0))) self.assertFalse(Bounds(-50, -40, 100, 80).contains(Point(51, 0)))
def test_bounds_points(self): points = (Point(-10, 20), Point(70, 20), Point(70, 60), Point(-10, 60)) self.assertEqual(points, Bounds(-10, 20, 80, 40).points)
def test_bounds_location(self): self.assertEqual(Point(10, 20), Bounds(10, 20, 100, 200).location)
def test_point_operations(self): self.assertEqual(Point(-5, 8), Point(-10, 6) + Point(5, 2)) self.assertEqual(Point(4., -2.5), Point(6.3, 0) - Point(2.3, 2.5)) self.assertEqual(Point(-6, -10), Point(3, 5) * -2) self.assertEqual(Point(5.2, -1.5), Point(10.4, -3) / 2) self.assertEqual(Point(-3, 6), -Point(3, -6))
def test_resize_to_collapse(self): window = Frame(self.context) window.draggable = True window.resizable = True def resize(drag_from: Point, drag_to: Point, expected: Bounds) -> None: window.bounds = Bounds(20, 20, 60, 60) self.mouse.move_to(drag_from) self.mouse.press(MouseButton.LEFT) self.mouse.move_to(drag_to) self.mouse.release(MouseButton.LEFT) self.context.process() self.assertEqual(expected, window.bounds) resize(Point(40, 25), Point(40, 95), Bounds(20, 80, 60, 0)) resize(Point(75, 25), Point(5, 95), Bounds(20, 80, 0, 0)) resize(Point(75, 40), Point(5, 40), Bounds(20, 20, 0, 60)) resize(Point(75, 75), Point(5, 5), Bounds(20, 20, 0, 0)) resize(Point(40, 75), Point(40, 5), Bounds(20, 20, 60, 0)) resize(Point(25, 75), Point(95, 5), Bounds(80, 20, 0, 0)) resize(Point(25, 40), Point(95, 40), Bounds(80, 20, 0, 60)) resize(Point(25, 25), Point(95, 95), Bounds(80, 80, 0, 0))
def test_point_init(self): point = Point(10.5, -20.2) self.assertEqual(10.5, point.x) self.assertEqual(-20.2, point.y)
def test_component_at_with_layers(self): parent = Container(self.context) parent.bounds = Bounds(0, 0, 200, 200) bottom = Container(self.context) bottom.bounds = Bounds(0, 0, 100, 100) middle = Container(self.context) middle.bounds = Bounds(100, 100, 100, 100) top = Container(self.context) top.bounds = Bounds(50, 50, 100, 100) parent.add(bottom) parent.add(middle) parent.add(top) self.assertEqual(Some(parent), parent.component_at(Point(200, 0))) self.assertEqual(Some(parent), parent.component_at(Point(0, 200))) self.assertEqual(Some(bottom), parent.component_at(Point(0, 0))) self.assertEqual(Some(bottom), parent.component_at(Point(100, 0))) self.assertEqual(Some(bottom), parent.component_at(Point(0, 100))) self.assertEqual(Some(middle), parent.component_at(Point(200, 100))) self.assertEqual(Some(middle), parent.component_at(Point(200, 200))) self.assertEqual(Some(middle), parent.component_at(Point(100, 200))) self.assertEqual(Some(top), parent.component_at(Point(100, 100))) self.assertEqual(Some(top), parent.component_at(Point(150, 150))) self.assertEqual(Some(top), parent.component_at(Point(150, 50))) self.assertEqual(Some(top), parent.component_at(Point(50, 150)))
def test_active(self): window = Window(self.context) window.bounds = Bounds(0, 0, 100, 60) button = LabelButton(self.context) button.text = "AlleyCat" button.text_size = 14 button.bounds = Bounds(10, 10, 80, 40) window.add(button) values = [] rv.observe(button, "active").subscribe(values.append) self.assertFalse(button.active) self.assertEqual([False], values) self.mouse.move_to(Point(50, 30)) self.mouse.press(MouseButton.LEFT) self.context.process() self.assertTrue(button.active) self.assertEqual([False, True], values) self.assertImage("active_mouse_down", self.context, tolerance=Tolerance) self.mouse.release(MouseButton.LEFT) self.context.process() self.assertFalse(button.active) self.assertEqual([False, True, False], values) self.assertImage("active_mouse_up", self.context, tolerance=Tolerance) self.mouse.press(MouseButton.LEFT) self.mouse.move_to(Point(0, 0)) self.context.process() self.assertTrue(button.active) self.assertEqual([False, True, False, True], values) self.assertImage("active_mouse_drag_out", self.context, tolerance=Tolerance) self.mouse.release(MouseButton.LEFT) self.mouse.press(MouseButton.LEFT) self.mouse.move_to(Point(50, 30)) self.context.process() self.assertFalse(button.active) self.assertEqual([False, True, False, True, False], values) self.assertImage("active_mouse_drag_in", self.context, tolerance=Tolerance) self.mouse.release(MouseButton.LEFT) self.mouse.press(MouseButton.RIGHT) self.context.process() self.assertFalse(button.active) self.assertEqual([False, True, False, True, False], values) self.assertImage("active_right_button", self.context, tolerance=Tolerance)
class Component(Drawable, StyleResolver, MouseEventHandler, EventDispatcher, ContextAware, ReactiveObject): visible: RP[bool] = rv.new_property() parent: RP[Maybe[Container]] = rv.from_value(Nothing) offset: RV[Point] = parent.as_view().map(lambda _, parent: parent.map( lambda p: rx.combine_latest(p.observe("offset"), p.observe("location")) .pipe(ops.map(lambda v: v[0] + v[1]))).or_else_call(lambda: rx.of( Point(0, 0)))).pipe(lambda _: (ops.exclusive(), )) _minimum_size: RP[Dimension] = rv.from_value(Dimension(0, 0)) _preferred_size: RP[Dimension] = rv.from_value(Dimension(0, 0)) minimum_size_override: RP[Maybe[Dimension]] = rv.from_value(Nothing) minimum_size: RV[Dimension] = rv.combine_latest( _minimum_size, minimum_size_override)(ops.pipe(ops.map(lambda v: v[1].value_or(v[0])), ops.distinct_until_changed())) preferred_size_override: RP[Maybe[Dimension]] = rv.from_value( Nothing).pipe(lambda o: (ops.combine_latest(o.observe("minimum_size")), ops.map(lambda t: t[0].map(lambda v: t[1].copy( width=max(v.width, t[1].width), height=max(v.height, t[1].height)))), ops.distinct_until_changed())) preferred_size: RV[Dimension] = rv.combine_latest( _preferred_size, preferred_size_override, minimum_size)(ops.pipe( ops.map(lambda v: (v[1].value_or(v[0]), v[2])), ops.map(lambda v: v[0].copy(width=max(v[0].width, v[1].width), height=max(v[0].height, v[1].height))), ops.distinct_until_changed())) bounds: RP[Bounds] = Bounded.bounds.pipe(lambda o: ( ops.combine_latest(o.observe("minimum_size")), ops.map(lambda v: v[0].copy(width=max(v[0].width, v[1].width), height=max(v[0].height, v[1].height))), ops.start_with(o.preferred_size))) def __init__(self, context: Context, visible: bool = True) -> None: if context is None: raise ValueError("Argument 'context' is required.") # noinspection PyTypeChecker self.visible = visible self._context = context self._valid = False self._ui = self.create_ui() assert self._ui is not None super().__init__() self.validate() self.ui \ .on_invalidate(self) \ .pipe(ops.take_until(self.on_dispose)) \ .subscribe(lambda _: self.invalidate(), on_error=self.error_handler) @property def context(self) -> Context: return self._context @property def ui(self) -> ComponentUI: return self._ui @property def look_and_feel(self) -> LookAndFeel: return self.context.look_and_feel def create_ui(self) -> ComponentUI: return self.context.look_and_feel.create_ui(self) def show(self) -> None: # noinspection PyTypeChecker self.visible = True def hide(self) -> None: # noinspection PyTypeChecker self.visible = False @property def valid(self) -> bool: return self._valid # noinspection PyTypeChecker def validate(self, force: bool = False) -> None: if self.visible and (not self.valid or force): self._minimum_size = self.ui.minimum_size(self) self._preferred_size = self.ui.preferred_size(self) self._valid = True self.parent.map(lambda p: p.request_layout()) def invalidate(self) -> None: self._valid = False self.parent.map(lambda p: p.invalidate()) def draw(self, g: Graphics) -> None: if self.visible: g.save() (dx, dy) = self.parent.map(lambda p: p.location).value_or(Point(0, 0)) (cx, cy, cw, ch) = self.ui.clip_bounds(self).tuple g.translate(dx, dy) g.rectangle(cx, cy, cw, ch) g.clip() try: self.draw_component(g) except BaseException as e: self.error_handler(e) g.restore() def draw_component(self, g: Graphics) -> None: self.ui.draw(g, self) def position_of(self, event: PositionalEvent) -> Point: if event is None: raise ValueError("Argument 'event' is required.") return event.position - self.offset @property def inputs(self) -> Mapping[str, Input]: return self.context.inputs @property def parent_dispatcher(self) -> Maybe[EventDispatcher]: # noinspection PyTypeChecker return self.parent def __repr__(self) -> Any: return str({"id": id(self), "type": type(self).__name__})
def test_resize_with_min_size(self): window = Frame(self.context) window.draggable = True window.resizable = True window.minimum_size_override = Some(Dimension(30, 30)) def resize(drag_from: Point, drag_to: Point, expected: Bounds) -> None: window.bounds = Bounds(20, 20, 60, 60) self.mouse.move_to(drag_from) self.mouse.press(MouseButton.LEFT) self.mouse.move_to(drag_to) self.mouse.release(MouseButton.LEFT) self.context.process() self.assertEqual(expected, window.bounds) resize(Point(40, 25), Point(40, 95), Bounds(20, 50, 60, 30)) resize(Point(75, 25), Point(5, 95), Bounds(20, 50, 30, 30)) resize(Point(75, 40), Point(5, 40), Bounds(20, 20, 30, 60)) resize(Point(75, 75), Point(5, 5), Bounds(20, 20, 30, 30)) resize(Point(40, 75), Point(40, 5), Bounds(20, 20, 60, 30)) resize(Point(25, 75), Point(95, 5), Bounds(50, 20, 30, 30)) resize(Point(25, 40), Point(95, 40), Bounds(50, 20, 30, 60)) resize(Point(25, 25), Point(95, 95), Bounds(50, 50, 30, 30))
def test_point_from_tuple(self): self.assertEqual(Point(3, 2), Point.from_tuple((3, 2)))
def test_point_unpack(self): (x, y) = Point(20.2, 15.3) self.assertEqual(20.2, x) self.assertEqual(15.3, y)
def test_point_copy(self): self.assertEqual(Point(3, 2), Point(1, 5).copy(x=3, y=2)) self.assertEqual(Point(3, 5), Point(1, 5).copy(x=3)) self.assertEqual(Point(1, 2), Point(1, 5).copy(y=2)) self.assertEqual(Point(1, 5), Point(1, 5).copy())
def test_bounds_operations(self): self.assertEqual(Bounds(0, 0, 100, 200), Bounds(0, 0, 100, 200) + Point(20, 30)) self.assertEqual(Bounds(0, 0, 100, 230), Bounds(0, 0, 100, 200) + Point(80, 230)) self.assertEqual(Bounds(0, 0, 120, 230), Bounds(0, 0, 100, 200) + Point(120, 230)) self.assertEqual(Bounds(-20, 0, 120, 200), Bounds(0, 0, 100, 200) + Point(-20, 30)) self.assertEqual(Bounds(-20, -30, 120, 230), Bounds(0, 0, 100, 200) + Point(-20, -30)) self.assertEqual(Bounds(0, 0, 50, 40), Bounds(0, 0, 30, 30) + Bounds(20, 10, 30, 30)) self.assertEqual(Bounds(-10, -20, 50, 30), Bounds(0, 0, 30, 30) + Bounds(-10, -20, 50, 30)) self.assertEqual(Bounds(0, 0, 30, 30), Bounds(0, 0, 30, 30) + Bounds(10, 10, 20, 10)) self.assertEqual(Bounds(20, 40, 200, 400), Bounds(20, 40, 100, 200) * 2) self.assertEqual(Bounds(20, 40, 100, 200), Bounds(20, 40, 200, 400) / 2) self.assertEqual(Nothing, Bounds(10, 20, 100, 60) & Bounds(120, 20, 100, 60)) self.assertEqual(Nothing, Bounds(10, 20, 100, 60) & Bounds(-120, 20, 100, 60)) self.assertEqual(Nothing, Bounds(10, 20, 100, 60) & Bounds(10, 90, 100, 60)) self.assertEqual(Nothing, Bounds(10, 20, 100, 60) & Bounds(10, -50, 100, 60)) self.assertEqual(Bounds(10, 20, 30, 20), (Bounds(10, 20, 100, 60) & Bounds(-60, -20, 100, 60)).unwrap()) self.assertEqual(Bounds(60, 20, 50, 20), (Bounds(10, 20, 100, 60) & Bounds(60, -20, 100, 60)).unwrap()) self.assertEqual(Bounds(60, 40, 50, 40), (Bounds(10, 20, 100, 60) & Bounds(60, 40, 100, 60)).unwrap()) self.assertEqual(Bounds(10, 40, 30, 40), (Bounds(10, 20, 100, 60) & Bounds(-60, 40, 100, 60)).unwrap()) self.assertEqual(Bounds(40, 30, 70, 40), (Bounds(10, 20, 100, 60) & Bounds(40, 30, 100, 40)).unwrap()) self.assertEqual(Bounds(10, 30, 70, 40), (Bounds(10, 20, 100, 60) & Bounds(-20, 30, 100, 40)).unwrap()) self.assertEqual(Bounds(20, 20, 80, 20), (Bounds(10, 20, 100, 60) & Bounds(20, -20, 80, 60)).unwrap()) self.assertEqual(Bounds(20, 40, 80, 40), (Bounds(10, 20, 100, 60) & Bounds(20, 40, 80, 60)).unwrap()) self.assertEqual(Bounds(30, 40, 50, 20), (Bounds(10, 20, 100, 60) & Bounds(30, 40, 50, 20)).unwrap()) self.assertEqual(Bounds(-10, 15, 130, 70), Bounds(10, 20, 100, 60) + Insets(5, 10, 5, 20)) self.assertEqual(Bounds(15, 40, 85, 0), Bounds(10, 20, 100, 60) - Insets(20, 10, 50, 5))
def translate(self, point: Point) -> Point: if point is None: raise ValueError("Argument 'point' is required.") return point.copy(y=self.window_size.height - point.y)
def test_resize(self): window = Frame(self.context) window.draggable = True window.resizable = True def resize(name: str, drag_from: Point, drag_to: Point) -> None: window.bounds = Bounds(20, 20, 60, 60) self.mouse.move_to(drag_from) self.mouse.press(MouseButton.LEFT) self.mouse.move_to(drag_to) self.mouse.release(MouseButton.LEFT) self.context.process() self.assertImage(name, self.context) resize("resize_North", Point(40, 25), Point(40, 10)) resize("resize_Northeast", Point(75, 25), Point(90, 10)) resize("resize_East", Point(75, 40), Point(90, 40)) resize("resize_Southeast", Point(75, 75), Point(90, 90)) resize("resize_South", Point(40, 75), Point(40, 90)) resize("resize_Southwest", Point(25, 75), Point(10, 90)) resize("resize_West", Point(25, 40), Point(10, 40)) resize("resize_Northwest", Point(25, 25), Point(10, 10)) resize("resize_North_shrink", Point(40, 25), Point(40, 40)) resize("resize_Northeast_shrink", Point(75, 25), Point(60, 40)) resize("resize_East_shrink", Point(75, 40), Point(60, 40)) resize("resize_Southeast_shrink", Point(75, 75), Point(60, 60)) resize("resize_South_shrink", Point(40, 75), Point(40, 50)) resize("resize_Southwest_shrink", Point(25, 75), Point(40, 60)) resize("resize_West_shrink", Point(25, 40), Point(40, 40)) resize("resize_Northwest_shrink", Point(25, 25), Point(40, 40)) window.resizable = False resize("resize_non_resizable", Point(40, 25), Point(40, 10))
def test_bounds_move_by(self): self.assertEqual(Bounds(10, 50, 100, 200), Bounds(20, 30, 100, 200).move_by(Point(-10, 20)))
def test_component_at_with_hierarchy(self): parent = Container(self.context) parent.bounds = Bounds(0, 0, 200, 200) child = Container(self.context) child.bounds = Bounds(50, 50, 100, 100) grand_child = Container(self.context) grand_child.bounds = Bounds(25, 25, 50, 50) child.add(grand_child) parent.add(child) self.assertEqual(Nothing, parent.component_at(Point(-1, 0))) self.assertEqual(Nothing, parent.component_at(Point(201, 0))) self.assertEqual(Nothing, parent.component_at(Point(200, 201))) self.assertEqual(Nothing, parent.component_at(Point(-1, 200))) self.assertEqual(Some(parent), parent.component_at(Point(50, 49))) self.assertEqual(Some(parent), parent.component_at(Point(151, 50))) self.assertEqual(Some(parent), parent.component_at(Point(150, 151))) self.assertEqual(Some(parent), parent.component_at(Point(49, 150))) self.assertEqual(Some(child), parent.component_at(Point(50, 50))) self.assertEqual(Some(child), parent.component_at(Point(150, 50))) self.assertEqual(Some(child), parent.component_at(Point(150, 150))) self.assertEqual(Some(child), parent.component_at(Point(50, 150))) self.assertEqual(Some(grand_child), parent.component_at(Point(75, 75))) self.assertEqual(Some(grand_child), parent.component_at(Point(125, 75))) self.assertEqual(Some(grand_child), parent.component_at(Point(125, 125))) self.assertEqual(Some(grand_child), parent.component_at(Point(75, 125))) self.assertEqual(Some(child), parent.component_at(Point(74, 75))) self.assertEqual(Some(child), parent.component_at(Point(125, 74))) self.assertEqual(Some(child), parent.component_at(Point(126, 125))) self.assertEqual(Some(child), parent.component_at(Point(75, 126)))