Merge lp:~widelands-dev/widelands/bug-1751620-floating-images into lp:widelands

Proposed by Notabilis
Status: Merged
Merged at revision: 8620
Proposed branch: lp:~widelands-dev/widelands/bug-1751620-floating-images
Merge into: lp:widelands
Diff against target: 431 lines (+174/-75)
3 files modified
data/scripting/richtext.lua (+2/-3)
src/graphic/text/rt_parse.cc (+4/-2)
src/graphic/text/rt_render.cc (+168/-70)
To merge this branch: bzr merge lp:~widelands-dev/widelands/bug-1751620-floating-images
Reviewer Review Type Date Requested Status
GunChleoc Approve
Review via email: mp+341286@code.launchpad.net

Commit message

Implementing text floating around images.

Description of the change

Implements a richtext attribute to allow text to flow around images.

To post a comment you must log in.
Revision history for this message
bunnybot (widelandsofficial) wrote :

Continuous integration builds have changed state:

Travis build 3288. State: passed. Details: https://travis-ci.org/widelands/widelands/builds/352028977.
Appveyor build 3096. State: success. Details: https://ci.appveyor.com/project/widelands-dev/widelands/build/_widelands_dev_widelands_bug_1751620_floating_images-3096.

Revision history for this message
GunChleoc (gunchleoc) wrote :

This is working great for float::left, thank you so much! Float::right doesn't do anything yet though. You can test by hacking li_image like this:

function li_image(imagepath, text)
   return
      div("width=100%",
         p(text) .. div("float=right padding_l=6", p(img(imagepath)))
      )
end

I leave it up to you if you want to do that in this branch or a follow-up branch.

review: Approve
Revision history for this message
GunChleoc (gunchleoc) wrote :

Never mind, the code below works :)

@bunnybot merge

      div("width=100%",
         div("float=right padding_r=6", p(img(imagepath))) ..
         p(text)
      )

Revision history for this message
Notabilis (notabilis27) wrote :

Thanks for the review and the documentation.

The problem is that the floating divs have to be in the "line" before the text, otherwise the layout class does not know about the required space. I guess it could be fixed, but would probably make two layout runs necessary. Simply documenting it should be fine in my opinion.

Revision history for this message
GunChleoc (gunchleoc) wrote :

Agreed. I have updated the documentation a bit in rt_parse, that should be sufficient for usage purpose. And what we lose in syntax flexibility here, we are gaining in speed :)

Revision history for this message
bunnybot (widelandsofficial) wrote :

Continuous integration builds have changed state:

