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