diff options
Diffstat (limited to 'src/display/control/canvas-item-rect.cpp')
-rw-r--r-- | src/display/control/canvas-item-rect.cpp | 262 |
1 files changed, 262 insertions, 0 deletions
diff --git a/src/display/control/canvas-item-rect.cpp b/src/display/control/canvas-item-rect.cpp new file mode 100644 index 0000000..0204cdf --- /dev/null +++ b/src/display/control/canvas-item-rect.cpp @@ -0,0 +1,262 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * A class to represent a control rectangle. Used for rubberband selector, page outline, etc. + */ + +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of CtrlRect + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cairo/cairo.h> + +#include "canvas-item-rect.h" + +#include "display/cairo-utils.h" +#include "color.h" // SP_RGBA_x_F +#include "helper/geom.h" +#include "inkscape.h" +#include "ui/util.h" +#include "ui/widget/canvas.h" + +namespace Inkscape { + +/** + * Create an null control rect. + */ +CanvasItemRect::CanvasItemRect(CanvasItemGroup *group) + : CanvasItem(group) +{ + _name = "CanvasItemRect:Null"; +} + +/** + * Create a control rect. Point are in document coordinates. + */ +CanvasItemRect::CanvasItemRect(CanvasItemGroup *group, Geom::Rect const &rect) + : CanvasItem(group) + , _rect(rect) +{ + _name = "CanvasItemRect"; +} + +/** + * Set a control rect. Points are in document coordinates. + */ +void CanvasItemRect::set_rect(Geom::Rect const &rect) +{ + defer([=] { + if (_rect == rect) return; + _rect = rect; + request_update(); + }); +} + +/** + * Run a callback for each rectangle that should be filled and painted in the background. + */ +void CanvasItemRect::visit_page_rects(std::function<void(Geom::Rect const &)> const &f) const +{ + if (_is_page && _fill != 0) { + f(_rect); + } +} + +/** + * Returns true if point p (in canvas units) is within tolerance (canvas units) distance of rect. + * Non-zero tolerance not implemented! Is valid for a rotated canvas. + */ +bool CanvasItemRect::contains(Geom::Point const &p, double tolerance) +{ + if (tolerance != 0) { + std::cerr << "CanvasItemRect::contains: Non-zero tolerance not implemented!" << std::endl; + } + + return _rect.contains(p * affine().inverse()); +} + +/** + * Update and redraw control rect. + */ +void CanvasItemRect::_update(bool) +{ + // Queue redraw of old area (erase previous content). + request_redraw(); + + // Enlarge bbox by twice shadow size (to allow for shadow on any side with a 45deg rotation). + _bounds = _rect; + // note: add shadow size before applying transformation, since get_shadow_size accounts for scale + if (_shadow_width > 0 && !_dashed) { + _bounds->expandBy(2 * get_shadow_size()); + } + *_bounds *= affine(); + _bounds->expandBy(2); // Room for stroke. + + // Queue redraw of new area + request_redraw(); +} + +/** + * Render rect to screen via Cairo. + */ +void CanvasItemRect::_render(Inkscape::CanvasItemBuffer &buf) const +{ + // Are we axis aligned? + auto const &aff = affine(); + bool const axis_aligned = (Geom::are_near(aff[1], 0) && Geom::are_near(aff[2], 0)) + || (Geom::are_near(aff[0], 0) && Geom::are_near(aff[3], 0)); + + // If so, then snap the rectangle to the pixel grid. + auto rect = _rect; + if (axis_aligned) { + rect = (floor(_rect * aff) + Geom::Point(0.5, 0.5)) * aff.inverse(); + } + + buf.cr->save(); + buf.cr->translate(-buf.rect.left(), -buf.rect.top()); + + if (_inverted) { + cairo_set_operator(buf.cr->cobj(), CAIRO_OPERATOR_DIFFERENCE); + } + + // Draw shadow first. Shadow extends under rectangle to reduce aliasing effects. Canvas draws page shadows in OpenGL mode. + if (_shadow_width > 0 && !_dashed && !(_is_page && get_canvas()->get_opengl_enabled())) { + // There's only one UI knob to adjust border and shadow color, so instead of using border color + // transparency as is, it is boosted by this function, since shadow attenuates it. + auto const alpha = (std::exp(-3 * SP_RGBA32_A_F(_shadow_color)) - 1) / (std::exp(-3) - 1); + + // Flip shadow upside-down if y-axis is inverted. + auto doc2dt = Geom::identity(); + if (auto desktop = get_canvas()->get_desktop()) { + doc2dt = desktop->doc2dt(); + } + + buf.cr->save(); + buf.cr->transform(geom_to_cairo(doc2dt * aff)); + ink_cairo_draw_drop_shadow(buf.cr, rect * doc2dt, get_shadow_size(), _shadow_color, alpha); + buf.cr->restore(); + } + + // Get the points we need transformed into window coordinates. + buf.cr->begin_new_path(); + for (int i = 0; i < 4; ++i) { + auto pt = rect.corner(i) * aff; + buf.cr->line_to(pt.x(), pt.y()); + } + buf.cr->close_path(); + + // Draw border. + static std::valarray<double> dashes = {4.0, 4.0}; + if (_dashed) { + buf.cr->set_dash(dashes, -0.5); + } + buf.cr->set_line_width(1); + // we maybe have painted the background, back to "normal" compositing + buf.cr->set_source_rgba(SP_RGBA32_R_F(_stroke), SP_RGBA32_G_F(_stroke), + SP_RGBA32_B_F(_stroke), SP_RGBA32_A_F(_stroke)); + buf.cr->stroke_preserve(); + + // Highlight the border by drawing it in _shadow_color. + if (_shadow_width == 1 && _dashed) { + buf.cr->set_dash(dashes, 3.5); // Dash offset by dash length. + buf.cr->set_source_rgba(SP_RGBA32_R_F(_shadow_color), SP_RGBA32_G_F(_shadow_color), + SP_RGBA32_B_F(_shadow_color), SP_RGBA32_A_F(_shadow_color)); + buf.cr->stroke_preserve(); + } + + buf.cr->begin_new_path(); // Clear path or get weird artifacts. + + // Uncomment to show bounds + // Geom::Rect bounds = _bounds; + // bounds.expandBy(-1); + // bounds -= buf.rect.min(); + // buf.cr->set_source_rgba(1.0, 0.0, _shadow_width / 3.0, 1.0); + // buf.cr->rectangle(bounds.min().x(), bounds.min().y(), bounds.width(), bounds.height()); + // buf.cr->stroke(); + + buf.cr->restore(); +} + +void CanvasItemRect::set_is_page(bool is_page) +{ + defer([=] { + if (_is_page == is_page) return; + _is_page = is_page; + request_redraw(); + }); +} + +void CanvasItemRect::set_fill(uint32_t fill) +{ + if (fill != _fill && _is_page) get_canvas()->set_page(fill); + CanvasItem::set_fill(fill); +} + +void CanvasItemRect::set_dashed(bool dashed) +{ + defer([=] { + if (_dashed == dashed) return; + _dashed = dashed; + request_redraw(); + }); +} + +void CanvasItemRect::set_inverted(bool inverted) +{ + defer([=] { + if (_inverted == inverted) return; + _inverted = inverted; + request_redraw(); + }); +} + +void CanvasItemRect::set_shadow(uint32_t color, int width) +{ + defer([=] { + if (_shadow_color == color && _shadow_width == width) return; + _shadow_color = color; + _shadow_width = width; + request_redraw(); + if (_is_page) get_canvas()->set_border(_shadow_width > 0 ? color : 0x0); + }); +} + +double CanvasItemRect::get_shadow_size() const +{ + // gradient drop shadow needs much more room than solid one, so inflating the size; + // fudge factor of 6 used to make sizes baked in svg documents work as steps: + // typical value of 2 will work out to 12 pixels which is a narrow shadow (b/c of exponential fall of) + auto size = _shadow_width * 6; + if (size < 0) { + size = 0; + } else if (size > 120) { + // arbitrarily selected max size, so Cairo gradient doesn't blow up if document has bogus shadow values + size = 120; + } + auto scale = affine().descrim(); + + // calculate space for gradient shadow; if divided by 'scale' it would be zoom independent (fixed in size); + // if 'scale' is not used, drop shadow will be getting smaller with document zoom; + // here hybrid approach is used: "unscaling" with square root of scale allows shadows to diminish + // more slowly at small zoom levels (so it's still perceptible) and grow more slowly at high mag (where it doesn't matter, b/c it's typically off-screen) + return size / (scale > 0 ? sqrt(scale) : 1); +} + +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : |