local super = require "Object"

Handle = super:new()

function Handle:new(hooks)
    self = super.new(self)
    
    self.actionName = nil
    if (hooks) then
        for k, v in pairs(hooks) do
            self[k] = v
        end
    end
    self:setUndoable(true)
    
    return self
end

function Handle:draw(canvas, view)
end

function Handle:cursorRects(metrics, view)
    return {}
end

function Handle:status(metrics, view, x, y)
end

function Handle:down(metrics, view, x, y)
    return self:status(metrics, view, x, y)
end

function Handle:dragged(metrics, view, x, y, down)
end

function Handle:up(metrics, view)
end

function Handle:action()
    if self.actionName then
        self:setUndoActionName(self.actionName)
    end
end

--

DraggingHandle = Handle:new()

function DraggingHandle:status(metrics, view, x, y)
    local myx, myy = self.location:getValue(view)
    return {dx = x - myx, dy = y - myy}
end

function DraggingHandle:dragged(metrics, view, x, y, down)
    local oldX, oldY = self.location:getValue(view)
    x, y = view:alignedPoint(x - down.dx, y - down.dy)
    if x ~= oldX or y ~= oldY then
        self.location:setValue(view, x, y)
        self:action()
    end
end

--

PointHandle = DraggingHandle:new()

function PointHandle:cursorRects(metrics, view)
    local adjust = metrics:growthAdjustment()
    local pixelSize = 1 / metrics:pointScale()
    local pixels = math.floor(5 * adjust / pixelSize)
    local x, y
    if pixels % 2 == 0 then
        x, y = metrics:alignToPixelEdge(self.location:getValue(view))
    else
        x, y = metrics:alignToPixelCenter(self.location:getValue(view))
    end
    local size = (pixels / 2 + 1) * pixelSize
    local rect = Rect:new{
        left   = x - size,
        bottom = y - size,
        right  = x + size,
        top    = y + size,
    }
    local cursorName = 'resize'
    return {{ rect, cursorName }}
end

function PointHandle:draw(canvas, view)
    local metrics = canvas:metrics()
    local adjust = metrics:growthAdjustment()
    local pixelSize = 1 / metrics:pointScale()
    local pixels = math.floor(5 * adjust / pixelSize)
    local x, y = self.location:getValue(view)
    if pixels % 2 == 0 then
        x, y = metrics:alignToPixelEdge(x, y)
    else
        x, y = metrics:alignToPixelCenter(x, y)
    end
    local size = pixels / 2 * pixelSize
    local path = Path.rect{
        left   = x - size - pixelSize,
        bottom = y - size - pixelSize,
        right  = x + size + pixelSize,
        top    = y + size + pixelSize,
    }
    canvas:setPaint(Color.black):fill(path)
    path = Path.rect{
        left   = x - size,
        bottom = y - size,
        right  = x + size,
        top    = y + size,
    }
    canvas:setPaint(Color.white):fill(path)
end

--

RectHandle = Handle:new()

