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