aseprite/src/app/ui/dock.cpp

867 lines
25 KiB
C++

// Aseprite
// Copyright (C) 2021-2025 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "app/ui/dock.h"
#include "app/app.h"
#include "app/i18n/strings.h"
#include "app/ini_file.h"
#include "app/modules/gfx.h"
#include "app/pref/preferences.h"
#include "app/ui/dockable.h"
#include "app/ui/layout_selector.h"
#include "app/ui/main_window.h"
#include "app/ui/skin/skin_theme.h"
#include "os/system.h"
#include "ui/cursor_type.h"
#include "ui/label.h"
#include "ui/menu.h"
#include "ui/message.h"
#include "ui/paint_event.h"
#include "ui/resize_event.h"
#include "ui/scale.h"
#include "ui/size_hint_event.h"
#include "ui/system.h"
#include "ui/widget.h"
namespace app {
using namespace app::skin;
using namespace ui;
namespace {
enum { kTopIndex, kBottomIndex, kLeftIndex, kRightIndex, kCenterIndex };
int side_index(int side)
{
switch (side) {
case ui::TOP: return kTopIndex;
case ui::BOTTOM: return kBottomIndex;
case ui::LEFT: return kLeftIndex;
case ui::RIGHT: return kRightIndex;
}
return kCenterIndex; // ui::CENTER
}
int side_from_index(int index)
{
switch (index) {
case kTopIndex: return ui::TOP;
case kBottomIndex: return ui::BOTTOM;
case kLeftIndex: return ui::LEFT;
case kRightIndex: return ui::RIGHT;
}
return ui::CENTER; // kCenterIndex
}
} // anonymous namespace
// TODO: Duplicated from main_window.cpp
static constexpr auto kLegacyLayoutMainWindowSection = "layout:main_window";
static constexpr auto kLegacyLayoutTimelineSplitter = "timeline_splitter";
Dock::DropzonePlaceholder::DropzonePlaceholder(Widget* dragWidget, const gfx::Point& mousePosition)
: Widget(kGenericWidget)
{
setExpansive(true);
setSizeHint(dragWidget->sizeHint());
setMinSize(dragWidget->size());
m_mouseOffset = mousePosition - dragWidget->bounds().origin();
const os::SurfaceRef surface = os::System::instance()->makeRgbaSurface(dragWidget->size().w,
dragWidget->size().h);
{
const os::SurfaceLock lock(surface.get());
Paint paint;
paint.color(gfx::rgba(0, 0, 0, 0));
paint.style(os::Paint::Fill);
surface->drawRect(gfx::Rect(0, 0, surface->width(), surface->height()), paint);
}
{
Graphics g(display(), surface, 0, 0);
g.setFont(font());
Paint paint;
paint.color(gfx::rgba(0, 0, 0, 200));
// TODO: This will render any open things, especially the preview editor, need to close or hide
// that for a frame or paint the widget itself to a surface instead of croppping the backLayer.
auto backLayerSurface = display()->backLayer()->surface();
g.drawSurface(backLayerSurface.get(),
dragWidget->bounds(),
gfx::Rect(0, 0, surface->width(), surface->height()),
os::Sampling(),
&paint);
}
m_floatingUILayer = UILayer::Make();
m_floatingUILayer->setSurface(surface);
m_floatingUILayer->setPosition(dragWidget->bounds().origin());
display()->addLayer(m_floatingUILayer);
}
Dock::DropzonePlaceholder::~DropzonePlaceholder()
{
display()->removeLayer(m_floatingUILayer);
}
void Dock::DropzonePlaceholder::setGhostPosition(const gfx::Point& position) const
{
ASSERT(m_floatingUILayer);
display()->dirtyRect(m_floatingUILayer->bounds());
m_floatingUILayer->setPosition(position - m_mouseOffset);
display()->dirtyRect(m_floatingUILayer->bounds());
}
void Dock::DropzonePlaceholder::onPaint(PaintEvent& ev)
{
Graphics* g = ev.graphics();
gfx::Rect bounds = clientBounds();
g->fillRect(bgColor(), bounds);
bounds.shrink(2 * guiscale());
const auto* theme = SkinTheme::get(this);
const gfx::Color color = theme->colors.workspaceText();
g->drawRect(color, bounds);
g->drawLine(color, bounds.center(), bounds.origin());
g->drawLine(color, bounds.center(), bounds.point2());
g->drawLine(color, bounds.center(), bounds.point2() - gfx::Point(bounds.w, 0));
g->drawLine(color, bounds.center(), bounds.origin() + gfx::Point(bounds.w, 0));
g->drawRect(
color,
gfx::Rect(bounds.center() - gfx::Point(2, 2) * guiscale(), gfx::Size(4, 4) * guiscale()));
}
Dock::Dock()
{
for (int i = 0; i < kSides; ++i) {
m_sides[i] = nullptr;
m_aligns[i] = 0;
m_sizes[i] = gfx::Size(0, 0);
}
initTheme();
}
void Dock::setCustomizing(bool enable, bool doLayout)
{
m_customizing = enable;
for (int i = 0; i < kSides; ++i) {
auto* child = m_sides[i];
if (!child)
continue;
if (auto* subdock = dynamic_cast<Dock*>(child))
subdock->setCustomizing(enable, false);
}
if (doLayout)
layout();
}
void Dock::resetDocks()
{
for (int i = 0; i < kSides; ++i) {
auto* child = m_sides[i];
if (!child)
continue;
if (auto* subdock = dynamic_cast<Dock*>(child)) {
subdock->resetDocks();
if (subdock->m_autoDelete)
delete subdock;
}
m_sides[i] = nullptr;
}
removeAllChildren();
}
void Dock::dock(int side, ui::Widget* widget, const gfx::Size& prefSize)
{
ASSERT(widget);
const int i = side_index(side);
if (!m_sides[i]) {
setSide(i, widget);
addChild(widget);
if (prefSize != gfx::Size(0, 0))
m_sizes[i] = prefSize;
}
else if (auto subdock = dynamic_cast<Dock*>(m_sides[i])) {
subdock->dock(CENTER, widget, prefSize);
}
else {
ASSERT(false); // Docking failure!
}
}
void Dock::dockRelativeTo(ui::Widget* relative,
int side,
ui::Widget* widget,
const gfx::Size& prefSize)
{
ASSERT(relative);
Widget* parent = relative->parent();
ASSERT(parent);
auto* subdock = new Dock;
subdock->m_autoDelete = true;
subdock->m_customizing = m_customizing;
parent->replaceChild(relative, subdock);
subdock->dock(CENTER, relative);
subdock->dock(side, widget, prefSize);
// Fix the m_sides item if the parent is a Dock
if (auto* relativeDock = dynamic_cast<Dock*>(parent)) {
for (int i = 0; i < kSides; ++i) {
if (relativeDock->m_sides[i] == relative) {
relativeDock->setSide(i, subdock);
break;
}
}
}
}
void Dock::undock(Widget* widget)
{
Widget* parent = widget->parent();
if (!parent)
return; // Already undocked
if (auto* parentDock = dynamic_cast<Dock*>(parent)) {
parentDock->removeChild(widget);
for (int i = 0; i < kSides; ++i) {
if (parentDock->m_sides[i] == widget) {
parentDock->setSide(i, nullptr);
m_sizes[i] = gfx::Size();
break;
}
}
if (parentDock != this && parentDock->children().empty()) {
undock(parentDock);
}
}
else {
parent->removeChild(widget);
}
}
int Dock::whichSideChildIsDocked(const ui::Widget* widget) const
{
for (int i = 0; i < kSides; ++i)
if (m_sides[i] == widget)
return side_from_index(i);
return 0;
}
const gfx::Size Dock::getUserDefinedSizeAtSide(int side) const
{
int i = side_index(side);
// Only EXPANSIVE sides can be user-defined (has a splitter so the
// user can expand or shrink it)
if (m_aligns[i] & EXPANSIVE)
return m_sizes[i];
return gfx::Size();
}
Dock* Dock::subdock(int side)
{
int i = side_index(side);
if (auto* subdock = dynamic_cast<Dock*>(m_sides[i]))
return subdock;
auto* oldWidget = m_sides[i];
auto* newSubdock = new Dock;
newSubdock->m_autoDelete = true;
newSubdock->m_customizing = m_customizing;
setSide(i, newSubdock);
if (oldWidget) {
replaceChild(oldWidget, newSubdock);
newSubdock->dock(CENTER, oldWidget);
}
else
addChild(newSubdock);
return newSubdock;
}
void Dock::onSizeHint(ui::SizeHintEvent& ev)
{
gfx::Size fitIn = ev.fitInSize();
gfx::Size sz;
for (int i = 0; i < kSides; ++i) {
auto* widget = m_sides[i];
if (!widget || !widget->isVisible() || widget->isDecorative())
continue;
const int spacing = (m_aligns[i] & EXPANSIVE ? childSpacing() : 0);
const auto hint = (m_aligns[i] & EXPANSIVE ? m_sizes[i] : widget->sizeHint(fitIn));
switch (i) {
case kTopIndex:
case kBottomIndex:
sz.h += hint.h;
fitIn.h = std::max(0, fitIn.h - hint.h);
if (spacing > 0) {
sz.h += spacing;
fitIn.h = std::max(0, fitIn.h - spacing);
}
break;
case kLeftIndex:
case kRightIndex:
sz.w += hint.w;
fitIn.w = std::max(0, fitIn.w - hint.w);
if (spacing > 0) {
sz.w += spacing;
fitIn.w = std::max(0, fitIn.w - spacing);
}
break;
case kCenterIndex:
sz += gfx::Size(std::max(hint.w, std::max(m_sizes[kTopIndex].w, m_sizes[kBottomIndex].w)),
std::max(hint.h, std::max(m_sizes[kLeftIndex].h, m_sizes[kRightIndex].h)));
break;
}
}
sz += border();
ev.setSizeHint(sz);
}
void Dock::onResize(ui::ResizeEvent& ev)
{
gfx::Rect bounds = ev.bounds();
setBoundsQuietly(bounds);
bounds = childrenBounds();
updateDockVisibility();
forEachSide(bounds,
[this](ui::Widget* widget,
const gfx::Rect& widgetBounds,
const gfx::Rect& separator,
const int index) {
gfx::Rect rc = widgetBounds;
auto th = textHeight();
if (isCustomizing()) {
int handleSide = 0;
if (auto* dockable = dynamic_cast<Dockable*>(widget))
handleSide = dockable->dockHandleSide();
switch (handleSide) {
case ui::TOP:
rc.y += th;
rc.h -= th;
break;
case ui::LEFT:
rc.x += th;
rc.w -= th;
break;
}
}
widget->setBounds(rc);
});
}
void Dock::onPaint(ui::PaintEvent& ev)
{
Graphics* g = ev.graphics();
const gfx::Rect& bounds = clientBounds();
g->fillRect(bgColor(), bounds);
if (isCustomizing()) {
forEachSide(bounds,
[this, g](ui::Widget* widget,
const gfx::Rect& widgetBounds,
const gfx::Rect& separator,
const int index) {
gfx::Rect rc = widgetBounds;
auto th = textHeight();
if (isCustomizing()) {
auto* theme = SkinTheme::get(this);
const gfx::Color color = theme->colors.workspaceText();
int handleSide = 0;
if (auto* dockable = dynamic_cast<Dockable*>(widget))
handleSide = dockable->dockHandleSide();
switch (handleSide) {
case ui::TOP:
rc.h = th;
for (int y = rc.y; y + 1 < rc.y2(); y += 2)
g->drawHLine(color,
rc.x + widget->border().left(),
y,
rc.w - widget->border().width());
break;
case ui::LEFT:
rc.w = th;
for (int x = rc.x; x + 1 < rc.x2(); x += 2)
g->drawVLine(color,
x,
rc.y + widget->border().top(),
rc.h - widget->border().height());
break;
}
}
});
}
}
void Dock::onInitTheme(ui::InitThemeEvent& ev)
{
Widget::onInitTheme(ev);
setBorder(gfx::Border(0));
setChildSpacing(4 * ui::guiscale());
for (int i = 0; i < kSides; ++i) {
Widget* widget = m_sides[i];
if (widget)
widget->initTheme();
}
}
bool Dock::onProcessMessage(ui::Message* msg)
{
switch (msg->type()) {
case kMouseDownMessage: {
auto* mouseMessage = static_cast<MouseMessage*>(msg);
const gfx::Point& pos = mouseMessage->position();
if (m_hit.sideIndex >= 0 || m_hit.dockable) {
m_startPos = pos;
if (m_hit.sideIndex >= 0)
m_startSize = m_sizes[m_hit.sideIndex];
captureMouse();
if (m_hit.dockable && !mouseMessage->right()) {
m_dragging = true;
}
return true;
}
break;
}
case kMouseMoveMessage: {
if (hasCapture()) {
const gfx::Point& pos = static_cast<MouseMessage*>(msg)->position();
if (m_dropzonePlaceholder)
m_dropzonePlaceholder->setGhostPosition(pos);
if (m_hit.sideIndex >= 0) {
if (!display()->bounds().contains(pos) ||
(m_hit.widget && m_hit.widget->parent() &&
!m_hit.widget->parent()->bounds().contains(pos)))
break; // Do not handle anything outside bounds.
gfx::Size& sz = m_sizes[m_hit.sideIndex];
gfx::Size minSize(16 * guiscale(), 16 * guiscale());
if (m_hit.widget) {
minSize.w = std::max(m_hit.widget->minSize().w, minSize.w);
minSize.h = std::max(m_hit.widget->minSize().h, minSize.h);
}
switch (m_hit.sideIndex) {
case kTopIndex: sz.h = std::max(m_startSize.h + pos.y - m_startPos.y, minSize.h); break;
case kBottomIndex:
sz.h = std::max(m_startSize.h - pos.y + m_startPos.y, minSize.h);
break;
case kLeftIndex:
sz.w = std::max(m_startSize.w + pos.x - m_startPos.x, minSize.w);
break;
case kRightIndex:
sz.w = std::max(m_startSize.w - pos.x + m_startPos.x, minSize.w);
break;
}
layout();
Resize();
}
else if (m_hit.dockable && m_dragging) {
invalidate();
auto* parentDock = dynamic_cast<Dock*>(m_hit.widget->parent());
ASSERT(parentDock);
if (!parentDock)
break;
if (!m_dropzonePlaceholder)
m_dropzonePlaceholder.reset(new DropzonePlaceholder(m_hit.widget, pos));
auto dockedAt = parentDock->whichSideChildIsDocked(m_hit.widget);
const auto& bounds = parentDock->bounds();
if (!bounds.contains(pos))
break; // Do not handle anything outside the bounds of the dock.
const int kBufferZone =
std::max(12 * guiscale(), std::min(m_hit.widget->size().w, m_hit.widget->size().h));
int newTargetSide = -1;
if (m_hit.dockable->dockableAt() & LEFT && !(dockedAt & LEFT) &&
pos.x < bounds.x + kBufferZone) {
newTargetSide = LEFT;
}
else if (m_hit.dockable->dockableAt() & RIGHT && !(dockedAt & RIGHT) &&
pos.x > (bounds.w - kBufferZone)) {
newTargetSide = RIGHT;
}
else if (m_hit.dockable->dockableAt() & TOP && !(dockedAt & TOP) &&
pos.y < bounds.y + kBufferZone) {
newTargetSide = TOP;
}
else if (m_hit.dockable->dockableAt() & BOTTOM && !(dockedAt & BOTTOM) &&
pos.y > (bounds.h - kBufferZone)) {
newTargetSide = BOTTOM;
}
if (m_hit.targetSide == newTargetSide)
break;
m_hit.targetSide = newTargetSide;
// Always undock the placeholder
if (m_dropzonePlaceholder && m_dropzonePlaceholder->parent()) {
auto* placeholderCurrentDock = dynamic_cast<Dock*>(m_dropzonePlaceholder->parent());
placeholderCurrentDock->undock(m_dropzonePlaceholder.get());
}
if (m_dropzonePlaceholder && m_hit.targetSide != -1) {
parentDock->dock(m_hit.targetSide,
m_dropzonePlaceholder.get(),
m_hit.widget->sizeHint());
}
layout();
}
}
break;
}
case kMouseUpMessage: {
if (hasCapture()) {
releaseMouse();
if (m_dropzonePlaceholder && m_dropzonePlaceholder->parent()) {
// Always undock the dropzone placeholder to avoid dangling sizes.
auto* placeholderCurrentDock = dynamic_cast<Dock*>(m_dropzonePlaceholder->parent());
placeholderCurrentDock->undock(m_dropzonePlaceholder.get());
}
if (m_hit.dockable) {
auto* dockableWidget = dynamic_cast<Widget*>(m_hit.dockable);
auto* widgetDock = dynamic_cast<Dock*>(dockableWidget->parent());
int currentSide = widgetDock->whichSideChildIsDocked(dockableWidget);
assert(dockableWidget && widgetDock);
const auto* mouseMessage = static_cast<MouseMessage*>(msg);
if (mouseMessage->right() && !m_dragging) {
Menu menu;
MenuItem left(Strings::dock_left());
MenuItem right(Strings::dock_right());
MenuItem top(Strings::dock_top());
MenuItem bottom(Strings::dock_bottom());
if (m_hit.dockable->dockableAt() & ui::LEFT) {
menu.addChild(&left);
}
if (m_hit.dockable->dockableAt() & ui::RIGHT) {
menu.addChild(&right);
}
if (m_hit.dockable->dockableAt() & ui::TOP) {
menu.addChild(&top);
}
if (m_hit.dockable->dockableAt() & ui::BOTTOM) {
menu.addChild(&bottom);
}
switch (currentSide) {
case ui::LEFT: left.setEnabled(false); break;
case ui::RIGHT: right.setEnabled(false); break;
case ui::TOP: top.setEnabled(false); break;
case ui::BOTTOM: bottom.setEnabled(false); break;
}
left.Click.connect([&] { redockWidget(widgetDock, dockableWidget, ui::LEFT); });
right.Click.connect([&] { redockWidget(widgetDock, dockableWidget, ui::RIGHT); });
top.Click.connect([&] { redockWidget(widgetDock, dockableWidget, ui::TOP); });
bottom.Click.connect([&] { redockWidget(widgetDock, dockableWidget, ui::BOTTOM); });
menu.showPopup(mouseMessage->position(), display());
return false;
}
else if (m_hit.targetSide > 0 && m_dragging) {
ASSERT(m_hit.dockable->dockableAt() & m_hit.targetSide);
redockWidget(widgetDock, dockableWidget, m_hit.targetSide);
m_dropzonePlaceholder = nullptr;
m_dragging = false;
m_hit = Hit();
return false;
}
}
m_dropzonePlaceholder = nullptr;
m_dragging = false;
// Call UserResizedDock signal after resizing a Dock splitter
if (m_hit.sideIndex >= 0)
onUserResizedDock();
m_hit = Hit();
}
break;
}
case kSetCursorMessage: {
const gfx::Point& pos = static_cast<MouseMessage*>(msg)->position();
ui::CursorType cursor = ui::kArrowCursor;
if (!hasCapture())
m_hit = calcHit(pos);
if (m_hit.sideIndex >= 0) {
switch (m_hit.sideIndex) {
case kTopIndex:
case kBottomIndex: cursor = ui::kSizeNSCursor; break;
case kLeftIndex:
case kRightIndex: cursor = ui::kSizeWECursor; break;
}
}
else if (m_hit.dockable && m_hit.targetSide == -1) {
cursor = ui::kMoveCursor;
}
ui::set_mouse_cursor(cursor);
return true;
}
}
return Widget::onProcessMessage(msg);
}
void Dock::onUserResizedDock()
{
// Generate the UserResizedDock signal, this can be used to know
// when the user modified the dock configuration to save the new
// layout in a user/preference file.
UserResizedDock();
// Send the same notification for the parent (as probably eh
// MainWindow is listening the signal of just the root dock).
if (auto* parentDock = dynamic_cast<Dock*>(parent())) {
parentDock->onUserResizedDock();
}
}
void Dock::setSide(const int i, Widget* newWidget)
{
m_sides[i] = newWidget;
m_aligns[i] = calcAlign(i);
if (newWidget) {
m_sizes[i] = newWidget->sizeHint();
}
}
int Dock::calcAlign(const int i)
{
Widget* widget = m_sides[i];
int align = 0;
if (!widget) {
// Do nothing
}
else if (auto* subdock = dynamic_cast<Dock*>(widget)) {
align = subdock->calcAlign(i);
}
else if (auto* dockable2 = dynamic_cast<Dockable*>(widget)) {
align = dockable2->dockableAt();
}
return align;
}
void Dock::updateDockVisibility()
{
bool visible = false;
setVisible(true);
for (int i = 0; i < kSides; ++i) {
Widget* widget = m_sides[i];
if (!widget)
continue;
if (auto* subdock = dynamic_cast<Dock*>(widget)) {
subdock->updateDockVisibility();
}
if (widget->isVisible()) {
visible = true;
}
}
setVisible(visible);
}
void Dock::forEachSide(gfx::Rect bounds,
std::function<void(ui::Widget* widget,
const gfx::Rect& widgetBounds,
const gfx::Rect& separator,
const int index)> f)
{
for (int i = 0; i < kSides; ++i) {
auto* widget = m_sides[i];
if (!widget || !widget->isVisible() || widget->isDecorative()) {
continue;
}
const int spacing = (m_aligns[i] & EXPANSIVE ? childSpacing() : 0);
const gfx::Size sz = (m_aligns[i] & EXPANSIVE ? m_sizes[i] : widget->sizeHint(bounds.size()));
gfx::Rect rc, separator;
switch (i) {
case kTopIndex:
rc = gfx::Rect(bounds.x, bounds.y, bounds.w, sz.h);
bounds.y += rc.h;
bounds.h -= rc.h;
if (spacing > 0) {
separator = gfx::Rect(bounds.x, bounds.y, bounds.w, spacing);
bounds.y += spacing;
bounds.h -= spacing;
}
break;
case kBottomIndex:
rc = gfx::Rect(bounds.x, bounds.y2() - sz.h, bounds.w, sz.h);
bounds.h -= rc.h;
if (spacing > 0) {
separator = gfx::Rect(bounds.x, bounds.y2() - spacing, bounds.w, spacing);
bounds.h -= spacing;
}
break;
case kLeftIndex:
rc = gfx::Rect(bounds.x, bounds.y, sz.w, bounds.h);
bounds.x += rc.w;
bounds.w -= rc.w;
if (spacing > 0) {
separator = gfx::Rect(bounds.x, bounds.y, spacing, bounds.h);
bounds.x += spacing;
bounds.w -= spacing;
}
break;
case kRightIndex:
rc = gfx::Rect(bounds.x2() - sz.w, bounds.y, sz.w, bounds.h);
bounds.w -= rc.w;
if (spacing > 0) {
separator = gfx::Rect(bounds.x2() - spacing, bounds.y, spacing, bounds.h);
bounds.w -= spacing;
}
break;
case kCenterIndex: rc = bounds; break;
}
f(widget, rc, separator, i);
}
}
void Dock::redockWidget(app::Dock* widgetDock, ui::Widget* dockableWidget, const int side)
{
gfx::Size size;
if (dockableWidget->id() == "timeline") {
const gfx::Rect workspaceBounds = widgetDock->bounds();
size.w = 64;
size.h = 64;
const auto timelineSplitterPos =
get_config_double(kLegacyLayoutMainWindowSection, kLegacyLayoutTimelineSplitter, 75.0) /
100.0;
auto pos = gen::TimelinePosition::LEFT;
size.w = (workspaceBounds.w * (1.0 - timelineSplitterPos)) / guiscale();
if (side & RIGHT) {
pos = gen::TimelinePosition::RIGHT;
}
if (side & BOTTOM || side & TOP) {
pos = gen::TimelinePosition::BOTTOM;
size.h = (workspaceBounds.h * (1.0 - timelineSplitterPos)) / guiscale();
}
Preferences::instance().general.timelinePosition(pos);
}
widgetDock->undock(dockableWidget);
widgetDock->dock(side, dockableWidget, size);
App::instance()->mainWindow()->invalidate();
layout();
onUserResizedDock();
}
Dock::Hit Dock::calcHit(const gfx::Point& pos)
{
Hit hit;
forEachSide(childrenBounds(),
[this, pos, &hit](ui::Widget* widget,
const gfx::Rect& widgetBounds,
const gfx::Rect& separator,
const int index) {
if (separator.contains(pos)) {
hit.widget = widget;
hit.sideIndex = index;
}
else if (isCustomizing()) {
auto th = textHeight();
gfx::Rect rc = widgetBounds;
if (auto* dockable = dynamic_cast<Dockable*>(widget)) {
switch (dockable->dockHandleSide()) {
case ui::TOP:
rc.h = th;
if (rc.contains(pos)) {
hit.widget = widget;
hit.dockable = dockable;
}
break;
case ui::LEFT:
rc.w = th;
if (rc.contains(pos)) {
hit.widget = widget;
hit.dockable = dockable;
}
break;
}
}
}
});
return hit;
}
} // namespace app