function RectHandle:cursorRects(metrics, view)
    local cursorRects = {}
    if view:isEditing() then
        return cursorRects
    end
    local adjust = metrics:growthAdjustment()
    local pixelSize = 1 / metrics:pointScale()
    local pixels = math.floor(5 * adjust / pixelSize)
    local xs, ys = {}, {}
    xs[1], ys[1], xs[3], ys[3] = self.location:getValue(view)
    xs[2], ys[2] = (xs[1] + xs[3]) / 2, (ys[1] + ys[3]) / 2
    xs[1], ys[1] = metrics:alignToPixelEdge(xs[1], ys[1])
    if pixels % 2 == 0 then
        xs[2], ys[2] = metrics:alignToPixelEdge(xs[2], ys[2])
    else
        xs[2], ys[2] = metrics:alignToPixelCenter(xs[2], ys[2])
    end
    xs[3], ys[3] = metrics:alignToPixelEdge(xs[3], ys[3])
    local size = pixels / 2 * pixelSize
    local cursorNames = {
        {'resize-nw-se', 'resize-n-s', 'resize-ne-sw'},
        {'resize-e-w', nil, 'resize-e-w'},
        {'resize-ne-sw', 'resize-n-s', 'resize-nw-se'},
    }
    local drawOrder = {2, 1, 3}
    for _, yi in ipairs(drawOrder) do
        for _, xi in ipairs(drawOrder) do
            if xi ~= 2 or yi ~= 2 then
                local x = xs[xi] + (xi - 2) * (size + pixelSize)
                local y = ys[yi] + (yi - 2) * (size + pixelSize)
                local rect = Rect:new{
                    left   = x - size - pixelSize,
                    bottom = y - size - pixelSize,
                    right  = x + size + pixelSize,
                    top    = y + size + pixelSize,
                }
                local cursorName = cursorNames[4 - yi][xi]
                cursorRects[#cursorRects + 1] = { rect, cursorName }
            end
        end
    end
    return cursorRects
end

function RectHandle:draw(canvas, view)
    local metrics = canvas:metrics()
    local adjust = metrics:growthAdjustment()
    local pixelSize = 1 / metrics:pointScale()
    local pixels = math.floor(5 * adjust / pixelSize)
    local xs, ys = {}, {}
    xs[1], ys[1], xs[3], ys[3] = self.location:getValue(view)
    xs[2], ys[2] = (xs[1] + xs[3]) / 2, (ys[1] + ys[3]) / 2
    xs[1], ys[1] = metrics:alignToPixelEdge(xs[1], ys[1])
    if pixels % 2 == 0 then
        xs[2], ys[2] = metrics:alignToPixelEdge(xs[2], ys[2])
    else
        xs[2], ys[2] = metrics:alignToPixelCenter(xs[2], ys[2])
    end
    xs[3], ys[3] = metrics:alignToPixelEdge(xs[3], ys[3])
    if not canvas:isHitTest() then
        local path = Path.rect{
            left   = xs[1] - pixelSize * 1.5,
            bottom = ys[1] - pixelSize * 1.5,
            right  = xs[3] + pixelSize * 1.5,
            top    = ys[3] + pixelSize * 1.5,
        }
        canvas:setPaint(Color.highlight(0.5))
            :setThickness(pixelSize * 3)
            :stroke(path)
    end
    if view:isEditing() then
        return
    end
    local size = pixels / 2 * pixelSize
    local drawOrder = {2, 1, 3}
    for _, yi in ipairs(drawOrder) do
        for _, xi in ipairs(drawOrder) do
            if xi ~= 2 or yi ~= 2 then
                local x = xs[xi] + (xi - 2) * (size + pixelSize)
                local y = ys[yi] + (yi - 2) * (size + pixelSize)
                local path = Path.rect{
                    left   = x - size - pixelSize,
                    bottom = y - size - pixelSize,
                    right  = x + size + pixelSize,
                    top    = y + size + pixelSize,
                }
                canvas:setPaint(Color.black):fill(path)
                path = Path.rect{
                    left   = x - size,
                    bottom = y - size,
                    right  = x + size,
                    top    = y + size,
                }
                canvas:setPaint(Color.white):fill(path)
            end
        end
    end
end

function RectHandle:status(metrics, view, x, y)
    local left, bottom, right, top = self.location:getValue(view)
    local side = {x = 0, y = 0, dx = 0, dy = 0}
    if view:isEditing() then
        return side
    end
    if x <= left then
        side.x = -1
        side.dx = x - left
    elseif x >= right then
        side.x = 1
        side.dx = x - right
    end
    if y <= bottom then
        side.y = -1
        side.dy = y - bottom
    elseif y >= top then
        side.y = 1
        side.dy = y - top
    end
    return side
end

function RectHandle:dragged(metrics, view, x, y, down)
    local left, bottom, right, top = self.location:getValue(view)
    local oldLeft, oldBottom, oldRight, oldTop = left, bottom, right, top
    if down.x == -1 then
        left = view:alignedPoint(x - down.dx, 0)
    elseif down.x == 1 then
        right = view:alignedPoint(x - down.dx, 0)
    end
    if down.y == -1 then
        local _
        _, bottom = view:alignedPoint(0, y - down.dy)
    elseif down.y == 1 then
        local _
        _, top = view:alignedPoint(0, y - down.dy)
    end
    if left ~= oldLeft or bottom ~= oldBottom or right ~= oldRight or top ~= oldTop then
        self.location:setValue(view, left, bottom, right, top)
        self:action()
    end
end

--

BoundedDraggingHandle = DraggingHandle:new()

function BoundedDraggingHandle:cursorRects(metrics, view)
    local cursorRects = {}
    local x1, y1, x2, y2 = self.location:getValue(view)
    if x1 ~= x2 and y1 ~= y2 then
        x1, y1 = metrics:alignToPixelEdge(x1, y1)
        x2, y2 = metrics:alignToPixelEdge(x2, y2)
        local rect = Rect:new{
            left   = x1,
            bottom = y1,
            right  = x2,
            top    = y2,
        }
        local cursorName = 'hand'
        cursorRects[#cursorRects + 1] = { rect, cursorName }
    end
    return cursorRects
end

function BoundedDraggingHandle:draw(canvas, view)
    local metrics = canvas:metrics()
    local x1, y1, x2, y2 = self.location:getValue(view)
    if x1 ~= x2 and y1 ~= y2 then
        x1, y1 = metrics:alignToPixelEdge(x1, y1)
        x2, y2 = metrics:alignToPixelEdge(x2, y2)
        local rect = Rect:new{
            left   = x1,
            bottom = y1,
            right  = x2,
            top    = y2,
        }
        canvas:setPaint(Color.invisible):fill(Path.rect(rect))
    end
end

function BoundedDraggingHandle:status(metrics, view, x, y)
    local x1, y1, x2, y2 = self.location:getValue(view)
    return {dx = x - (x1 + x2) / 2, dy = y - (y1 + y2) / 2}
end

function BoundedDraggingHandle:dragged(metrics, view, x, y, down)
    local x1, y1, x2, y2 = self.location:getValue(view)
    local left, bottom, right, top = self.bounds:getValue(view)
    local dx, dy = (x2 - x1) / 2, (y2 - y1) / 2
    x, y = math._mid(left + dx, x - down.dx, right - dx), math._mid(bottom + dy, y - down.dy, top - dy)
    if x ~= (x1 + x2) / 2 or y ~= (y1 + y2) / 2 then
        self.location:setValue(view, x - dx, y - dy, x + dx, y + dy)
        self:action()
    end
end

--

ButtonHandle = Handle:new()

function ButtonHandle:down(metrics, view, x, y)
    self.isDown = true
    view:invalidate(self)
end

function ButtonHandle:dragged(metrics, view, x, y, down)
    local left, bottom, right, top = self.location:getValue(view)
    local rect = self:getRect(metrics, view)
    self.isDown = rect and rect:contains(x, y)
    view:invalidate(self)
end

function ButtonHandle:up(metrics, view)
    local result
    if self.isDown then
        result = self.inspector:getValue(view)
    end
    self.isDown = nil
    view:invalidate(self)
    return result
end

function ButtonHandle:draw(canvas, view)
    if view:isEditing() then
        return
    end
    local metrics = canvas:metrics()
    local styledString = self:getStyledString(metrics, view)
    local rect = self:getRect(metrics, view)
    if styledString and rect then
        local fillPaint, textPaint
        if self.isDown then
            fillPaint = Color.highlight(1.0)
            textPaint = Color.white
        else
            fillPaint = Color.highlight(0.75)
            textPaint = Color.white
        end
        canvas:setPaint(fillPaint)
            :fill(Path.rect(rect, math.min(rect:width(), rect:height()) / 2))
        local transformation
        if self.isVertical:getValue(view) then
            transformation = Transformation:identity():rotate(math.pi / 2)
        end
        StyledStringPointStamp(canvas, rect:midx(), rect:midy() - 1, styledString, Color.tint(0.75), 0.5, 0.5, transformation)
        StyledStringPointStamp(canvas, rect:midx(), rect:midy(), styledString, textPaint, 0.5, 0.5, transformation)
    end
end

function ButtonHandle:getStyledString(metrics, view)
    local adjust = metrics:growthAdjustment()
    local text = self.text:getValue(view)
    if text then
        local font = Font.boldSystem(10 * adjust)
        return StyledString.new(text, { font = font })
    end
end

function ButtonHandle:getRect(metrics, view)
    local adjust = metrics:growthAdjustment()
    local x, y, halign, valign, dx, dy = self.location:getValue(view)
    local styledString = self:getStyledString(metrics, view)
    if x and y and styledString then
        local textRect = styledString:measure()
        local width, height = textRect:width() + 12 * adjust, textRect:height() + 4 * adjust
        if self.isVertical:getValue(view) then
            width, height = height, width
        end
        return Rect:point(x, y)
            :insetXY(-width / 2, -height / 2)
            :offset(width * (0.5 - halign), height * (0.5 - valign))
            :offset(dx * adjust, dy * adjust)
    end
end

--

ColorHandle = Handle:new()

function ColorHandle:down(metrics, view, x, y)
    self.isDown = true
    view:invalidate(self)
end

function ColorHandle:dragged(metrics, view, x, y, down)
    local adjust = metrics:growthAdjustment()
    local myx, myy = self.location:getValue(view)
    if (math.sqrt(math.pow(x - myx, 2) + math.pow(y - myy, 2)) >= 8 * adjust) then
        local angle = (math.atan2(y - myy, x - myx) / (2 * math.pi)) % 1
        local color = Color.rgba(
            0.8 * math.min(1, math.max(0, 2 - 6 * math.min(angle - 0,   1   - angle))),
            0.8 * math.min(1, math.max(0, 2 - 6 * math.max(angle - 1/3, 1/3 - angle))),
            0.8 * math.min(1, math.max(0, 2 - 6 * math.max(angle - 2/3, 2/3 - angle))),
            1
        )
        self.color:setValue(view, color)
        self:action()
    end
end

function ColorHandle:up(metrics, view)
    self.isDown = nil
    view:invalidate(self)
end

function ColorHandle:draw(canvas, view)
    local adjust = canvas:metrics():growthAdjustment()
    local x, y = self.location:getValue(view)
    local size = 8 * adjust
  
    if (self.isDown) then
        local size = size + 1.5 * adjust
        local num = 12
        local r
        r = 6 * adjust
        canvas:setPaint(Color.black)
        for i = 2, num, 2 do
            local angle = 2*math.pi * i/num
            local path = Path.oval{left = x + size*math.cos(angle)-r, right = x + size*math.cos(angle)+r, bottom = y + size*math.sin(angle)-r, top = y + size*math.sin(angle)+r}
            canvas:fill(path)
        end
        r = 5 * adjust
        for i = 1, num, 2 do
            local angle = 2*math.pi * i/num
            local path = Path.oval{left = x + size*math.cos(angle)-r, right = x + size*math.cos(angle)+r, bottom = y + size*math.sin(angle)-r, top = y + size*math.sin(angle)+r}
            canvas:fill(path)
        end
        r = 4.5 * adjust
        for i = 2, num, 2 do
            local angle = 2*math.pi * i/num
            local color = Color.rgba(math.min(1, math.max(0, 2 - 6*math.min(i/num - 0,   1   - i/num))),
                                     math.min(1, math.max(0, 2 - 6*math.max(i/num - 1/3, 1/3 - i/num))),
                                     math.min(1, math.max(0, 2 - 6*math.max(i/num - 2/3, 2/3 - i/num))),
                                     1)
            local path = Path.oval{left = x + size*math.cos(angle)-r, right = x + size*math.cos(angle)+r, bottom = y + size*math.sin(angle)-r, top = y + size*math.sin(angle)+r}
            canvas:setPaint(color):fill(path)
        end
        r = 3.5 * adjust
        for i = 1, num, 2 do
            local angle = 2*math.pi * i/num
            local color = Color.rgba(math.min(1, math.max(0, 2 - 6*math.min(i/num - 0,   1   - i/num))),
                                     math.min(1, math.max(0, 2 - 6*math.max(i/num - 1/3, 1/3 - i/num))),
                                     math.min(1, math.max(0, 2 - 6*math.max(i/num - 2/3, 2/3 - i/num))),
                                     1)
            local path = Path.oval{left = x + size*math.cos(angle)-r, right = x + size*math.cos(angle)+r, bottom = y + size*math.sin(angle)-r, top = y + size*math.sin(angle)+r}
            canvas:setPaint(color):fill(path)
        end
    end

    local path = Path.oval{left = x - size, right = x + size, bottom = y - size, top = y + size}
    canvas:setPaint(Color.black):fill(path)
    size = size - 1.5 * adjust
    local path = Path.oval{left = x - size, right = x + size, bottom = y - size, top = y + size}
    canvas:setPaint(Color.white):fill(path)
    size = size - 1.5 * adjust
    local path = Path.oval{left = x - size, right = x + size, bottom = y - size, top = y + size}
    canvas:setPaint(self.color:getValue(view)):fill(path)
end

--

ScrollerHandle = Handle:new()

function ScrollerHandle:draw(canvas, view)
    local adjust = canvas:metrics():growthAdjustment()
    local min, max = self.range:getValue(view)
    local visibleMin, visibleMax = self.visibleRange:getValue(view)
    if visibleMax - visibleMin < max - min then
        local x1, y1, x2, y2 = self.track:getValue(view)
        local trackLength = math.sqrt(math.pow(x2 - x1, 2) + math.pow(y2 - y1, 2))
        local trackAngle = math.atan2(y2 - y1, x2 - x1)
        local path = Path.line{
            x1 = (visibleMin - min) * trackLength / (max - min),
            y1 = 0,
            x2 = (visibleMax - min) * trackLength / (max - min),
            y2 = 0,
        }
        local t = Transformation.identity():translate(x1, y1):rotate(trackAngle)
        canvas:preserve(function(canvas)
            canvas:concatTransformation(t)
                :setThickness(8 * adjust)
                :setPaint(Color.gray(1, 0.8))
                :stroke(path)
                :setThickness(6 * adjust)
                :setPaint(Color.gray(0.2, 0.5))
                :stroke(path)
        end)
    end
end

function ScrollerHandle:status(metrics, view, x, y)
    local min, max = self.range:getValue(view)
    local visibleMin, visibleMax = self.visibleRange:getValue(view)
    local x1, y1, x2, y2 = self.track:getValue(view)
    local numerator = (x - x1) * (x2 - x1) + (y - y1) * (y2 - y1)
    local denominator = (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)
    return (max - min) * numerator / denominator - (visibleMin - min)
end

function ScrollerHandle:dragged(metrics, view, x, y, down)
    local min, max = self.range:getValue(view)
    local visibleMin, visibleMax = self.visibleRange:getValue(view)
    local current = self:status(metrics, view, x, y)
    current = math.min(math.max(min - visibleMin, current - down), max - visibleMax)
    self.visibleRange:setValue(view, visibleMin + current, visibleMax + current)
    self:action()
end

--

SliderHandle = Handle:new()

function SliderHandle:draw(canvas, view)
    local adjust = canvas:metrics():growthAdjustment()
    local x1, y1, x2, y2 = self.track:getValue(view)
    if x1 ~= x2 or y1 ~= y2 then
        local fraction = self.position:getValue(view)
        local x = x1 * (1 - fraction) + x2 * fraction
        local y = y1 * (1 - fraction) + y2 * fraction
        local size = 5 * adjust
        local path = Path.line{
            x1 = 0,
            y1 = 0 - size,
            x2 = 0,
            y2 = 0 + size,
        }
        local t = Transformation.identity():translate(x, y):rotate(math.atan2(y2 - y1, x2 - x1))
        canvas:preserve(function(canvas)
            canvas:concatTransformation(t)
                :setThickness(6 * adjust):setPaint(Color.black):stroke(path)
                :setThickness(3 * adjust):setPaint(Color.white):stroke(path)
        end)
    end
end

function SliderHandle:dragged(metrics, view, x, y, down)
    local x1, y1, x2, y2 = self.track:getValue(view)
    local numerator = (x - x1) * (x2 - x1) + (y - y1) * (y2 - y1)
    local denominator = (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)
    local fraction = math.min(math.max(0, numerator / denominator), 1)
    self.position:setValue(view, fraction)
    self:action()
end

--

PanningHandle = Handle:new()

function PanningHandle:draw(canvas, view)
    local x1, y1, x2, y2 = self.track:getValue(view)
    if x1 ~= x2 or y1 ~= y2 then
        local metrics = canvas:metrics()
        local adjust = metrics:growthAdjustment()
        local thickness, needsAdjust = self.trackThickness:getValue(view)
        thickness = needsAdjust and (thickness * adjust) or thickness
        local pixelSize = 1 / metrics:pointScale()
        local pixels = math.floor(0.5 + thickness / 2 / pixelSize)
        local xPadding = (y1 ~= y2 and pixels * pixelSize) or 0
        local yPadding = (x1 ~= x2 and pixels * pixelSize) or 0
        local left, bottom = metrics:alignToPixelEdge(x1 - xPadding, y1 - yPadding)
        local right, top = metrics:alignToPixelEdge(x2 + xPadding, y2 + yPadding)
        local path = Path.rect{left = left, bottom = bottom, right = right, top = top}
        canvas:setPaint(Color.invisible)
            :fill(path)
    end
end

function PanningHandle:cursorRects(metrics, view)
    local cursorRects = {}
    local x1, y1, x2, y2 = self.track:getValue(view)
    if x1 ~= x2 or y1 ~= y2 then
        local adjust = metrics:growthAdjustment()
        local thickness, needsAdjust = self.trackThickness:getValue(view)
        thickness = needsAdjust and (thickness * adjust) or thickness
        local pixelSize = 1 / metrics:pointScale()
        local pixels = math.floor(0.5 + thickness / 2 / pixelSize)
        local xPadding = (y1 ~= y2 and pixels * pixelSize) or 0
        local yPadding = (x1 ~= x2 and pixels * pixelSize) or 0
        local left, bottom = metrics:alignToPixelEdge(x1 - xPadding, y1 - yPadding)
        local right, top = metrics:alignToPixelEdge(x2 + xPadding, y2 + yPadding)
        local rect = Rect:new{left = left, bottom = bottom, right = right, top = top}
        cursorRects[#cursorRects + 1] = { rect, 'hand' }
    end
    return cursorRects
end

function PanningHandle:status(metrics, view, x, y)
    local tx, ty = self.token:getValue(view, x, y)
    return {x = x, y = y, tx = tx, ty = ty}
end

function PanningHandle:dragged(metrics, view, x, y, down)
    local adjust = metrics:growthAdjustment()
    local thickness, needsAdjust = self.trackThickness:getValue(view)
    thickness = needsAdjust and (thickness * adjust) or thickness
    if not down.movex and not down.movey then
        down.movex = (math.abs(x - down.x) > thickness / 2)
        down.movey = (math.abs(y - down.y) > thickness / 2)
    end
    down.movex = down.movex or (math.abs(x - down.x) > 3 * thickness)
    down.movey = down.movey or (math.abs(y - down.y) > 3 * thickness)
    if down.movex or down.movey then
        self.token:setValue(view, down.movex and down.tx, down.movey and down.ty, x, y)
        self:action()
    end
end

--

MultiResizerHandle = Handle:new()

function MultiResizerHandle:draw(canvas, view)
    local x1, y1, x2, y2 = self.track:getValue(view)
    if x1 ~= x2 or y1 ~= y2 then
        local metrics = canvas:metrics()
        local adjust = metrics:growthAdjustment()
        local thickness, needsAdjust = self.trackThickness:getValue(view)
        thickness = needsAdjust and (thickness * adjust) or thickness
        local pixelSize = 1 / metrics:pointScale()
        local pixels = math.floor(0.5 + thickness / 2 / pixelSize)
        local grabPixels = math.floor(0.5 + 3 * adjust / pixelSize)
        local xPadding = (y1 ~= y2 and pixels * pixelSize) or grabPixels * pixelSize
        local yPadding = (x1 ~= x2 and pixels * pixelSize) or grabPixels * pixelSize
        local tokens, positions = self.tokenPositions:getValue(view)
        canvas:setPaint(Color.invisible)
        for index = 1, #positions do
            local x = (x1 == x2 and x1 or positions[index])
            local y = (y1 == y2 and y1 or positions[index])
            local left, bottom = metrics:alignToPixelEdge(x - xPadding, y - yPadding)
            local right, top = metrics:alignToPixelEdge(x + xPadding, y + yPadding)
            local path = Path.rect{left = left, bottom = bottom, right = right, top = top}
            canvas:fill(path)
        end
    end
end

function MultiResizerHandle:cursorRects(metrics, view)
    local cursorRects = {}
    local x1, y1, x2, y2 = self.track:getValue(view)
    if x1 ~= x2 or y1 ~= y2 then
        local cursorName = (y1 == y2 and 'resize-e-w') or 'resize-n-s'
        local adjust = metrics:growthAdjustment()
        local thickness, needsAdjust = self.trackThickness:getValue(view)
        thickness = needsAdjust and (thickness * adjust) or thickness
        local pixelSize = 1 / metrics:pointScale()
        local pixels = math.floor(0.5 + thickness / 2 / pixelSize)
        local grabPixels = math.floor(0.5 + 3 * adjust / pixelSize)
        local xPadding = (y1 ~= y2 and pixels * pixelSize) or grabPixels * pixelSize
        local yPadding = (x1 ~= x2 and pixels * pixelSize) or grabPixels * pixelSize
        local tokens, positions = self.tokenPositions:getValue(view)
        for index = 1, #positions do
            local x = (x1 == x2 and x1 or positions[index])
            local y = (y1 == y2 and y1 or positions[index])
            local left, bottom = metrics:alignToPixelEdge(x - xPadding, y - yPadding)
            local right, top = metrics:alignToPixelEdge(x + xPadding, y + yPadding)
            local rect = Rect:new{left = left, bottom = bottom, right = right, top = top}
            cursorRects[#cursorRects + 1] = { rect, cursorName }
        end
    end
    return cursorRects
end

function MultiResizerHandle:status(metrics, view, x, y)
    local x1, y1, x2, y2 = self.track:getValue(view)
    local position = (x1 ~= x2 and x or y)
    local tokens, positions = self.tokenPositions:getValue(view)
    local bestDistance = math.huge
    local bestPosition, bestToken
    for index = 1, #positions do
        local distance = math.abs(positions[index] - position)
        if distance < bestDistance then
            bestDistance = distance
            bestPosition = positions[index]
            bestToken = tokens[index]
        end
    end
    return {token = bestToken, position = bestPosition}
end

function MultiResizerHandle:dragged(metrics, view, x, y, down)
    local x1, y1, x2, y2 = self.track:getValue(view)
    local position = (x1 ~= x2 and x or y)
    self.tokenPositions:setValue(view, down.token, position)
    self:action()
end

--

AxisHandle = Handle:new()

function AxisHandle:draw(canvas, view)
    local x1, y1, x2, y2 = self.track:getValue(view)
    if x1 ~= x2 or y1 ~= y2 then
        local metrics = canvas:metrics()
        local adjust = metrics:growthAdjustment()
        local thickness, needsAdjust = self.trackThickness:getValue(view)
        thickness = needsAdjust and (thickness * adjust) or thickness
        local pixelSize = 1 / metrics:pointScale()
        local pixels = math.floor(0.5 + thickness / 2 / pixelSize)
        local grabPixels = math.floor(0.5 + 3 * adjust / pixelSize)
        local xPadding = (y1 ~= y2 and pixels * pixelSize) or grabPixels * pixelSize
        local yPadding = (x1 ~= x2 and pixels * pixelSize) or grabPixels * pixelSize
        local left, bottom = metrics:alignToPixelEdge(x1 - xPadding, y1 - yPadding)
        local right, top = metrics:alignToPixelEdge(x2 + xPadding, y2 + yPadding)
        local path = Path.rect{left = left, bottom = bottom, right = right, top = top}
        canvas:setPaint(Color.invisible)
            :fill(path)
    end
end

function AxisHandle:cursorRects(metrics, view)
    local cursorRects = {}
    local x1, y1, x2, y2 = self.track:getValue(view)
    if x1 ~= x2 or y1 ~= y2 then
        local panningCursorName = 'hand'
        local resizeCursorName = (y1 == y2 and 'resize-e-w') or 'resize-n-s'
        local adjust = metrics:growthAdjustment()
        local thickness, needsAdjust = self.trackThickness:getValue(view)
        thickness = needsAdjust and (thickness * adjust) or thickness
        local pixelSize = 1 / metrics:pointScale()
        local pixels = math.floor(0.5 + thickness / 2 / pixelSize)
        local grabPixels = math.floor(0.5 + 3 * adjust / pixelSize)
        local xPadding = (y1 ~= y2 and pixels * pixelSize) or grabPixels * pixelSize
        local yPadding = (x1 ~= x2 and pixels * pixelSize) or grabPixels * pixelSize
        local tokens, positions = self.tokenPositions:getValue(view)
        local last
        if x1 ~= x2 then
            last = metrics:alignToPixelEdge(x1 - xPadding, 0)
        else
            local _
            _, last = metrics:alignToPixelEdge(0, y1 - yPadding)
        end
        for index = 1, #positions do
            local x = (x1 == x2 and x1 or positions[index])
            local y = (y1 == y2 and y1 or positions[index])
            local left, bottom = metrics:alignToPixelEdge(x - xPadding, y - yPadding)
            local right, top = metrics:alignToPixelEdge(x + xPadding, y + yPadding)
            local panningRect
            if x1 ~= x2 then
                if left > last then
                    panningRect = Rect:new{left = last, bottom = bottom, right = left, top = top}
                end
                last = right
            else
                if bottom > last then
                    panningRect = Rect:new{left = left, bottom = last, right = right, top = bottom}
                end
                last = top
            end
            if panningRect then
                cursorRects[#cursorRects + 1] = { panningRect, panningCursorName }
            end
            local rect = Rect:new{left = left, bottom = bottom, right = right, top = top}
            cursorRects[#cursorRects + 1] = { rect, resizeCursorName }
        end
        local left, bottom = metrics:alignToPixelEdge(x1 - xPadding, y1 - yPadding)
        local right, top = metrics:alignToPixelEdge(x2 + xPadding, y2 + yPadding)
        local panningRect
        if x1 ~= x2 then
            if last < right then
                panningRect = Rect:new{left = last, bottom = bottom, right = right, top = top}
            end
        else
            if last < top then
                panningRect = Rect:new{left = left, bottom = last, right = right, top = top}
            end
        end
        if panningRect then
            cursorRects[#cursorRects + 1] = { panningRect, panningCursorName }
        end
    end
    return cursorRects
end

function AxisHandle:status(metrics, view, x, y)
    local x1, y1, x2, y2 = self.track:getValue(view)
    local adjust = metrics:growthAdjustment()
    local thickness, needsAdjust = self.trackThickness:getValue(view)
    thickness = needsAdjust and (thickness * adjust) or thickness
    local pixelSize = 1 / metrics:pointScale()
    local pixels = math.floor(0.5 + thickness / 2 / pixelSize)
    local grabPixels = math.floor(0.5 + 3 * adjust / pixelSize)
    local xPadding = (y1 ~= y2 and pixels * pixelSize) or grabPixels * pixelSize
    local yPadding = (x1 ~= x2 and pixels * pixelSize) or grabPixels * pixelSize
    local tokens, positions = self.tokenPositions:getValue(view)
    local token, position
    for index = 1, #positions do
        local tokenX = (x1 == x2 and x1 or positions[index])
        local tokenY = (y1 == y2 and y1 or positions[index])
        local left, bottom = metrics:alignToPixelEdge(tokenX - xPadding, tokenY - yPadding)
        local right, top = metrics:alignToPixelEdge(tokenX + xPadding, tokenY + yPadding)
        if left <= x and x <= right and bottom <= y and y <= top then
            token = tokens[index]
            position = positions[index]
            break
        end
    end
    local tx, ty = self.token:getValue(view, x, y)
    return {
        token = token, position = position,
        x = x, y = y,
        tx = tx, ty = ty,
    }
end

function AxisHandle:dragged(metrics, view, x, y, down)
    if down.token and down.position then
        return MultiResizerHandle.dragged(self, metrics, view, x, y, down)
    else
        return PanningHandle.dragged(self, metrics, view, x, y, down)
    end
end

--

return Handle