Travis build 3292. State: passed. Details: https://travis-ci.org/widelands/widelands/builds/353681300.
Appveyor build 3100. State: failed. Details: https://ci.appveyor.com/project/widelands-dev/widelands/build/_widelands_dev_widelands_bug_1751620_floating_images-3100.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'data/scripting/richtext.lua'
2--- data/scripting/richtext.lua 2018-02-24 15:31:26 +0000
3+++ data/scripting/richtext.lua 2018-03-15 05:52:38 +0000
4@@ -419,9 +419,8 @@
5 function li_image(imagepath, text)
6 return
7 div("width=100%",
8- div(p(vspace(6) .. img(imagepath) .. space(6))) ..
9- div(p(space(6))) ..
10- div("width=*", p(vspace(6) .. text .. vspace(12)))
11+ div("float=left padding_r=6", p(img(imagepath))) ..
12+ p(text)
13 )
14 end
15
16
17=== modified file 'src/graphic/text/rt_parse.cc'
18--- src/graphic/text/rt_parse.cc 2018-02-25 11:51:29 +0000
19+++ src/graphic/text/rt_parse.cc 2018-03-15 05:52:38 +0000
20@@ -261,8 +261,10 @@
21
22 The same attributes as :ref:`rt_tags_rt`, plus the following:
23
24-* **margin**: Shrink all contents to leave a margin towards the outer edge of this tag's rectangle
25-* **float**: To be implemented. Allowed values are ``left``, ``right``
26+* **margin**: Shrink all contents to leave a margin towards the outer edge of this tag's rectangle.
27+* **float**: Make text float around this div. Allowed values are ``left``, ``right``.
28+ The structure has to be something like: ``div("width=100%", div("float=left padding_r=6", p(img(imagepath))) .. p(text))``,
29+ with the first embedded div being the floating one.
30 * **valign**: Align the contents vertically. Allowed values are ``top`` (default), ``center`` or ``middle``, ``bottom``.
31 * **width**: The width of this element, as a pixel amount, or as a percentage.
32 The last ``div`` in a row can be expanded automatically by using ``*``.
33
34=== modified file 'src/graphic/text/rt_render.cc'
35--- src/graphic/text/rt_render.cc 2018-02-25 18:20:58 +0000
36+++ src/graphic/text/rt_render.cc 2018-03-15 05:52:38 +0000
37@@ -198,13 +198,13 @@
38
39 class RenderNode {
40 public:
41- enum Floating {
42- NO_FLOAT = 0,
43- FLOAT_RIGHT,
44- FLOAT_LEFT,
45+ enum class Floating {
46+ kNone,
47+ kRight,
48+ kLeft,
49 };
50 explicit RenderNode(NodeStyle& ns)
51- : floating_(NO_FLOAT), halign_(ns.halign), valign_(ns.valign), x_(0), y_(0) {
52+ : floating_(Floating::kNone), halign_(ns.halign), valign_(ns.valign), x_(0), y_(0) {
53 }
54 virtual ~RenderNode() {
55 }
56@@ -292,6 +292,9 @@
57 int32_t x_, y_;
58 };
59
60+/*
61+ * Class to calculate positions of nodes within a div tag.
62+ */
63 class Layout {
64 public:
65 explicit Layout(std::vector<std::shared_ptr<RenderNode>>& all)
66@@ -307,19 +310,7 @@
67 fit_nodes(std::vector<std::shared_ptr<RenderNode>>* rv, uint16_t w, Borders p, bool trim_spaces);
68
69 private:
70- // Represents a change in the rendering constraints. For example when an
71- // Image is inserted, the width will become wider after it. This is a
72- // constraint change.
73- struct ConstraintChange {
74- int at_y;
75- int32_t delta_w;
76- int32_t delta_offset_x;
77-
78- bool operator<(const ConstraintChange& o) const {
79- return at_y > o.at_y || (at_y == o.at_y && delta_w > o.delta_w);
80- }
81- };
82-
83+ bool calculate_line_width(uint16_t *x, uint16_t *w, uint16_t lineheight);
84 uint16_t fit_line(uint16_t w,
85 const Borders&,
86 std::vector<std::shared_ptr<RenderNode>>* rv,
87@@ -328,13 +319,55 @@
88 uint16_t h_;
89 size_t idx_;
90 std::vector<std::shared_ptr<RenderNode>>& all_nodes_;
91- std::priority_queue<ConstraintChange> constraint_changes_;
92+ std::queue<std::shared_ptr<RenderNode>> floats_;
93 };
94
95-uint16_t Layout::fit_line(uint16_t w,
96- const Borders& p,
97- std::vector<std::shared_ptr<RenderNode>>* rv,
98- bool trim_spaces) {
99+/**
100+ * Calculate the width of a line at h_ taking into account floating elements.
101+ * @param x The first useable x position in the line. Has to be set by the caller,
102+ * might be modified inside this function.
103+ * @param w The useable width of the line. Has to be set by the caller,
104+ * might be modified inside this function.
105+ * @param lineheight The height of the line the maximum width should be calculated of.
106+ * @return Whether less than the full width can be used (i.e., there is a float).
107+ */
108+bool Layout::calculate_line_width(uint16_t *x, uint16_t *w, uint16_t lineheight) {
109+ // Drop elements we already passed
110+ while (!floats_.empty() && h_ >= floats_.front()->y() + floats_.front()->height()) {
111+ floats_.pop();
112+ }
113+ if (floats_.empty()) {
114+ return false;
115+ }
116+ // Check whether there is an element at the current height
117+ std::shared_ptr<RenderNode> n = floats_.front();
118+ if (h_ + lineheight < n->y()) {
119+ // Nope, nothing in the current line
120+ // Since the elements are ordered, no further element can match
121+ return false;
122+ }
123+
124+ if (n->get_floating() == RenderNode::Floating::kLeft) {
125+ *x += n->width();
126+ } else {
127+ assert(n->get_floating() == RenderNode::Floating::kRight);
128+ *w -= n->width();
129+ }
130+ return true;
131+}
132+
133+/*
134+ * Calculate x positions of nodes in one line and return them in rv.
135+ * As many nodes of all_nodes_ are added to the line as there is space.
136+ * Remove leading/trailing spaces and assign x positions to all elements.
137+ * Use remaining space to distribute expanding elements a bit further.
138+ * Returns hotspot of the line.
139+ * Method is called from within Layout::fit_nodes().
140+ */
141+uint16_t Layout::fit_line(const uint16_t w_max, // Maximum width of line
142+ const Borders& p, // Left/right border. Is left empty
143+ std::vector<std::shared_ptr<RenderNode>>* rv, // Output: Nodes to render
144+ bool trim_spaces) { // Whether leading/trailing space should be removed
145 assert(rv->empty());
146
147 // Remove leading spaces
148@@ -342,17 +375,79 @@
149 all_nodes_[idx_++].reset();
150 }
151
152- uint16_t x = p.left;
153+ uint16_t w;
154+ uint16_t x;
155 std::size_t first_idx = idx_;
156+ uint16_t lineheight = 0;
157+
158+ // Pass 1: Run through all nodes who *might* end up in this line and check for floats
159+ w = w_max - p.right;
160+ x = p.left;
161+ bool width_was_reduced = calculate_line_width(&x, &w, lineheight);
162+ lineheight = 0;
163+ uint16_t w_used = 0;
164+ for (; idx_ < all_nodes_.size(); ++idx_) {
165+ if (w_used + all_nodes_[idx_]->width() > w) {
166+ // Line is full
167+ break;
168+ }
169+ if (all_nodes_[idx_]->get_floating() == RenderNode::Floating::kNone) {
170+ // Normal, non-floating node
171+ w_used += all_nodes_[idx_]->width();
172+ assert(all_nodes_[idx_]->height() >= all_nodes_[idx_]->hotspot_y());
173+ lineheight = std::max(lineheight, all_nodes_[idx_]->height());
174+ continue;
175+ }
176+ // Found a float. Add it to list
177+ // New float start directly below lowest flow in list
178+ if (!floats_.empty()) {
179+ all_nodes_[idx_]->set_y(floats_.back()->y() + floats_.back()->height());
180+ } else {
181+ all_nodes_[idx_]->set_y(h_);
182+ }
183+ // Set x position of the float based on its desired orientation
184+ if (all_nodes_[idx_]->get_floating() == RenderNode::Floating::kLeft) {
185+ all_nodes_[idx_]->set_x(p.left);
186+ } else {
187+ assert(all_nodes_[idx_]->get_floating() == RenderNode::Floating::kRight);
188+ all_nodes_[idx_]->set_x(w_max - all_nodes_[idx_]->width() - p.right);
189+ }
190+ floats_.push(all_nodes_[idx_]);
191+ // When the line width hasn't been reduced by a float yet, do so now.
192+ // If it already has been reduced than the new float will be placed somewhere below
193+ // the current line so no need to adapt the line width
194+ if (!width_was_reduced) {
195+ // Don't need to reset x and w since they haven't been modified on last call
196+ width_was_reduced = calculate_line_width(&x, &w, lineheight);
197+ assert(width_was_reduced);
198+ }
199+ }
200+
201+ idx_ = first_idx;
202+ // w and x now contain the width of the line and the x position of the first element in it
203
204 // Calc fitting nodes
205 while (idx_ < all_nodes_.size()) {
206 std::shared_ptr<RenderNode> n = all_nodes_[idx_];
207+ if (n->get_floating() != RenderNode::Floating::kNone) {
208+ // Don't handle floaters here
209+ rv->push_back(n);
210+ if (idx_ == first_idx) {
211+ first_idx++;
212+ }
213+ ++idx_;
214+ continue;
215+ }
216 uint16_t nw = n->width();
217- if (x + nw + p.right > w || n->get_floating() != RenderNode::NO_FLOAT) {
218+ // Check whether the element is too big for the current line
219+ // (position + width-of-element + border) > width-of-line
220+ if (x + nw + p.right > w) {
221+ // Its too big
222 if (idx_ == first_idx) {
223+ // If it is the first element in the line, add it anyway and pretend that it matches exactly
224 nw = w - p.right - x;
225 } else {
226+ // Too wide and not the first element: We are done with the line
227 break;
228 }
229 }
230@@ -401,9 +496,12 @@
231 }
232 }
233
234- // Find the biggest hotspot of the truly remaining items.
235+ // Find the biggest hotspot of the truly remaining non-floating items.
236 uint16_t cur_line_hotspot = 0;
237 for (std::shared_ptr<RenderNode> node : *rv) {
238+ if (node->get_floating() != RenderNode::Floating::kNone) {
239+ continue;
240+ }
241 cur_line_hotspot = std::max(cur_line_hotspot, node->hotspot_y());
242 }
243 return cur_line_hotspot;
244@@ -412,6 +510,9 @@
245 /*
246 * Take ownership of all nodes, delete those that we do not render anyways (for
247 * example unneeded spaces), append the rest to the vector we got.
248+ * Also, calculate positions for all nodes based on the given width w
249+ * and the widths of the nodes.
250+ * Method is called from within DivTagHandler::enter().
251 */
252 uint16_t Layout::fit_nodes(std::vector<std::shared_ptr<RenderNode>>* rv,
253 uint16_t w,
254@@ -428,10 +529,12 @@
255
256 int line_height = 0;
257 int line_start = INFINITE_WIDTH;
258- // Compute real line height and width, taking into account alignement
259+ // Compute real line height and width, taking into account alignment
260 for (std::shared_ptr<RenderNode> n : nodes_in_line) {
261- line_height = std::max(line_height, biggest_hotspot - n->hotspot_y() + n->height());
262- n->set_y(h_ + biggest_hotspot - n->hotspot_y());
263+ if (n->get_floating() == RenderNode::Floating::kNone) {
264+ line_height = std::max(line_height, biggest_hotspot - n->hotspot_y() + n->height());
265+ n->set_y(h_ + biggest_hotspot - n->hotspot_y());
266+ }
267 if (line_start >= INFINITE_WIDTH || n->x() < line_start) {
268 line_start = n->x() - p.left;
269 }
270@@ -440,7 +543,7 @@
271
272 // Go over again and adjust position for VALIGN
273 for (std::shared_ptr<RenderNode> n : nodes_in_line) {
274- uint16_t space = line_height - n->height();
275+ int space = line_height - n->height();
276 if (!space || n->valign() == UI::Align::kBottom) {
277 continue;
278 }
279@@ -450,43 +553,25 @@
280 // Space can become negative, for example when we have mixed fontsets on the same line
281 // (e.g. "default" and "arabic"), due to differing font heights and hotspots.
282 // So, we fix the sign.
283- n->set_y(std::abs(n->y() - space));
284+ if (n->get_floating() == RenderNode::Floating::kNone) {
285+ n->set_y(std::abs(n->y() - space));
286+ }
287 }
288 rv->insert(rv->end(), nodes_in_line.begin(), nodes_in_line.end());
289
290 h_ += line_height;
291- while (!constraint_changes_.empty() && constraint_changes_.top().at_y <= h_) {
292- const ConstraintChange& top = constraint_changes_.top();
293- w += top.delta_w;
294- p.left += top.delta_offset_x;
295- constraint_changes_.pop();
296- }
297
298- if ((idx_ < all_nodes_.size()) && all_nodes_[idx_]->get_floating()) {
299- std::shared_ptr<RenderNode> n = all_nodes_[idx_];
300- n->set_y(h_);
301- ConstraintChange cc = {h_ + n->height(), 0, 0};
302- if (n->get_floating() == RenderNode::FLOAT_LEFT) {
303- n->set_x(p.left);
304- p.left += n->width();
305- cc.delta_offset_x = -n->width();
306- max_line_width = std::max<int>(max_line_width, n->x() + n->width() + p.right);
307- } else {
308- n->set_x(w - n->width() - p.right);
309- w -= n->width();
310- cc.delta_w = n->width();
311- max_line_width = std::max(max_line_width, w);
312- }
313- constraint_changes_.push(cc);
314- rv->push_back(n);
315- ++idx_;
316- }
317 if (idx_ == idx_before_iteration_) {
318 throw WidthTooSmall(
319 "Could not fit a single render node in line. Width of an Element is too small!");
320 }
321 }
322
323+ if (!floats_.empty()) {
324+ // If there is a float left this means the floats go down further than the text.
325+ // If this is the case, reset the height of the div
326+ h_ = std::max<uint16_t>(h_, floats_.back()->y() + floats_.back()->height());
327+ }
328 h_ += p.bottom;
329 return max_line_width;
330 }
331@@ -800,6 +885,7 @@
332
333 // Draw Solid background Color
334 if (is_background_color_set_) {
335+ // TODO(Notabilis): I think margin_.right and .bottom are missing in next line
336 UI::RenderedRect* bg_rect =
337 new UI::RenderedRect(Recti(margin_.left, margin_.top, w_, h_), background_color_);
338 // Size is automatically adjusted in RenderedText while blitting, so no need to call
339@@ -1336,6 +1422,13 @@
340 render_node_(new DivTagRenderNode(ns)) {
341 }
342
343+ /*
344+ * Calculate width of all children of this div.
345+ * Width is either fixed, a percentage of the parent or fill. If the width should fill
346+ * the line, it is set to the remaining width of the current line. New lines aren't really
347+ * started here but percent/filling elements are made so big that they won't fit into the
348+ * previous line when their final position is calculated in Layout::fit_nodes().
349+ */
350 void enter() override {
351 Borders padding, margin;
352
353@@ -1368,26 +1461,24 @@
354 }
355
356 std::vector<std::shared_ptr<RenderNode>> subnodes;
357- // If a percentage width is used, temporarily set it as the overall width. This way, divs with
358- // width "fill" only use the width of their parent node. Also, percentages given in child
359- // nodes
360- // are relative to the width of their parent node.
361- const uint16_t old_overall_width = renderer_style_.overall_width;
362+ // If a percentage width is used, temporarily set it as the overall width. This way,
363+ // divs with width "fill" only use the width of their parent node. Also, percentages
364+ // given in child nodes are relative to the width of their parent node.
365+ const uint16_t old_line_width = renderer_style_.overall_width;
366 if (render_node_->desired_width().unit == WidthUnit::kPercent) {
367- renderer_style_.overall_width =
368- render_node_->desired_width().width * renderer_style_.overall_width / 100;
369+ renderer_style_.overall_width = render_node_->desired_width().width * renderer_style_.overall_width / 100;
370 }
371 TagHandler::emit_nodes(subnodes);
372- renderer_style_.overall_width = old_overall_width;
373+ renderer_style_.overall_width = old_line_width;
374
375- // Determine the width by the width of the widest subnode
376+ // Determine the required width by the width of the widest subnode
377 uint16_t width_first_subnode = INFINITE_WIDTH;
378 uint16_t widest_subnode = 0;
379 for (std::shared_ptr<RenderNode> n : subnodes) {
380 if (n->width() >= INFINITE_WIDTH) {
381 continue;
382 }
383- if (width_first_subnode >= INFINITE_WIDTH && n->width() > 0) {
384+ if (width_first_subnode >= INFINITE_WIDTH && n->width()) {
385 width_first_subnode = n->width() + padding.left + padding.right;
386 }
387 widest_subnode = std::max<int>(widest_subnode, n->width() + padding.left + padding.right);
388@@ -1401,6 +1492,9 @@
389 case WidthUnit::kPercent:
390 w_ = render_node_->desired_width().width * renderer_style_.overall_width / 100;
391
392+ if (render_node_->get_floating() != RenderNode::Floating::kNone) {
393+ break;
394+ }
395 // Reduce remaining width
396 if (renderer_style_.remaining_width <= w_) {
397 // Not enough space. Div will be placed in the next line, calculate the remaining space
398@@ -1412,7 +1506,9 @@
399 break;
400 case WidthUnit::kFill:
401 w_ = renderer_style_.remaining_width;
402- renderer_style_.remaining_width = 0;
403+ if (render_node_->get_floating() == RenderNode::Floating::kNone) {
404+ renderer_style_.remaining_width = 0;
405+ }
406 break;
407 default:
408 if (!w_) {
409@@ -1430,7 +1526,9 @@
410 extra_width = w_ - max_line_width;
411 } else if (render_node_->desired_width().unit == WidthUnit::kShrink) {
412 w_ = max_line_width;
413- renderer_style_.remaining_width -= w_;
414+ if (render_node_->get_floating() == RenderNode::Floating::kNone) {
415+ renderer_style_.remaining_width -= w_;
416+ }
417 }
418
419 // Collect all tags from children
420@@ -1494,9 +1592,9 @@
421 if (a.has("float")) {
422 const std::string s = a["float"].get_string();
423 if (s == "right")
424- render_node_->set_floating(RenderNode::FLOAT_RIGHT);
425+ render_node_->set_floating(RenderNode::Floating::kRight);
426 else if (s == "left")
427- render_node_->set_floating(RenderNode::FLOAT_LEFT);
428+ render_node_->set_floating(RenderNode::Floating::kLeft);
429 }
430 if (a.has("valign")) {
431 const std::string align = a["valign"].get_string();

Subscribers

People subscribed via source and target branches

to status/vote changes: