The Battle for Wesnoth  1.19.25+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/grid.hpp"
26 #include "gui/widgets/image.hpp"
27 #include "gui/widgets/label.hpp"
29 #include "gui/widgets/settings.hpp"
31 #include "gui/widgets/window.hpp"
32 #include "sound.hpp"
33 #include "variable.hpp"
34 
35 #include <SDL3/SDL_timer.h>
36 
37 namespace gui2::dialogs
38 {
39 
40 // Helper function to get the canvas shape data for the shading under the title area until
41 // I can figure out how to ensure it always stays on top of the canvas stack.
43 {
44  static config cfg;
45  cfg["x"] = 0;
46  cfg["y"] = 0;
47  cfg["w"] = "(screen_width)";
48  cfg["h"] = "(image_original_height * 2)";
49  cfg["name"] = "dialogs/story_title_decor.png~O(75%)";
50 
51  return cfg;
52 }
53 
54 // Stacked widget layer constants for the text stack.
55 static const unsigned int LAYER_BACKGROUND = 1;
56 static const unsigned int LAYER_TEXT = 2;
57 
59 
60 story_viewer::story_viewer(const std::string& scenario_name, const config& cfg_parsed)
61  : modal_dialog(window_id())
62  , controller_(vconfig(cfg_parsed, true), scenario_name)
63  , part_index_(0)
64  , current_part_(nullptr)
65  , timer_id_(0)
66  , next_draw_(0)
67  , fade_step_(0)
68  , fade_state_(NOT_FADING)
69 {
70  update_current_part_ptr();
71 }
72 
74 {
75  if(timer_id_ != 0) {
77  timer_id_ = 0;
78  }
79 }
80 
82 {
84 }
85 
87 {
88  set_enter_disabled(true);
89 
90  // Special callback handle key presses
91  connect_signal_pre_key_press(*this, std::bind(&story_viewer::key_press_callback, this, std::placeholders::_5));
92 
93  connect_signal_mouse_left_click(find_widget<button>("next"),
95  connect_signal_mouse_left_click(find_widget<button>("prev"),
97 
98  connect_signal<event::BACK_BUTTON_CLICK>([this](auto&&...) {
101  connect_signal<event::FORWARD_BUTTON_CLICK>([this](auto&&...) {
104 
105  find_widget<scroll_label>("part_text").connect_signal<event::LEFT_BUTTON_CLICK>([this](auto&&...) {
108 
109  // Tell the game display not to draw
112 
113  display_part();
114 }
115 
117 {
118  // Bring the game display back again, if appropriate
120 }
121 
123 {
125 }
126 
128 {
129  static const int VOICE_SOUND_SOURCE_ID = 255;
130  // Update Back button state. Doing this here so it gets called in pre_show too.
131  find_widget<button>("prev").set_active(part_index_ != 0);
132 
133  //
134  // Music and sound
135  //
136  if(!current_part_->music().empty()) {
138  "name", current_part_->music(),
139  "ms_after", 2000,
140  "immediate", true
141  });
142  }
143 
144  if(!current_part_->sound().empty()) {
146  }
147 
148  sound::stop_sound(VOICE_SOUND_SOURCE_ID);
149  if(!current_part_->voice().empty()) {
151  }
152 
153  config cfg;
154 
155  //
156  // Background images
157  //
158  bool has_background = false;
159  config* base_layer = nullptr;
160 
161  for(const auto& layer : current_part_->get_background_layers()) {
162  has_background |= !layer.file().empty();
163 
164  const bool preserve_ratio = layer.keep_aspect_ratio();
165  const bool tile_h = layer.tile_horizontally();
166  const bool tile_v = layer.tile_vertically();
167 
168  // Background layers are almost always centered. In case of tiling, we want the full
169  // area in the horizontal or vertical direction, so set the origin to 0 for that axis.
170  // The resize mode will center the original image in the available area first/
171  std::string x_formula = tile_h ? "0" : "(max(pos, 0) where pos = (width / 2 - image_width / 2))";
172  std::string y_formula = tile_v ? "0" : "(max(pos, 0) where pos = (height / 2 - image_height / 2))";
173 
174  // By default, no scaling will be applied.
175  std::string width_formula = "(image_original_width)";
176  std::string height_formula = "(image_original_height)";
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  config& image = 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  if(base_layer == nullptr || layer.is_base_layer()) {
200  base_layer = &image;
201  }
202  }
203 
204  canvas& window_canvas = 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 
231 
232  //
233  // Title
234  //
235  const std::string& title_text = current_part_->title();
236  if(current_part_->show_title() && !title_text.empty()) {
237  const point& title_position = current_part_->title_position();
238  cfg.add_child("text", config{
239  "x", "(max(" + std::to_string(title_position.x) + " * (width - 10 - text_width) / 100, 10))",
240  "y", "(max(" + std::to_string(title_position.y) + " * (height - 10 - text_height) / 100, 10))",
241  "w", "(text_width)",
242  "h", "(text_height)",
243  "maximum_width", "(text_width)",
244  "font_family", "script",
245  "font_size", 32,
246  "color", "([215, 215, 215, title_alpha])",
247  "text", title_text,
248  "text_markup", true,
249  "text_alignment", current_part_->title_text_alignment(),
250  "text_link_aware", "false"
251  });
252  }
253 
254  window_canvas.set_shapes(cfg);
255 
256  // Needed to make the background redraw correctly.
257  window_canvas.update_size_variables();
258  queue_redraw();
259 
260  //
261  // Story text
262  //
263  stacked_widget& text_stack = find_widget<stacked_widget>("text_and_control_stack");
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 = text_stack.get_layer_grid(LAYER_BACKGROUND)->find_widget<panel>("text_panel").get_canvas(0);
287 
288  panel_canvas.set_variable("panel_position", wfl::variant(new_panel_mode));
289 
290  const std::string& part_text = current_part_->text();
291 
292  if(part_text.empty() || !has_background) {
293  // No text or no background for this part, hide the background layer.
294  text_stack.select_layer(LAYER_TEXT);
295  } else if(text_stack.current_layer() != -1) {
296  // If the background layer was previously hidden, re-show it.
297  text_stack.select_layer(-1);
298  }
299 
300  // Convert the story part text alignment types into the Pango equivalents
301  PangoAlignment story_text_alignment = decode_text_alignment(current_part_->story_text_alignment());
302 
303  scroll_label& text_label = find_widget<scroll_label>("part_text");
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_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  window_canvas.append_shapes(cfg);
368 
369  // Needed to make the background redraw correctly.
370  window_canvas.update_size_variables();
371  queue_redraw();
372 
373  // If a delay is specified, schedule the next image draw and break out of the loop.
374  const auto& draw_delay = floating_image.display_delay();
375  if(draw_delay != std::chrono::milliseconds{0}) {
376  // This must be a non-repeating timer
377  timer_id_ = add_timer(draw_delay, std::bind(&story_viewer::draw_floating_image, this, image_iter, this_part_index), false);
378  return;
379  }
380  }
381 
382  timer_id_ = 0;
383 }
384 
386 {
387  // If a button is pressed while fading in, abort and set alpha to full opaque.
388  if(fade_state_ == FADING_IN) {
389  halt_fade_draw();
390 
391  // Only set full alpha if Forward was pressed.
392  if(direction == DIR_FORWARD) {
393  find_widget<scroll_label>("part_text").set_text_alpha(ALPHA_OPAQUE);
395  return;
396  }
397  }
398 
399  // If a button is pressed while fading out, skip and show next part.
400  if(fade_state_ == FADING_OUT) {
401  display_part();
402  return;
403  }
404 
405  assert(fade_state_ == NOT_FADING);
406 
407  part_index_ = (direction == DIR_FORWARD ? part_index_ + 1 : part_index_ -1);
408 
409  // If we've viewed all the parts, close the dialog.
411  close();
412  return;
413  }
414 
415  if(part_index_ < 0) {
416  part_index_ = 0;
417  }
418 
420 
421  begin_fade_draw(false);
422 }
423 
424 void story_viewer::key_press_callback(const SDL_Keycode key)
425 {
426  const bool next_keydown =
427  key == SDLK_SPACE
428  || key == SDLK_RETURN
429  || key == SDLK_KP_ENTER
430  || key == SDLK_RIGHT;
431 
432  const bool back_keydown =
433  key == SDLK_BACKSPACE
434  || key == SDLK_LEFT;
435 
436  if(next_keydown) {
438  } else if(back_keydown) {
440  }
441 }
442 
444 {
445  next_draw_ = SDL_GetTicks() + 20;
446 }
447 
449 {
450  set_next_draw();
451 
452  fade_step_ = fade_in ? 0 : 10;
453  fade_state_ = fade_in ? FADING_IN : FADING_OUT;
454 }
455 
457 {
458  next_draw_ = 0;
459  fade_step_ = -1;
461 }
462 
464 {
466 
467  if(next_draw_ && SDL_GetTicks() < next_draw_) {
468  return;
469  }
470 
471  if(fade_state_ == NOT_FADING) {
472  return;
473  }
474 
475  // If we've faded fully in...
476  if(fade_state_ == FADING_IN && fade_step_ > 10) {
477  halt_fade_draw();
478  return;
479  }
480 
481  // If we've faded fully out...
482  if(fade_state_ == FADING_OUT && fade_step_ < 0) {
483  halt_fade_draw();
484 
485  display_part();
486  return;
487  }
488 
489  unsigned short new_alpha = std::clamp<short>(fade_step_ * 25.5, 0, ALPHA_OPAQUE);
490  find_widget<scroll_label>("part_text").set_text_alpha(new_alpha);
491  get_canvas(0).set_variable("title_alpha", wfl::variant(new_alpha));
492  queue_redraw();
493 
494  // The text stack also needs to be marked dirty so the background panel redraws correctly.
496 
497  if(fade_state_ == FADING_IN) {
498  fade_step_ ++;
499  } else if(fade_state_ == FADING_OUT) {
500  fade_step_ --;
501  }
502 
503  set_next_draw();
504 }
505 
507 {
508  find_widget<stacked_widget>("text_and_control_stack").queue_redraw();
509 }
510 
511 } // 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:436
bool empty() const
Definition: config.cpp:823
void set_prevent_draw(bool pd=true)
Prevent the game display from drawing.
Definition: display.cpp:2021
bool get_prevent_draw()
Definition: display.cpp:2030
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:165
void queue_reblur()
Clear the cached blur texture, forcing it to regenerate.
Definition: canvas.cpp:774
void append_shapes(const config &cfg)
Appends data to the config.
Definition: canvas.hpp:143
void set_shapes(const config &cfg, const bool force=false)
Sets the config.
Definition: canvas.hpp:126
void update_size_variables()
Update WFL size variables.
Definition: canvas.cpp:848
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.
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 queue_redraw()
Indicates that this widget should be redrawn.
Definition: widget.cpp:464
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:239
@ BLOCK_MIDDLE
Center of the screen.
Definition: part.hpp:238
@ BLOCK_TOP
Top of the screen.
Definition: part.hpp:237
A variable-expanding proxy for the config class.
Definition: variable.hpp:45
constexpr uint8_t ALPHA_OPAQUE
Definition: color.hpp:37
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_music_config(const config &music_node, bool allow_interrupt_current_track, int i)
Definition: sound.cpp:619
void play_sound(const std::string &files, sound_tracks::type group, unsigned int repeats)
Definition: sound.cpp:921
void stop_sound()
Definition: sound.cpp:494
std::string to_string(const Range &range, const Func &op)
This file contains the settings handling of the widget library.
Holds a 2D point.
Definition: point.hpp:25
Contains the gui2 timer routines.