Only render the visible area in canvas.cpp

If a widget is partially hidden or off-screen, this commit makes canvas.cpp
optimise and only draw the visible part.

Many of the asserts in canvas.cpp are removed, as the surface is no longer the
full canvas. The SDL_Render* functions do support the out-of-bounds and
negative values for x and y that are now passed to them, these are tested by
SDL2's testviewport.c.

Scrolling by a single pixel will force canvas::draw to do the full work of
redrawing the canvas. I had considered rendering a few of the off-screen lines
too, however it seems this isn't optimisable because the dirty flag is already
set on each redraw - that can be traced to window.cpp's push_draw_event()
causing canvas::set_is_dirty(true) to be called.

Refactor handling of the x, y, w, h variables with a common rect_bounded_shape
class, so that there are less code paths that might have unnoticed bugs.
This commit is contained in:
Steve Cotton 2021-03-15 05:14:08 +01:00 committed by Steve Cotton
parent d7234242d4
commit b2d39d2754
4 changed files with 278 additions and 222 deletions

View file

@ -18,6 +18,7 @@
### WML Engine
* Modify implementation of overwrite_specials attribute for replace yes/no parameter by none/one_side/both_sides and select abilities used like weapons and specials who must be overwrited(owned by fighter where special applied or both)
### Miscellaneous and Bug Fixes
* More optimizations in the UI drawing code, these shouldn't have visible effects (PR #5681).
* Made GUI.pyw compatible with Python 3.9 (issue #5719).
* Removed workarounds for bugs affecting older SDL 2.0 versions, including an extra copy of the game screen made during gamemap scrolling (PR #5736).
* FPS values calculated when the :fps or :benchmark are now written to a file which can then be used to track FPS values over time.

View file

@ -50,9 +50,6 @@ static void set_renderer_color(SDL_Renderer* renderer, color_t color)
/**
* Draws a line on a surface.
*
* @pre The caller needs to make sure the entire line fits on
* the @p surface.
* @pre @p x2 >= @p x1
* @pre The @p surface is locked.
*
* @param canvas The canvas to draw upon, the caller should lock the
@ -77,11 +74,6 @@ static void draw_line(surface& canvas,
<< ',' << y2 << " canvas width " << w << " canvas height "
<< canvas->h << ".\n";
assert(static_cast<int>(x1) < canvas->w);
assert(static_cast<int>(x2) < canvas->w);
assert(static_cast<int>(y1) < canvas->h);
assert(static_cast<int>(y2) < canvas->h);
set_renderer_color(renderer, color);
if(x1 == x2 && y1 == y2) {
@ -95,7 +87,6 @@ static void draw_line(surface& canvas,
/**
* Draws a circle on a surface.
*
* @pre The circle must fit on the canvas.
* @pre The @p surface is locked.
*
* @param canvas The canvas to draw upon, the caller should lock the
@ -120,11 +111,6 @@ static void draw_circle(surface& canvas,
<< " with radius " << radius << " canvas width " << w
<< " canvas height " << canvas->h << ".\n";
if(octants & 0x0f) assert((x_center + radius) < canvas->w);
if(octants & 0xf0) assert((x_center - radius) >= 0);
if(octants & 0x3c) assert((y_center + radius) < canvas->h);
if(octants & 0xc3) assert((y_center - radius) >= 0);
set_renderer_color(renderer, color);
// Algorithm based on
@ -161,7 +147,6 @@ static void draw_circle(surface& canvas,
/**
* Draws a filled circle on a surface.
*
* @pre The circle must fit on the canvas.
* @pre The @p surface is locked.
*
* @param canvas The canvas to draw upon, the caller should lock the
@ -186,11 +171,6 @@ static void fill_circle(surface& canvas,
<< " with radius " << radius << " canvas width " << w
<< " canvas height " << canvas->h << ".\n";
if(octants & 0x0f) assert((x_center + radius) < canvas->w);
if(octants & 0xf0) assert((x_center - radius) >= 0);
if(octants & 0x3c) assert((y_center + radius) < canvas->h);
if(octants & 0xc3) assert((y_center - radius) >= 0);
set_renderer_color(renderer, color);
int d = -static_cast<int>(radius);
@ -241,6 +221,7 @@ line_shape::line_shape(const config& cfg)
void line_shape::draw(surface& canvas,
SDL_Renderer* renderer,
const SDL_Rect& view_bounds,
wfl::map_formula_callable& variables)
{
/**
@ -249,20 +230,14 @@ void line_shape::draw(surface& canvas,
* flag or do the calculation in a separate routine.
*/
const unsigned x1 = x1_(variables);
const unsigned y1 = y1_(variables);
const unsigned x2 = x2_(variables);
const unsigned y2 = y2_(variables);
const unsigned x1 = x1_(variables) - view_bounds.x;
const unsigned y1 = y1_(variables) - view_bounds.y;
const unsigned x2 = x2_(variables) - view_bounds.x;
const unsigned y2 = y2_(variables) - view_bounds.y;
DBG_GUI_D << "Line: draw from " << x1 << ',' << y1 << " to " << x2 << ','
<< y2 << " canvas size " << canvas->w << ',' << canvas->h
<< ".\n";
VALIDATE(static_cast<int>(x1) < canvas->w
&& static_cast<int>(x2) < canvas->w
&& static_cast<int>(y1) < canvas->h
&& static_cast<int>(y2) < canvas->h,
_("Line doesn't fit on canvas."));
DBG_GUI_D << "Line: draw from " << x1 << ',' << y1 << " to " << x2 << ',' << y2
<< " within bounds {" << view_bounds.x << ", " << view_bounds.y
<< ", " << view_bounds.w << ", " << view_bounds.h << "}.\n";
// @todo FIXME respect the thickness.
@ -272,14 +247,59 @@ void line_shape::draw(surface& canvas,
draw_line(canvas, renderer, color_(variables), x1, y1, x2, y2);
}
/***** ***** ***** ***** ***** Rectangle ***** ***** ***** ***** *****/
/***** ***** ***** Base class for rectangular shapes ***** ***** *****/
rectangle_shape::rectangle_shape(const config& cfg)
rect_bounded_shape::rect_bounded_shape(const config& cfg)
: shape(cfg)
, x_(cfg["x"])
, y_(cfg["y"])
, w_(cfg["w"])
, h_(cfg["h"])
{
}
rect_bounded_shape::calculated_rects rect_bounded_shape::calculate_rects(const SDL_Rect& view_bounds, wfl::map_formula_callable& variables) const
{
// Formulas are recalculated every draw cycle, even if there hasn't been a resize.
const unsigned x = x_(variables);
const unsigned y = y_(variables);
const unsigned w = w_(variables);
const unsigned h = h_(variables);
const auto dst_on_widget = sdl::create_rect(x, y, w, h);
SDL_Rect clip_on_widget;
if(!SDL_IntersectRect(&dst_on_widget, &view_bounds, &clip_on_widget)) {
DBG_GUI_D << "Text: Clipping view_bounds resulted in an empty intersection, nothing to do.\n";
return {true, dst_on_widget, {}, {}, {}, {}};
}
auto unclipped_around_viewport = dst_on_widget;
unclipped_around_viewport.x -= view_bounds.x;
unclipped_around_viewport.y -= view_bounds.y;
auto clip_in_shape = clip_on_widget;
clip_in_shape.x -= x;
clip_in_shape.y -= y;
auto dst_in_viewport = clip_on_widget;
dst_in_viewport.x -= view_bounds.x;
dst_in_viewport.y -= view_bounds.y;
DBG_GUI_D << "Calculate_rects: from " << x << ',' << y << " width " << w << " height " << h << "\n"
<< " view_bounds {" << view_bounds.x << ", " << view_bounds.y << ", "
<< view_bounds.w << ", " << view_bounds.h << "}.\n"
<< " dst_in_viewport {" << dst_in_viewport.x << ", " << dst_in_viewport.y << ", "
<< dst_in_viewport.w << ", " << dst_in_viewport.h << "}.\n";
return {false, dst_on_widget, clip_on_widget, clip_in_shape, unclipped_around_viewport, dst_in_viewport};
}
/***** ***** ***** ***** ***** Rectangle ***** ***** ***** ***** *****/
rectangle_shape::rectangle_shape(const config& cfg)
: rect_bounded_shape(cfg)
, border_thickness_(cfg["border_thickness"])
, border_color_(cfg["border_color"], color_t::null_color())
, fill_color_(cfg["fill_color"], color_t::null_color())
@ -297,55 +317,38 @@ rectangle_shape::rectangle_shape(const config& cfg)
void rectangle_shape::draw(surface& canvas,
SDL_Renderer* renderer,
const SDL_Rect& view_bounds,
wfl::map_formula_callable& variables)
{
/**
* @todo formulas are now recalculated every draw cycle which is a bit
* silly unless there has been a resize. So to optimize we should use an
* extra flag or do the calculation in a separate routine.
*/
const int x = x_(variables);
const int y = y_(variables);
const int w = w_(variables);
const int h = h_(variables);
DBG_GUI_D << "Rectangle: draw from " << x << ',' << y << " width " << w
<< " height " << h << " canvas size " << canvas->w << ','
<< canvas->h << ".\n";
VALIDATE(x < canvas->w
&& x + w <= canvas->w
&& y < canvas->h
&& y + h <= canvas->h, _("Rectangle doesn't fit on canvas."));
const auto rects = calculate_rects(view_bounds, variables);
if(rects.empty) {
return;
}
surface_lock locker(canvas);
const color_t fill_color = fill_color_(variables);
// Fill the background, if applicable
if(!fill_color.null() && w && h) {
if(!fill_color.null()) {
set_renderer_color(renderer, fill_color);
SDL_Rect area {
x + border_thickness_,
y + border_thickness_,
w - (border_thickness_ * 2),
h - (border_thickness_ * 2)
};
auto area = rects.unclipped_around_viewport;
area.x += border_thickness_;
area.y += border_thickness_;
area.w -= 2 * border_thickness_;
area.h -= 2 * border_thickness_;
SDL_RenderFillRect(renderer, &area);
}
// Draw the border
set_renderer_color(renderer, border_color_(variables));
for(int i = 0; i < border_thickness_; ++i) {
SDL_Rect dimensions {
x + i,
y + i,
w - (i * 2),
h - (i * 2)
};
set_renderer_color(renderer, border_color_(variables));
auto dimensions = rects.unclipped_around_viewport;
dimensions.x += i;
dimensions.y += i;
dimensions.w -= 2 * i;
dimensions.h -= 2 * i;
SDL_RenderDrawRect(renderer, &dimensions);
}
@ -354,11 +357,7 @@ void rectangle_shape::draw(surface& canvas,
/***** ***** ***** ***** ***** Rounded Rectangle ***** ***** ***** ***** *****/
round_rectangle_shape::round_rectangle_shape(const config& cfg)
: shape(cfg)
, x_(cfg["x"])
, y_(cfg["y"])
, w_(cfg["w"])
, h_(cfg["h"])
: rect_bounded_shape(cfg)
, r_(cfg["corner_radius"])
, border_thickness_(cfg["border_thickness"])
, border_color_(cfg["border_color"], color_t::null_color())
@ -377,27 +376,20 @@ round_rectangle_shape::round_rectangle_shape(const config& cfg)
void round_rectangle_shape::draw(surface& canvas,
SDL_Renderer* renderer,
const SDL_Rect& view_bounds,
wfl::map_formula_callable& variables)
{
/**
* @todo formulas are now recalculated every draw cycle which is a bit
* silly unless there has been a resize. So to optimize we should use an
* extra flag or do the calculation in a separate routine.
*/
const int x = x_(variables);
const int y = y_(variables);
const int w = w_(variables);
const int h = h_(variables);
const auto rects = calculate_rects(view_bounds, variables);
const int x = rects.unclipped_around_viewport.x;
const int y = rects.unclipped_around_viewport.y;
const int w = rects.unclipped_around_viewport.w;
const int h = rects.unclipped_around_viewport.h;
const int r = r_(variables);
DBG_GUI_D << "Rounded Rectangle: draw from " << x << ',' << y << " width " << w
<< " height " << h << " canvas size " << canvas->w << ','
<< canvas->h << ".\n";
VALIDATE(x < canvas->w
&& x + w <= canvas->w
&& y < canvas->h
&& y + h <= canvas->h, _("Rounded Rectangle doesn't fit on canvas."));
<< " height "
<< " within bounds {" << view_bounds.x << ", " << view_bounds.y
<< ", " << view_bounds.w << ", " << view_bounds.h << "}.\n";
surface_lock locker(canvas);
@ -459,6 +451,7 @@ circle_shape::circle_shape(const config& cfg)
void circle_shape::draw(surface& canvas,
SDL_Renderer* renderer,
const SDL_Rect& view_bounds,
wfl::map_formula_callable& variables)
{
/**
@ -467,34 +460,13 @@ void circle_shape::draw(surface& canvas,
* extra flag or do the calculation in a separate routine.
*/
const unsigned x = x_(variables);
const unsigned y = y_(variables);
const int x = x_(variables) - view_bounds.x;
const int y = y_(variables) - view_bounds.y;
const unsigned radius = radius_(variables);
DBG_GUI_D << "Circle: drawn at " << x << ',' << y << " radius " << radius
<< " canvas size " << canvas->w << ',' << canvas->h << ".\n";
VALIDATE_WITH_DEV_MESSAGE(
static_cast<int>(x - radius) >= 0,
_("Circle doesn't fit on canvas."),
formatter() << "x = " << x << ", radius = " << radius);
VALIDATE_WITH_DEV_MESSAGE(
static_cast<int>(y - radius) >= 0,
_("Circle doesn't fit on canvas."),
formatter() << "y = " << y << ", radius = " << radius);
VALIDATE_WITH_DEV_MESSAGE(
static_cast<int>(x + radius) < canvas->w,
_("Circle doesn't fit on canvas."),
formatter() << "x = " << x << ", radius = " << radius
<< "', canvas width = " << canvas->w << ".");
VALIDATE_WITH_DEV_MESSAGE(
static_cast<int>(y + radius) < canvas->h,
_("Circle doesn't fit on canvas."),
formatter() << "y = " << y << ", radius = " << radius
<< "', canvas height = " << canvas->h << ".");
<< " within bounds {" << view_bounds.x << ", " << view_bounds.y
<< ", " << view_bounds.w << ", " << view_bounds.h << "}.\n";
// lock the surface
surface_lock locker(canvas);
@ -542,6 +514,7 @@ void image_shape::dimension_validation(unsigned value, const std::string& name,
void image_shape::draw(surface& canvas,
SDL_Renderer* /*renderer*/,
const SDL_Rect& view_bounds,
wfl::map_formula_callable& variables)
{
DBG_GUI_D << "Image: draw.\n";
@ -587,10 +560,7 @@ void image_shape::draw(surface& canvas,
local_variables.add("image_height", wfl::variant(h ? h : image_->h));
const unsigned clip_x = x_(local_variables);
dimension_validation(clip_x, name, "x");
const unsigned clip_y = y_(local_variables);
dimension_validation(clip_y, name, "y");
local_variables.add("clip_x", wfl::variant(clip_x));
local_variables.add("clip_y", wfl::variant(clip_y));
@ -600,7 +570,6 @@ void image_shape::draw(surface& canvas,
// Copy the data to local variables to avoid overwriting the originals.
SDL_Rect src_clip = src_clip_;
SDL_Rect dst_clip = sdl::create_rect(clip_x, clip_y, 0, 0);
surface surf;
// Test whether we need to scale and do the scaling if needed.
@ -656,11 +625,26 @@ void image_shape::draw(surface& canvas,
src_clip.h = h;
}
// Calculate the destination, clip it to the view_bounds, and then change both
// src_clip and dst_on_view_bounds to view_bounds' coordinate system.
const SDL_Rect dst_on_widget = sdl::create_rect(clip_x, clip_y, src_clip.w, src_clip.h);
SDL_Rect dst_on_view_bounds;
if(!SDL_IntersectRect(&dst_on_widget, &view_bounds, &dst_on_view_bounds)) {
DBG_GUI_D << "Image: completely outside view_bounds\n";
return;
}
dst_on_view_bounds.x -= view_bounds.x;
dst_on_view_bounds.y -= view_bounds.y;
src_clip.x += view_bounds.x;
src_clip.y += view_bounds.y;
src_clip.w = dst_on_view_bounds.w;
src_clip.h = dst_on_view_bounds.h;
if(vertical_mirror_(local_variables)) {
surf = flip_surface(surf);
}
blit_surface(surf, &src_clip, canvas, &dst_clip);
blit_surface(surf, &src_clip, canvas, &dst_on_view_bounds);
}
image_shape::resize_mode image_shape::get_resize_mode(const std::string& resize_mode)
@ -683,11 +667,7 @@ image_shape::resize_mode image_shape::get_resize_mode(const std::string& resize_
/***** ***** ***** ***** ***** TEXT ***** ***** ***** ***** *****/
text_shape::text_shape(const config& cfg)
: shape(cfg)
, x_(cfg["x"])
, y_(cfg["y"])
, w_(cfg["w"])
, h_(cfg["h"])
: rect_bounded_shape(cfg)
, font_family_(font::str_to_family_class(cfg["font_family"]))
, font_size_(cfg["font_size"])
, font_style_(decode_font_style(cfg["font_style"]))
@ -713,6 +693,7 @@ text_shape::text_shape(const config& cfg)
void text_shape::draw(surface& canvas,
SDL_Renderer* /*renderer*/,
const SDL_Rect& view_bounds,
wfl::map_formula_callable& variables)
{
assert(variables.has_key("text"));
@ -746,6 +727,20 @@ void text_shape::draw(surface& canvas,
: PANGO_ELLIPSIZE_END)
.set_characters_per_line(characters_per_line_);
wfl::map_formula_callable local_variables(variables);
local_variables.add("text_width", wfl::variant(text_renderer.get_width()));
local_variables.add("text_height", wfl::variant(text_renderer.get_height()));
const auto rects = calculate_rects(view_bounds, local_variables);
if(rects.empty) {
DBG_GUI_D << "Text: Clipping to view_bounds resulted in an empty intersection, nothing to do.\n";
return;
}
// TODO: This creates a surface that's the full text_width x text_height, and then discards most of it by
// calling blit_surface(... , &rects.clip_in_shape, ..., ...). Should be improved with a change to pango_text,
// so that we can call text_renderer.render(rects.clip_in_shape) and get a smaller surface instead.
surface& surf = text_renderer.render();
if(surf->w == 0) {
DBG_GUI_D << "Text: Rendering '" << text
@ -753,45 +748,10 @@ void text_shape::draw(surface& canvas,
return;
}
wfl::map_formula_callable local_variables(variables);
local_variables.add("text_width", wfl::variant(surf->w));
local_variables.add("text_height", wfl::variant(surf->h));
/*
std::cerr << "Text: drawing text '" << text
<< " maximum width " << maximum_width_(variables)
<< " maximum height " << maximum_height_(variables)
<< " text width " << surf->w
<< " text height " << surf->h;
*/
// TODO: formulas are now recalculated every draw cycle which is a
// bit silly unless there has been a resize. So to optimize we should
// use an extra flag or do the calculation in a separate routine.
const unsigned x = x_(local_variables);
const unsigned y = y_(local_variables);
const unsigned w = w_(local_variables);
const unsigned h = h_(local_variables);
DBG_GUI_D << "Text: drawing text '" << text << "' drawn from " << x << ','
<< y << " width " << w << " height " << h << " canvas size "
<< canvas->w << ',' << canvas->h << ".\n";
VALIDATE(static_cast<int>(x) < canvas->w && static_cast<int>(y) < canvas->h,
_("Text doesn't start on canvas."));
// A text might be to long and will be clipped.
if(surf->w > static_cast<int>(w)) {
WRN_GUI_D << "Text: text is too wide for the "
"canvas and will be clipped.\n";
}
if(surf->h > static_cast<int>(h)) {
WRN_GUI_D << "Text: text is too high for the "
"canvas and will be clipped.\n";
}
SDL_Rect dst = sdl::create_rect(x, y, canvas->w, canvas->h);
blit_surface(surf, nullptr, canvas, &dst);
// Blit the clipped region - this needs non-const copies of the rects
auto clip_in_shape = rects.clip_in_shape;
auto dst_in_viewport = rects.dst_in_viewport;
blit_surface(surf, &clip_in_shape, canvas, &dst_in_viewport);
}
/***** ***** ***** ***** ***** CANVAS ***** ***** ***** ***** *****/
@ -801,7 +761,8 @@ canvas::canvas()
, blur_depth_(0)
, w_(0)
, h_(0)
, canvas_()
, viewport_()
, view_bounds_{0, 0, 0, 0}
, variables_()
, functions_()
, is_dirty_(true)
@ -813,17 +774,21 @@ canvas::canvas(canvas&& c) noexcept
, blur_depth_(c.blur_depth_)
, w_(c.w_)
, h_(c.h_)
, canvas_(std::move(c.canvas_))
, viewport_(std::move(c.viewport_))
, view_bounds_(std::move(c.view_bounds_))
, variables_(c.variables_)
, functions_(c.functions_)
, is_dirty_(c.is_dirty_)
{
}
void canvas::draw(const bool force)
void canvas::draw(const SDL_Rect& area_to_draw, bool force)
{
log_scope2(log_gui_draw, "Canvas: drawing.");
if(!is_dirty_ && !force) {
if(!viewport_ || !SDL_RectEquals(&view_bounds_, &area_to_draw)) {
DBG_GUI_D << "Canvas: redrawing because the cached view_bounds no longer fits.\n";
invalidate_cache();
} else if(!is_dirty_ && !force) {
DBG_GUI_D << "Canvas: nothing to draw.\n";
return;
}
@ -835,23 +800,25 @@ void canvas::draw(const bool force)
}
auto renderer = std::unique_ptr<SDL_Renderer, decltype(&SDL_DestroyRenderer)> {nullptr, &SDL_DestroyRenderer};
if(canvas_) {
if(viewport_ && view_bounds_.w == area_to_draw.w && view_bounds_.h == area_to_draw.h) {
DBG_GUI_D << "Canvas: use cached canvas.\n";
renderer.reset(SDL_CreateSoftwareRenderer(canvas_));
renderer.reset(SDL_CreateSoftwareRenderer(viewport_));
SDL_SetRenderDrawColor(renderer.get(), 0, 0, 0, 0);
SDL_RenderClear(renderer.get());
} else {
DBG_GUI_D << "Canvas: create new empty canvas.\n";
canvas_ = surface(w_, h_);
renderer.reset(SDL_CreateSoftwareRenderer(canvas_));
viewport_ = surface(area_to_draw.w, area_to_draw.h);
renderer.reset(SDL_CreateSoftwareRenderer(viewport_));
}
view_bounds_ = area_to_draw;
SDL_SetRenderDrawBlendMode(renderer.get(), SDL_BLENDMODE_BLEND);
// draw items
for(auto& shape : shapes_) {
lg::scope_logger inner_scope_logging_object__(log_gui_draw, "Canvas: draw shape.");
shape->draw(canvas_, renderer.get(), variables_);
shape->draw(viewport_, renderer.get(), view_bounds_, variables_);
}
SDL_RenderPresent(renderer.get());
@ -861,7 +828,38 @@ void canvas::draw(const bool force)
void canvas::blit(surface& surf, SDL_Rect rect)
{
draw();
// This early-return has to come before the `validate(rect.w <= w_)` check, as during the boost_unit_tests execution
// the debug_clock widget will have no shapes, 0x0 size, yet be given a larger rect to draw.
if(shapes_.empty()) {
DBG_GUI_D << "Canvas: empty (no shapes to draw).\n";
return;
}
VALIDATE(rect.w >= 0 && rect.h >= 0, _("Area to draw has negative size"));
VALIDATE(static_cast<unsigned>(rect.w) <= w_ && static_cast<unsigned>(rect.h) <= h_,
_("Area to draw is larger than widget size"));
// If the widget is partly off-screen, this might get called with
// surf width=1000, height=1000
// rect={-1, 2, 330, 440}
//
// From those, as the first column is off-screen:
// rect_clipped_to_parent={0, 2, 329, 440}
// area_to_draw={1, 0, 329, 440}
SDL_Rect parent {0, 0, surf->w, surf->h};
SDL_Rect rect_clipped_to_parent;
if(!SDL_IntersectRect(&rect, &parent, &rect_clipped_to_parent)) {
DBG_GUI_D << "Area to draw is completely outside parent.\n";
return;
}
SDL_Rect area_to_draw {
std::max(0, -rect.x),
std::max(0, -rect.y),
rect_clipped_to_parent.w,
rect_clipped_to_parent.h
};
draw(area_to_draw);
if(blur_depth_) {
/*
@ -880,7 +878,18 @@ void canvas::blit(surface& surf, SDL_Rect rect)
}
}
sdl_blit(canvas_, nullptr, surf, &rect);
// Currently draw(area_to_draw) will always allocate a viewport_ that exactly matches area_to_draw, which means that
// scrolling by a single pixel will force a complete redraw. I tested rendering a few of the off-screen lines too,
// however it didn't seem to be an optimisation - the dirty flag was already set on each such redraw, triggering a
// complete redraw.
//
// If you try to re-add this overdraw, the nullptr below will need to be replaced with
// {area_to_draw.x - view_bounds_.x, area_to_draw.y - view_bounds_.y, area_to_draw.w, area_to_draw.h};
assert(area_to_draw.x == view_bounds_.x);
assert(area_to_draw.y == view_bounds_.y);
assert(area_to_draw.w == view_bounds_.w);
assert(area_to_draw.h == view_bounds_.h);
sdl_blit(viewport_, nullptr, surf, &rect_clipped_to_parent);
}
void canvas::parse_cfg(const config& cfg)
@ -943,7 +952,7 @@ void canvas::clear_shapes(const bool force)
void canvas::invalidate_cache()
{
canvas_ = nullptr;
viewport_ = nullptr;
}
/***** ***** ***** ***** ***** SHAPE ***** ***** ***** ***** *****/

View file

@ -64,11 +64,14 @@ public:
* @param canvas The resulting image will be blitted upon this
* canvas.
* @param renderer The SDL_Renderer to use.
* @param view_bounds Part of the shape to render - this is the location of @a canvas
* within the coordinates of the shape.
* @param variables The canvas can have formulas in it's
* definition, this parameter contains the values
* for these formulas.
*/
virtual void draw(surface& canvas, SDL_Renderer* renderer,
const SDL_Rect& view_bounds,
wfl::map_formula_callable& variables) = 0;
bool immutable() const
@ -86,18 +89,23 @@ public:
canvas();
canvas(const canvas&) = delete;
canvas& operator=(const canvas&) = delete;
canvas(canvas&& c) noexcept;
private:
/**
* Draws the canvas.
* Internal part of the blit() function - prepares the contents of the internal viewport_
* surface, reallocating that surface if necessary.
*
* @param area_to_draw Currently-visible part of the widget, any area outside here won't be blitted to the parent.
* @param force If the canvas isn't dirty it isn't redrawn
* unless force is set to true.
*/
void draw(const bool force = false);
void draw(const SDL_Rect& area_to_draw, const bool force = false);
public:
/**
* Blits the canvas unto another surface.
* Draw the canvas' shapes onto another surface.
*
* It makes sure the image on the canvas is up to date. Also executes the
* pre-blitting functions.
@ -156,11 +164,6 @@ public:
return h_;
}
surface& surf()
{
return canvas_;
}
void set_variable(const std::string& key, const wfl::variant& value)
{
variables_.add(key, value);
@ -187,14 +190,21 @@ private:
*/
unsigned blur_depth_;
/** Width of the canvas. */
/** Width of the canvas (the full size, not limited to the view_bounds_). */
unsigned w_;
/** Height of the canvas. */
/** Height of the canvas (the full size, not limited to the view_bounds_). */
unsigned h_;
/** The surface we draw all items on. */
surface canvas_;
surface viewport_;
/**
* The placement and size of viewport_ in the coordinates of this widget; value is not useful when bool(viewport_) is false.
*
* For large widgets, a small viewport_ may be used that contains only the currently-visible part of the widget.
*/
SDL_Rect view_bounds_;
/** The variables of the canvas. */
wfl::map_formula_callable variables_;

View file

@ -76,9 +76,9 @@ public:
*/
explicit line_shape(const config& cfg);
/** Implement shape::draw(). */
void draw(surface& canvas,
SDL_Renderer* renderer,
const SDL_Rect& viewport,
wfl::map_formula_callable& variables) override;
private:
@ -103,9 +103,8 @@ private:
/**
* @ingroup GUICanvasWML
*
* Definition of a rectangle.
* When drawing a rectangle it doesn't get blended on the surface but replaces the pixels instead.
* A blitting flag might be added later if needed.
* Class holding common attribute names (for WML) and common implementation (in C++) for shapes
* placed with the 4 attributes x, y, w and h.
*
* Keys:
* Key |Type |Default|Description
@ -114,6 +113,65 @@ private:
* y | @ref guivartype_f_unsigned "f_unsigned"|0 |The y coordinate of the top left corner.
* w | @ref guivartype_f_unsigned "f_unsigned"|0 |The width of the rectangle.
* h | @ref guivartype_f_unsigned "f_unsigned"|0 |The height of the rectangle.
*/
class rect_bounded_shape : public canvas::shape {
protected:
/**
* Constructor.
*
* @param cfg The config object to define the rectangle.
*/
explicit rect_bounded_shape(const config& cfg);
/**
* Where to draw, calculated from the x,y,w,h formulas but with different reference points used
* as the origin of the co-ordinate system.
*/
struct calculated_rects {
/**
* True if there was no intersection between dst_on_widget and the viewport. If true, the
* data in the SDL_Rects must be ignored.
*/
bool empty;
/** In the co-ordinate system that the WML uses, and unaffected by the view_bounds */
SDL_Rect dst_on_widget;
/** Intersection of dst_on_widget with view_bounds, in the co-ordinate system that the WML uses. */
SDL_Rect clip_on_widget;
/**
* Intersection of view_bounds with the shape, in co-ordinates with the shape's top-left at
* (0,0). For a text_shape, this co-ordinate system corresponds to the co-ordinates of the
* Cairo surface that Pango draws on.
*/
SDL_Rect clip_in_shape;
/**
* Translation of dst_on_widget to the viewport's co-ordinates. Here (0,0) corresponds to
* view_bounds's top-left. Will often have negative values for x and y, and widths or
* heights larger than the viewport's size.
*/
SDL_Rect unclipped_around_viewport;
/** Where to draw, in the co-ordinates of the viewport, and restricted to the view_bounds. */
SDL_Rect dst_in_viewport;
};
calculated_rects calculate_rects(const SDL_Rect& view_bounds, wfl::map_formula_callable& variables) const;
private:
typed_formula<int> x_; /**< The x coordinate of the rectangle. */
typed_formula<int> y_; /**< The y coordinate of the rectangle. */
typed_formula<int> w_; /**< The width of the rectangle. */
typed_formula<int> h_; /**< The height of the rectangle. */
};
/**
* @ingroup GUICanvasWML
*
* Definition of a rectangle.
* When drawing a rectangle it doesn't get blended on the surface but replaces the pixels instead.
* A blitting flag might be added later if needed.
*
* Keys:
* Key |Type |Default|Description
* -------------------|----------------------------------------|-------|-----------
* border_thickness | @ref guivartype_unsigned "unsigned" |0 |The thickness of the border if the thickness is zero it's not drawn.
* border_color | @ref guivartype_color "color" |"" |The color of the border if empty it's not drawn.
* fill_color | @ref guivartype_color "color" |"" |The color of the interior if omitted it's not drawn.
@ -121,7 +179,7 @@ private:
*
* Variables: see line_shape
*/
class rectangle_shape : public canvas::shape {
class rectangle_shape : public rect_bounded_shape {
public:
/**
* Constructor.
@ -130,17 +188,12 @@ public:
*/
explicit rectangle_shape(const config& cfg);
/** Implement shape::draw(). */
void draw(surface& canvas,
SDL_Renderer* renderer,
const SDL_Rect& viewport,
wfl::map_formula_callable& variables) override;
private:
typed_formula<int> x_, /**< The x coordinate of the rectangle. */
y_, /**< The y coordinate of the rectangle. */
w_, /**< The width of the rectangle. */
h_; /**< The height of the rectangle. */
/**
* Border thickness.
*
@ -171,17 +224,13 @@ private:
* A blitting flag might be added later if needed.
* Key |Type |Default |Description
* ----------------|----------------------------------------|---------|-----------
* x | @ref guivartype_f_unsigned "f_unsigned"|0 |The x coordinate of the top left corner.
* y | @ref guivartype_f_unsigned "f_unsigned"|0 |The y coordinate of the top left corner.
* w | @ref guivartype_f_unsigned "f_unsigned"|0 |The width of the rounded rectangle.
* h | @ref guivartype_f_unsigned "f_unsigned"|0 |The height of the rounded rectangle.
* corner_radius | @ref guivartype_f_unsigned "f_unsigned"|0 |The radius of the rectangle's corners.
* border_thickness| @ref guivartype_unsigned "unsigned" |0 |The thickness of the border; if the thickness is zero it's not drawn.
* border_color | @ref guivartype_color "color" |"" |The color of the border; if empty it's not drawn.
* fill_color | @ref guivartype_color "color" |"" |The color of the interior; if omitted it's not drawn.
* debug | @ref guivartype_string "string" |"" |Debug message to show upon creation; this message is not stored.
*/
class round_rectangle_shape : public canvas::shape {
class round_rectangle_shape : public rect_bounded_shape {
public:
/**
* Constructor.
@ -190,17 +239,13 @@ public:
*/
explicit round_rectangle_shape(const config& cfg);
/** Implement shape::draw(). */
void draw(surface& canvas,
SDL_Renderer* renderer,
const SDL_Rect& viewport,
wfl::map_formula_callable& variables) override;
private:
typed_formula<int> x_, /**< The x coordinate of the rectangle. */
y_, /**< The y coordinate of the rectangle. */
w_, /**< The width of the rectangle. */
h_, /**< The height of the rectangle. */
r_; /**< The radius of the corners. */
typed_formula<int> r_; /**< The radius of the corners. */
/**
* Border thickness.
@ -253,9 +298,9 @@ public:
*/
explicit circle_shape(const config& cfg);
/** Implement shape::draw(). */
void draw(surface& canvas,
SDL_Renderer* renderer,
const SDL_Rect& viewport,
wfl::map_formula_callable& variables) override;
private:
@ -305,9 +350,9 @@ public:
*/
image_shape(const config& cfg, wfl::action_function_symbol_table& functions);
/** Implement shape::draw(). */
void draw(surface& canvas,
SDL_Renderer* renderer,
const SDL_Rect& viewport,
wfl::map_formula_callable& variables) override;
private:
@ -364,10 +409,6 @@ private:
*
* Key |Type |Default |Description
* -------------------|------------------------------------------|---------|-----------
* x | @ref guivartype_f_unsigned "f_unsigned" |0 |The x coordinate of the top left corner.
* y | @ref guivartype_f_unsigned "f_unsigned" |0 |The y coordinate of the top left corner.
* w | @ref guivartype_f_unsigned "f_unsigned" |0 |The width of the text's bounding rectangle.
* h | @ref guivartype_f_unsigned "f_unsigned" |0 |The height of the text's bounding rectangle.
* font_family | @ref guivartype_font_style "font_style" |"sans" |The font family used for the text.
* font_size | @ref guivartype_f_unsigned "f_unsigned" |mandatory|The size of the text font.
* font_style | @ref guivartype_f_unsigned "f_unsigned" |"" |The style of the text.
@ -391,7 +432,7 @@ private:
* text_height | @ref guivartype_unsigned "unsigned" |The height of the rendered text.
* Also the general variables are available, see line_shape
*/
class text_shape : public canvas::shape {
class text_shape : public rect_bounded_shape {
public:
/**
* Constructor.
@ -400,17 +441,12 @@ public:
*/
explicit text_shape(const config& cfg);
/** Implement shape::draw(). */
void draw(surface& canvas,
SDL_Renderer* renderer,
const SDL_Rect& viewport,
wfl::map_formula_callable& variables) override;
private:
typed_formula<unsigned> x_, /**< The x coordinate of the text. */
y_, /**< The y coordinate of the text. */
w_, /**< The width of the text. */
h_; /**< The height of the text. */
/** The text font family. */
font::family_class font_family_;