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/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 namespace gui2::dialogs
36 {
37 
38 // Helper function to get the canvas shape data for the shading under the title area until
39 // I can figure out how to ensure it always stays on top of the canvas stack.
41 {
42  static config cfg;
43  cfg["x"] = 0;
44  cfg["y"] = 0;
45  cfg["w"] = "(screen_width)";
46  cfg["h"] = "(image_original_height * 2)";
47  cfg["name"] = "dialogs/story_title_decor.png~O(75%)";
48 
49  return cfg;
50 }
51 
52 // Stacked widget layer constants for the text stack.
53 static const unsigned int LAYER_BACKGROUND = 1;
54 static const unsigned int LAYER_TEXT = 2;
55 
57 
58 story_viewer::story_viewer(const std::string& scenario_name, const config& cfg_parsed)
59  : modal_dialog(window_id())
60  , controller_(vconfig(cfg_parsed, true), scenario_name)
61  , part_index_(0)
62  , current_part_(nullptr)
63  , timer_id_(0)
64  , next_draw_(0)
65  , fade_step_(0)
66  , fade_state_(NOT_FADING)
67 {
68  update_current_part_ptr();
69 }
70 
72 {
73  if(timer_id_ != 0) {
75  timer_id_ = 0;
76  }
77 }
78 
80 {
82 }
83 
85 {
86  set_enter_disabled(true);
87 
88  // Special callback handle key presses
89  connect_signal_pre_key_press(*this, std::bind(&story_viewer::key_press_callback, this, std::placeholders::_5));
90 
91  connect_signal_mouse_left_click(find_widget<button>("next"),
93  connect_signal_mouse_left_click(find_widget<button>("prev"),
95 
96  connect_signal<event::BACK_BUTTON_CLICK>([this](auto&&...) {
99  connect_signal<event::FORWARD_BUTTON_CLICK>([this](auto&&...) {
102 
103  find_widget<scroll_label>("part_text").connect_signal<event::LEFT_BUTTON_CLICK>([this](auto&&...) {
106 
107  // Tell the game display not to draw
110 
111  display_part();
112 }
113 
115 {
116  // Bring the game display back again, if appropriate
118 }
119 
121 {
123 }
124 
126 {
127  static const int VOICE_SOUND_SOURCE_ID = 255;
128  // Update Back button state. Doing this here so it gets called in pre_show too.
129  find_widget<button>("prev").set_active(part_index_ != 0);
130 
131  //
132  // Music and sound
133  //
134  if(!current_part_->music().empty()) {
136  "name", current_part_->music(),
137  "ms_after", 2000,
138  "immediate", true
139  });
140  }
141 
142  if(!current_part_->sound().empty()) {
144  }
145 
146  sound::stop_sound(VOICE_SOUND_SOURCE_ID);
147  if(!current_part_->voice().empty()) {
148  sound::play_sound_positioned(current_part_->voice(), VOICE_SOUND_SOURCE_ID, 0, 0);
149  }
150 
151  config cfg;
152 
153  //
154  // Background images
155  //
156  bool has_background = false;
157  config* base_layer = nullptr;
158 
159  for(const auto& layer : current_part_->get_background_layers()) {
160  has_background |= !layer.file().empty();
161 
162  const bool preserve_ratio = layer.keep_aspect_ratio();
163  const bool tile_h = layer.tile_horizontally();
164  const bool tile_v = layer.tile_vertically();
165 
166  // Background layers are almost always centered. In case of tiling, we want the full
167  // area in the horizontal or vertical direction, so set the origin to 0 for that axis.
168  // The resize mode will center the original image in the available area first/
169  std::string x_formula = tile_h ? "0" : "(max(pos, 0) where pos = (width / 2 - image_width / 2))";
170  std::string y_formula = tile_v ? "0" : "(max(pos, 0) where pos = (height / 2 - image_height / 2))";
171 
172  // By default, no scaling will be applied.
173  std::string width_formula = "(image_original_width)";
174  std::string height_formula = "(image_original_height)";
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  config& image = cfg.add_child("image", config{
189  "x", x_formula,
190  "y", y_formula,
191  "w", width_formula,
192  "h", height_formula,
193  "name", layer.file(),
194  "resize_mode", (tile_h || tile_v) ? "tile_center" : "scale"
195  });
196 
197  if(base_layer == nullptr || layer.is_base_layer()) {
198  base_layer = &image;
199  }
200  }
201 
202  canvas& window_canvas = 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 
229 
230  window_canvas.set_shapes(cfg);
231 
232  // Needed to make the background redraw correctly.
233  window_canvas.update_size_variables();
234  queue_redraw();
235 
236  //
237  // Title
238  //
239  label& title_label = find_widget<stacked_widget>("background_stack").find_widget<label>("title_text");
240  std::string title_text = current_part_->title();
241  bool showing_title = current_part_->show_title() && !title_text.empty();
242  title_label.set_visible(showing_title);
243  if(showing_title) {
244  for(auto& canv : title_label.get_canvases()) {
245  auto& title_position = current_part_->title_position();
246  canv.set_variable("hperc", wfl::variant(title_position.x));
247  canv.set_variable("vperc", wfl::variant(title_position.y));
248  }
249  title_label.set_label(title_text);
250  title_label.set_text_alpha(0);
251  title_label.set_text_alignment(decode_text_alignment(current_part_->title_text_alignment()));
252  }
253  title_label.queue_redraw();
254 
255  //
256  // Story text
257  //
258  stacked_widget& text_stack = find_widget<stacked_widget>("text_and_control_stack");
259 
260  std::string new_panel_mode;
261 
262  switch(current_part_->story_text_location()) {
264  new_panel_mode = "top";
265  break;
267  new_panel_mode = "center";
268  break;
270  new_panel_mode = "bottom";
271  break;
272  }
273 
274  text_stack.set_vertical_alignment(new_panel_mode);
275 
276  /* Set the panel mode control variables.
277  *
278  * We use get_layer_grid here to ensure the widget is always found regardless of
279  * whether the background is visible or not.
280  */
281  canvas& panel_canvas = text_stack.get_layer_grid(LAYER_BACKGROUND)->find_widget<panel>("text_panel").get_canvas(0);
282 
283  panel_canvas.set_variable("panel_position", wfl::variant(new_panel_mode));
284  panel_canvas.set_variable("title_present", wfl::variant(showing_title));
285 
286  const std::string& part_text = current_part_->text();
287 
288  if(part_text.empty() || !has_background) {
289  // No text or no background for this part, hide the background layer.
290  text_stack.select_layer(LAYER_TEXT);
291  } else if(text_stack.current_layer() != -1) {
292  // If the background layer was previously hidden, re-show it.
293  text_stack.select_layer(-1);
294  }
295 
296  // Convert the story part text alignment types into the Pango equivalents
297  PangoAlignment story_text_alignment = decode_text_alignment(current_part_->story_text_alignment());
298 
299  scroll_label& text_label = find_widget<scroll_label>("part_text");
300  text_label.set_text_alignment(story_text_alignment);
301  text_label.set_text_alpha(0);
302  text_label.set_label(part_text);
303 
304  // Regenerate any background blur texture
305  panel_canvas.queue_reblur();
306 
307  begin_fade_draw(true);
308  // if the previous page was skipped, it is possible that we already have a timer running.
310  //
311  // Floating images (handle this last)
312  //
313  const auto& floating_images = current_part_->get_floating_images();
314 
315  // If we have images to draw, draw the first one now. A new non-repeating timer is added
316  // after every draw to schedule the next one after the specified interval.
317  //
318  // TODO: in the old GUI1 dialog, floating images delayed the appearance of the story panel until
319  // drawing was finished. Might be worth looking into restoring that.
320  if(!floating_images.empty()) {
321  draw_floating_image(floating_images.begin(), part_index_);
322  }
323 }
324 
325 void story_viewer::draw_floating_image(floating_image_list::const_iterator image_iter, int this_part_index)
326 {
327  const auto& images = current_part_->get_floating_images();
328  canvas& window_canvas = get_canvas(0);
329 
330  // If the current part has changed or we're out of images to draw, exit the draw loop.
331  while((this_part_index == part_index_) && (image_iter != images.end())) {
332  const auto& floating_image = *image_iter;
333  ++image_iter;
334 
335  std::ostringstream x_ss;
336  std::ostringstream y_ss;
337 
338  // Floating images' locations are scaled by the same factor as the background.
339  x_ss << "(trunc(" << floating_image.ref_x() << " * base_scale_x) + base_origin_x";
340  y_ss << "(trunc(" << floating_image.ref_y() << " * base_scale_y) + base_origin_y";
341 
342  if(floating_image.centered()) {
343  x_ss << " - (image_width / 2)";
344  y_ss << " - (image_height / 2)";
345  }
346 
347  x_ss << ")";
348  y_ss << ")";
349 
350  config image;
351  image["x"] = x_ss.str();
352  image["y"] = y_ss.str();
353 
354  // Width and height don't need to be set unless the image needs to be scaled.
355  if(floating_image.resize_with_background()) {
356  image["w"] = "(image_original_width * base_scale_x)";
357  image["h"] = "(image_original_height * base_scale_y)";
358  }
359 
360  image["name"] = floating_image.file();
361  config cfg{"image", std::move(image)};
362 
363  window_canvas.append_shapes(cfg);
364 
365  // Needed to make the background redraw correctly.
366  window_canvas.update_size_variables();
367  queue_redraw();
368 
369  // If a delay is specified, schedule the next image draw and break out of the loop.
370  const auto& draw_delay = floating_image.display_delay();
371  if(draw_delay != std::chrono::milliseconds{0}) {
372  // This must be a non-repeating timer
373  timer_id_ = add_timer(draw_delay, std::bind(&story_viewer::draw_floating_image, this, image_iter, this_part_index), false);
374  return;
375  }
376  }
377 
378  timer_id_ = 0;
379 }
380 
382 {
383  // If a button is pressed while fading in, abort and set alpha to full opaque.
384  if(fade_state_ == FADING_IN) {
385  halt_fade_draw();
386 
387  // Only set full alpha if Forward was pressed.
388  if(direction == DIR_FORWARD) {
389  find_widget<scroll_label>("part_text").set_text_alpha(ALPHA_OPAQUE);
391  return;
392  }
393  }
394 
395  // If a button is pressed while fading out, skip and show next part.
396  if(fade_state_ == FADING_OUT) {
397  display_part();
398  return;
399  }
400 
401  assert(fade_state_ == NOT_FADING);
402 
403  part_index_ = (direction == DIR_FORWARD ? part_index_ + 1 : part_index_ -1);
404 
405  // If we've viewed all the parts, close the dialog.
407  close();
408  return;
409  }
410 
411  if(part_index_ < 0) {
412  part_index_ = 0;
413  }
414 
416 
417  begin_fade_draw(false);
418 }
419 
420 void story_viewer::key_press_callback(const SDL_Keycode key)
421 {
422  const bool next_keydown =
423  key == SDLK_SPACE
424  || key == SDLK_RETURN
425  || key == SDLK_KP_ENTER
426  || key == SDLK_RIGHT;
427 
428  const bool back_keydown =
429  key == SDLK_BACKSPACE
430  || key == SDLK_LEFT;
431 
432  if(next_keydown) {
434  } else if(back_keydown) {
436  }
437 }
438 
440 {
441  next_draw_ = SDL_GetTicks() + 20;
442 }
443 
445 {
446  set_next_draw();
447 
448  fade_step_ = fade_in ? 0 : 10;
449  fade_state_ = fade_in ? FADING_IN : FADING_OUT;
450 }
451 
453 {
454  next_draw_ = 0;
455  fade_step_ = -1;
457 }
458 
460 {
462 
463  if(next_draw_ && SDL_GetTicks() < next_draw_) {
464  return;
465  }
466 
467  if(fade_state_ == NOT_FADING) {
468  return;
469  }
470 
471  // If we've faded fully in...
472  if(fade_state_ == FADING_IN && fade_step_ > 10) {
473  halt_fade_draw();
474  return;
475  }
476 
477  // If we've faded fully out...
478  if(fade_state_ == FADING_OUT && fade_step_ < 0) {
479  halt_fade_draw();
480 
481  display_part();
482  return;
483  }
484 
485  unsigned short new_alpha = std::clamp<short>(fade_step_ * 25.5, 0, ALPHA_OPAQUE);
486  find_widget<scroll_label>("part_text").set_text_alpha(new_alpha);
487  find_widget<label>("title_text").set_text_alpha(new_alpha);
488  queue_redraw();
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)
void set_text_alpha(unsigned short alpha)
Definition: label.cpp:73
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.
std::vector< canvas > & get_canvases()
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
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: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.