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:
parent
d7234242d4
commit
b2d39d2754
4 changed files with 278 additions and 222 deletions
|
@ -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.
|
||||
|
|
|
@ -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 ***** ***** ***** ***** *****/
|
||||
|
|
|
@ -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_;
|
||||
|
|
|
@ -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_;
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue