The Battle for Wesnoth  1.19.0-dev
story_viewer.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2017 - 2024
3  by Charles Dang <exodia339@gmail.com>
4  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5 
6  This program is free software; you can redistribute it and/or modify
7  it under the terms of the GNU General Public License as published by
8  the Free Software Foundation; either version 2 of the License, or
9  (at your option) any later version.
10  This program is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY.
12 
13  See the COPYING file for more details.
14 */
15 
16 #define GETTEXT_DOMAIN "wesnoth-lib"
17 
19 
20 #include "display.hpp"
21 #include "formula/variant.hpp"
23 #include "gui/core/timer.hpp"
24 #include "gui/widgets/button.hpp"
25 #include "gui/widgets/label.hpp"
28 #include "gui/widgets/window.hpp"
29 #include "sound.hpp"
30 #include "variable.hpp"
31 
32 namespace gui2::dialogs
33 {
34 
35 // Helper function to get the canvas shape data for the shading under the title area until
36 // I can figure out how to ensure it always stays on top of the canvas stack.
38 {
39  static config cfg;
40  cfg["x"] = 0;
41  cfg["y"] = 0;
42  cfg["w"] = "(screen_width)";
43  cfg["h"] = "(image_original_height * 2)";
44  cfg["name"] = "dialogs/story_title_decor.png~O(75%)";
45 
46  return cfg;
47 }
48 
49 // Stacked widget layer constants for the text stack.
50 static const unsigned int LAYER_BACKGROUND = 1;
51 static const unsigned int LAYER_TEXT = 2;
52 
54 
55 story_viewer::story_viewer(const std::string& scenario_name, const config& cfg_parsed)
56  : modal_dialog(window_id())
57  , controller_(vconfig(cfg_parsed, true), scenario_name)
58  , part_index_(0)
59  , current_part_(nullptr)
60  , timer_id_(0)
61  , next_draw_(0)
62  , fade_step_(0)
63  , fade_state_(NOT_FADING)
64 {
65  update_current_part_ptr();
66 }
67 
69 {
70  if(timer_id_ != 0) {
72  timer_id_ = 0;
73  }
74 }
75 
77 {
79 }
80 
82 {
84 
85  // Special callback handle key presses
86  connect_signal_pre_key_press(window, std::bind(&story_viewer::key_press_callback, this, std::placeholders::_5));
87 
88  connect_signal_mouse_left_click(find_widget<button>(&window, "next", false),
90 
91  connect_signal_mouse_left_click(find_widget<button>(&window, "back", false),
93 
94  // Tell the game display not to draw
97 
98  display_part();
99 }
100 
102 {
103  // Bring the game display back again, if appropriate
105 }
106 
108 {
110 }
111 
113 {
114  static const int VOICE_SOUND_SOURCE_ID = 255;
115  // Update Back button state. Doing this here so it gets called in pre_show too.
116  find_widget<button>(get_window(), "back", false).set_active(part_index_ != 0);
117 
118  //
119  // Music and sound
120  //
121  if(!current_part_->music().empty()) {
122  config music_config;
123  music_config["name"] = current_part_->music();
124  music_config["ms_after"] = 2000;
125  music_config["immediate"] = true;
126 
127  sound::play_music_config(music_config);
128  }
129 
130  if(!current_part_->sound().empty()) {
132  }
133 
134  sound::stop_sound(VOICE_SOUND_SOURCE_ID);
135  if(!current_part_->voice().empty()) {
136  sound::play_sound_positioned(current_part_->voice(), VOICE_SOUND_SOURCE_ID, 0, 0);
137  }
138 
139  config cfg, image;
140 
141  //
142  // Background images
143  //
144  bool has_background = false;
145  config* base_layer = nullptr;
146 
147  for(const auto& layer : current_part_->get_background_layers()) {
148  has_background |= !layer.file().empty();
149 
150  const bool preserve_ratio = layer.keep_aspect_ratio();
151  const bool tile_h = layer.tile_horizontally();
152  const bool tile_v = layer.tile_vertically();
153 
154  // By default, no scaling will be applied.
155  std::string width_formula = "(image_original_width)";
156  std::string height_formula = "(image_original_height)";
157 
158  // Background layers are almost always centered. In case of tiling, we want the full
159  // area in the horizontal or vertical direction, so set the origin to 0 for that axis.
160  // The resize mode will center the original image in the available area first/
161  std::string x_formula;
162  std::string y_formula;
163 
164  if(tile_h) {
165  x_formula = "0";
166  } else {
167  x_formula = "(max(pos, 0) where pos = (width / 2 - image_width / 2))";
168  }
169 
170  if(tile_v) {
171  y_formula = "0";
172  } else {
173  y_formula = "(max(pos, 0) where pos = (height / 2 - image_height / 2))";
174  }
175 
176  if(layer.scale_horizontally() && preserve_ratio) {
177  height_formula = "(min((image_original_height * width / image_original_width), height))";
178  } else if(layer.scale_vertically() || tile_v) {
179  height_formula = "(height)";
180  }
181 
182  if(layer.scale_vertically() && preserve_ratio) {
183  width_formula = "(min((image_original_width * height / image_original_height), width))";
184  } else if(layer.scale_horizontally() || tile_h) {
185  width_formula = "(width)";
186  }
187 
188  image["x"] = x_formula;
189  image["y"] = y_formula;
190  image["w"] = width_formula;
191  image["h"] = height_formula;
192  image["name"] = layer.file();
193  image["resize_mode"] = (tile_h || tile_v) ? "tile_center" : "scale";
194 
195  config& layer_image = cfg.add_child("image", image);
196 
197  if(base_layer == nullptr || layer.is_base_layer()) {
198  base_layer = &layer_image;
199  }
200  }
201 
202  canvas& window_canvas = get_window()->get_canvas(0);
203 
204  /* In order to avoid manually loading the image and calculating the scaling factor, we instead
205  * delegate the task of setting the necessary variables to the canvas once the calculations
206  * have been made internally.
207  *
208  * This sets the necessary values with the data for "this" image when its drawn. If no base
209  * layer was found (which would be the case if no backgrounds were provided at all), simply set
210  * some sane defaults directly.
211  */
212  if(base_layer != nullptr) {
213  (*base_layer)["actions"] = R"((
214  [
215  set_var('base_scale_x', as_decimal(image_width) / as_decimal(image_original_width)),
216  set_var('base_scale_y', as_decimal(image_height) / as_decimal(image_original_height)),
217  set_var('base_origin_x', clip_x),
218  set_var('base_origin_y', clip_y)
219  ]
220  ))";
221  } else {
222  window_canvas.set_variable("base_scale_x", wfl::variant(1));
223  window_canvas.set_variable("base_scale_y", wfl::variant(1));
224  window_canvas.set_variable("base_origin_x", wfl::variant(0));
225  window_canvas.set_variable("base_origin_y", wfl::variant(0));
226  }
227 
228  cfg.add_child("image", get_title_area_decor_config());
229 
230  window_canvas.set_cfg(cfg);
231 
232  // Needed to make the background redraw correctly.
233  window_canvas.update_size_variables();
235 
236  //
237  // Title
238  //
239  label& title_label = find_widget<label>(get_window(), "title", false);
240 
241  std::string title_text = current_part_->title();
242  bool showing_title;
243 
244  if(current_part_->show_title() && !title_text.empty()) {
245  showing_title = true;
246 
247  PangoAlignment title_text_alignment = decode_text_alignment(current_part_->title_text_alignment());
248 
250  title_label.set_text_alignment(title_text_alignment);
251  title_label.set_label(title_text);
252  } else {
253  showing_title = false;
254 
256  }
257 
258  //
259  // Story text
260  //
261  stacked_widget& text_stack = find_widget<stacked_widget>(get_window(), "text_and_control_stack", false);
262 
263  std::string new_panel_mode;
264 
265  switch(current_part_->story_text_location()) {
267  new_panel_mode = "top";
268  break;
270  new_panel_mode = "center";
271  break;
273  new_panel_mode = "bottom";
274  break;
275  }
276 
277  text_stack.set_vertical_alignment(new_panel_mode);
278 
279  /* Set the panel mode control variables.
280  *
281  * We use get_layer_grid here to ensure the widget is always found regardless of
282  * whether the background is visible or not.
283  */
284  canvas& panel_canvas = find_widget<panel>(text_stack.get_layer_grid(LAYER_BACKGROUND), "text_panel", false).get_canvas(0);
285 
286  panel_canvas.set_variable("panel_position", wfl::variant(new_panel_mode));
287  panel_canvas.set_variable("title_present", wfl::variant(static_cast<int>(showing_title))); // cast to 0/1
288 
289  const std::string& part_text = current_part_->text();
290 
291  if(part_text.empty() || !has_background) {
292  // No text or no background for this part, hide the background layer.
293  text_stack.select_layer(LAYER_TEXT);
294  } else if(text_stack.current_layer() != -1) {
295  // If the background layer was previously hidden, re-show it.
296  text_stack.select_layer(-1);
297  }
298 
299  // Convert the story part text alignment types into the Pango equivalents
300  PangoAlignment story_text_alignment = decode_text_alignment(current_part_->story_text_alignment());
301 
302  scroll_label& text_label = find_widget<scroll_label>(get_window(), "part_text", false);
303 
304  text_label.set_text_alignment(story_text_alignment);
305  text_label.set_text_alpha(0);
306  text_label.set_label(part_text);
307 
308  // Regenerate any background blur texture
309  panel_canvas.queue_reblur();
310 
311  begin_fade_draw(true);
312  // if the previous page was skipped, it is possible that we already have a timer running.
314  //
315  // Floating images (handle this last)
316  //
317  const auto& floating_images = current_part_->get_floating_images();
318 
319  // If we have images to draw, draw the first one now. A new non-repeating timer is added
320  // after every draw to schedule the next one after the specified interval.
321  //
322  // TODO: in the old GUI1 dialog, floating images delayed the appearance of the story panel until
323  // drawing was finished. Might be worth looking into restoring that.
324  if(!floating_images.empty()) {
325  draw_floating_image(floating_images.begin(), part_index_);
326  }
327 }
328 
329 void story_viewer::draw_floating_image(floating_image_list::const_iterator image_iter, int this_part_index)
330 {
331  const auto& images = current_part_->get_floating_images();
332  canvas& window_canvas = get_window()->get_canvas(0);
333 
334  // If the current part has changed or we're out of images to draw, exit the draw loop.
335  while((this_part_index == part_index_) && (image_iter != images.end())) {
336  const auto& floating_image = *image_iter;
337  ++image_iter;
338 
339  std::ostringstream x_ss;
340  std::ostringstream y_ss;
341 
342  // Floating images' locations are scaled by the same factor as the background.
343  x_ss << "(trunc(" << floating_image.ref_x() << " * base_scale_x) + base_origin_x";
344  y_ss << "(trunc(" << floating_image.ref_y() << " * base_scale_y) + base_origin_y";
345 
346  if(floating_image.centered()) {
347  x_ss << " - (image_width / 2)";
348  y_ss << " - (image_height / 2)";
349  }
350 
351  x_ss << ")";
352  y_ss << ")";
353 
354  config image;
355  image["x"] = x_ss.str();
356  image["y"] = y_ss.str();
357 
358  // Width and height don't need to be set unless the image needs to be scaled.
359  if(floating_image.resize_with_background()) {
360  image["w"] = "(image_original_width * base_scale_x)";
361  image["h"] = "(image_original_height * base_scale_y)";
362  }
363 
364  image["name"] = floating_image.file();
365  config cfg{"image", std::move(image)};
366 
367  cfg.add_child("image", std::move(image));
368  window_canvas.append_cfg(std::move(cfg));
369 
370  // Needed to make the background redraw correctly.
371  window_canvas.update_size_variables();
373 
374  // If a delay is specified, schedule the next image draw and break out of the loop.
375  const unsigned int draw_delay = floating_image.display_delay();
376  if(draw_delay != 0) {
377  // This must be a non-repeating timer
378  timer_id_ = add_timer(draw_delay, std::bind(&story_viewer::draw_floating_image, this, image_iter, this_part_index), false);
379  return;
380  }
381  }
382 
383  timer_id_ = 0;
384 }
385 
387 {
388  // If a button is pressed while fading in, abort and set alpha to full opaque.
389  if(fade_state_ == FADING_IN) {
390  halt_fade_draw();
391 
392  // Only set full alpha if Forward was pressed.
393  if(direction == DIR_FORWARD) {
394  find_widget<scroll_label>(get_window(), "part_text", false).set_text_alpha(ALPHA_OPAQUE);
396  return;
397  }
398  }
399 
400  // If a button is pressed while fading out, skip and show next part.
401  if(fade_state_ == FADING_OUT) {
402  display_part();
403  return;
404  }
405 
406  assert(fade_state_ == NOT_FADING);
407 
408  part_index_ = (direction == DIR_FORWARD ? part_index_ + 1 : part_index_ -1);
409 
410  // If we've viewed all the parts, close the dialog.
412  get_window()->close();
413  return;
414  }
415 
416  if(part_index_ < 0) {
417  part_index_ = 0;
418  }
419 
421 
422  begin_fade_draw(false);
423 }
424 
425 void story_viewer::key_press_callback(const SDL_Keycode key)
426 {
427  const bool next_keydown =
428  key == SDLK_SPACE
429  || key == SDLK_RETURN
430  || key == SDLK_KP_ENTER
431  || key == SDLK_RIGHT;
432 
433  const bool back_keydown =
434  key == SDLK_BACKSPACE
435  || key == SDLK_LEFT;
436 
437  if(next_keydown) {
439  } else if(back_keydown) {
441  }
442 }
443 
445 {
446  next_draw_ = SDL_GetTicks() + 20;
447 }
448 
450 {
451  set_next_draw();
452 
453  fade_step_ = fade_in ? 0 : 10;
454  fade_state_ = fade_in ? FADING_IN : FADING_OUT;
455 }
456 
458 {
459  next_draw_ = 0;
460  fade_step_ = -1;
462 }
463 
465 {
467 
468  if(next_draw_ && SDL_GetTicks() < next_draw_) {
469  return;
470  }
471 
472  if(fade_state_ == NOT_FADING) {
473  return;
474  }
475 
476  // If we've faded fully in...
477  if(fade_state_ == FADING_IN && fade_step_ > 10) {
478  halt_fade_draw();
479  return;
480  }
481 
482  // If we've faded fully out...
483  if(fade_state_ == FADING_OUT && fade_step_ < 0) {
484  halt_fade_draw();
485 
486  display_part();
487  return;
488  }
489 
490  unsigned short new_alpha = std::clamp<short>(fade_step_ * 25.5, 0, ALPHA_OPAQUE);
491  find_widget<scroll_label>(get_window(), "part_text", false).set_text_alpha(new_alpha);
492 
493  // The text stack also needs to be marked dirty so the background panel redraws correctly.
495 
496  if(fade_state_ == FADING_IN) {
497  fade_step_ ++;
498  } else if(fade_state_ == FADING_OUT) {
499  fade_step_ --;
500  }
501 
502  set_next_draw();
503 }
504 
506 {
507  find_widget<stacked_widget>(get_window(), "text_and_control_stack", false).queue_redraw();
508 }
509 
510 } // namespace dialogs
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:159
bool empty() const
Definition: config.cpp:852
config & add_child(config_key_type key)
Definition: config.cpp:441
void set_prevent_draw(bool pd=true)
Prevent the game display from drawing.
Definition: display.cpp:2244
bool get_prevent_draw()
Definition: display.cpp:2253
static display * get_singleton()
Returns the display object if a display object exists.
Definition: display.hpp:95
A simple canvas which can be drawn upon.
Definition: canvas.hpp:44
void set_variable(const std::string &key, wfl::variant &&value)
Definition: canvas.hpp:153
void queue_reblur()
Clear the cached blur texture, forcing it to regenerate.
Definition: canvas.cpp:571
void append_cfg(const config &cfg)
Appends data to the config.
Definition: canvas.hpp:131
void update_size_variables()
Update WFL size variables.
Definition: canvas.cpp:648
void set_cfg(const config &cfg, const bool force=false)
Sets the config.
Definition: canvas.hpp:120
Abstract base class for all modal dialogs.
window * get_window()
Returns a pointer to the dialog's window.
Dialog to view the storyscreen.
virtual void pre_show(window &window) override
Actions to be taken before showing the window.
virtual void post_show(window &window) override
Actions to be taken after the window has been shown.
storyscreen::controller controller_
void begin_fade_draw(bool fade_in)
storyscreen::controller::part_pointer_type current_part_
virtual void update() override
top_level_drawable hook to animate the view
void draw_floating_image(floating_image_list::const_iterator image_iter, int this_part_index)
void nav_button_callback(NAV_DIRECTION direction)
void key_press_callback(const SDL_Keycode key)
A label displays text that can be wrapped but no scrollbars are provided.
Definition: label.hpp:56
Label showing a text.
virtual void set_label(const t_string &label) override
See styled_widget::set_label.
virtual void set_text_alignment(const PangoAlignment text_alignment) override
See styled_widget::set_text_alignment.
void set_text_alpha(unsigned short alpha)
A stacked widget holds several widgets on top of each other.
int current_layer() const
Gets the current visible layer number.
grid * get_layer_grid(unsigned int i)
Gets the grid for a specified layer.
void select_layer(const int layer)
Selects and displays a particular layer.
virtual void set_text_alignment(const PangoAlignment text_alignment)
virtual void set_label(const t_string &text)
canvas & get_canvas(const unsigned index)
virtual void update()
Update state and any parameters that may effect layout, or any of the later stages.
void set_visible(const visibility visible)
Definition: widget.cpp:470
void queue_redraw()
Indicates that this widget should be redrawn.
Definition: widget.cpp:455
@ visible
The user sets the widget visible, that means:
@ invisible
The user set the widget invisible, that means:
virtual void set_vertical_alignment(const std::string &alignment)
Sets the horizontal alignment of the widget within its parent grid.
Definition: widget.cpp:284
base class of top level items, the only item which needs to store the final canvases to draw on.
Definition: window.hpp:63
void set_enter_disabled(const bool enter_disabled)
Disable the enter key.
Definition: window.hpp:327
void close()
Requests to close the window.
Definition: window.hpp:223
part_pointer_type get_part(int index) const
Definition: controller.hpp:40
@ BLOCK_BOTTOM
Bottom of the screen.
Definition: part.hpp:237
@ BLOCK_MIDDLE
Center of the screen.
Definition: part.hpp:236
@ BLOCK_TOP
Top of the screen.
Definition: part.hpp:235
A variable-expanding proxy for the config class.
Definition: variable.hpp:45
constexpr uint8_t ALPHA_OPAQUE
Definition: color.hpp:45
This file contains the window object, this object is a top level container which has the event manage...
REGISTER_DIALOG(tod_new_schedule)
static config get_title_area_decor_config()
static const unsigned int LAYER_TEXT
static const unsigned int LAYER_BACKGROUND
void connect_signal_pre_key_press(dispatcher &dispatcher, const signal_keyboard &signal)
Connects the signal for 'snooping' on the keypress.
Definition: dispatcher.cpp:172
void connect_signal_mouse_left_click(dispatcher &dispatcher, const signal &signal)
Connects a signal handler for a left mouse button click.
Definition: dispatcher.cpp:177
std::size_t add_timer(const uint32_t interval, const std::function< void(std::size_t id)> &callback, const bool repeat)
Adds a new timer.
Definition: timer.cpp:127
PangoAlignment decode_text_alignment(const std::string &alignment)
Converts a text alignment string to a text alignment.
Definition: helper.cpp:58
bool remove_timer(const std::size_t id)
Removes a timer.
Definition: timer.cpp:168
Functions to load and save images from/to disk.
int draw_delay()
Definition: general.cpp:897
void play_sound(const std::string &files, channel_group group, unsigned int repeats)
Definition: sound.cpp:1033
void play_music_config(const config &music_node, bool allow_interrupt_current_track, int i)
Definition: sound.cpp:711
void play_sound_positioned(const std::string &files, int id, int repeats, unsigned int distance)
Definition: sound.cpp:1040
void stop_sound()
Definition: sound.cpp:563
Contains the gui2 timer routines.