The Battle for Wesnoth  1.19.13+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()) {
135  config music_config;
136  music_config["name"] = current_part_->music();
137  music_config["ms_after"] = 2000;
138  music_config["immediate"] = true;
139 
140  sound::play_music_config(music_config);
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, image;
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  // By default, no scaling will be applied.
168  std::string width_formula = "(image_original_width)";
169  std::string height_formula = "(image_original_height)";
170 
171  // Background layers are almost always centered. In case of tiling, we want the full
172  // area in the horizontal or vertical direction, so set the origin to 0 for that axis.
173  // The resize mode will center the original image in the available area first/
174  std::string x_formula;
175  std::string y_formula;
176 
177  if(tile_h) {
178  x_formula = "0";
179  } else {
180  x_formula = "(max(pos, 0) where pos = (width / 2 - image_width / 2))";
181  }
182 
183  if(tile_v) {
184  y_formula = "0";
185  } else {
186  y_formula = "(max(pos, 0) where pos = (height / 2 - image_height / 2))";
187  }
188 
189  if(layer.scale_horizontally() && preserve_ratio) {
190  height_formula = "(min((image_original_height * width / image_original_width), height))";
191  } else if(layer.scale_vertically() || tile_v) {
192  height_formula = "(height)";
193  }
194 
195  if(layer.scale_vertically() && preserve_ratio) {
196  width_formula = "(min((image_original_width * height / image_original_height), width))";
197  } else if(layer.scale_horizontally() || tile_h) {
198  width_formula = "(width)";
199  }
200 
201  image["x"] = x_formula;
202  image["y"] = y_formula;
203  image["w"] = width_formula;
204  image["h"] = height_formula;
205  image["name"] = layer.file();
206  image["resize_mode"] = (tile_h || tile_v) ? "tile_center" : "scale";
207 
208  config& layer_image = cfg.add_child("image", image);
209 
210  if(base_layer == nullptr || layer.is_base_layer()) {
211  base_layer = &layer_image;
212  }
213  }
214 
215  canvas& window_canvas = get_canvas(0);
216 
217  /* In order to avoid manually loading the image and calculating the scaling factor, we instead
218  * delegate the task of setting the necessary variables to the canvas once the calculations
219  * have been made internally.
220  *
221  * This sets the necessary values with the data for "this" image when its drawn. If no base
222  * layer was found (which would be the case if no backgrounds were provided at all), simply set
223  * some sane defaults directly.
224  */
225  if(base_layer != nullptr) {
226  (*base_layer)["actions"] = R"((
227  [
228  set_var('base_scale_x', as_decimal(image_width) / as_decimal(image_original_width)),
229  set_var('base_scale_y', as_decimal(image_height) / as_decimal(image_original_height)),
230  set_var('base_origin_x', clip_x),
231  set_var('base_origin_y', clip_y)
232  ]
233  ))";
234  } else {
235  window_canvas.set_variable("base_scale_x", wfl::variant(1));
236  window_canvas.set_variable("base_scale_y", wfl::variant(1));
237  window_canvas.set_variable("base_origin_x", wfl::variant(0));
238  window_canvas.set_variable("base_origin_y", wfl::variant(0));
239  }
240 
241  cfg.add_child("image", get_title_area_decor_config());
242 
243  window_canvas.set_shapes(cfg);
244 
245  // Needed to make the background redraw correctly.
246  window_canvas.update_size_variables();
247  queue_redraw();
248 
249  //
250  // Title
251  //
252  label& title_label = find_widget<label>("title");
253 
254  std::string title_text = current_part_->title();
255  bool showing_title;
256 
257  if(current_part_->show_title() && !title_text.empty()) {
258  showing_title = true;
259 
260  PangoAlignment title_text_alignment = decode_text_alignment(current_part_->title_text_alignment());
261 
263  title_label.set_text_alignment(title_text_alignment);
264  title_label.set_label(title_text);
265  } else {
266  showing_title = false;
267 
269  }
270 
271  //
272  // Story text
273  //
274  stacked_widget& text_stack = find_widget<stacked_widget>("text_and_control_stack");
275 
276  std::string new_panel_mode;
277 
278  switch(current_part_->story_text_location()) {
279 
281  new_panel_mode = "top";
282  break;
284  new_panel_mode = "center";
285  break;
287  new_panel_mode = "bottom";
288  break;
289  }
290 
291  text_stack.set_vertical_alignment(new_panel_mode);
292 
293  /* Set the panel mode control variables.
294  *
295  * We use get_layer_grid here to ensure the widget is always found regardless of
296  * whether the background is visible or not.
297  */
298  canvas& panel_canvas = text_stack.get_layer_grid(LAYER_BACKGROUND)->find_widget<panel>("text_panel").get_canvas(0);
299 
300  panel_canvas.set_variable("panel_position", wfl::variant(new_panel_mode));
301  panel_canvas.set_variable("title_present", wfl::variant(static_cast<int>(showing_title))); // cast to 0/1
302 
303  const std::string& part_text = current_part_->text();
304 
305  if(part_text.empty() || !has_background) {
306  // No text or no background for this part, hide the background layer.
307  text_stack.select_layer(LAYER_TEXT);
308  } else if(text_stack.current_layer() != -1) {
309  // If the background layer was previously hidden, re-show it.
310  text_stack.select_layer(-1);
311  }
312 
313  // Convert the story part text alignment types into the Pango equivalents
314  PangoAlignment story_text_alignment = decode_text_alignment(current_part_->story_text_alignment());
315 
316  scroll_label& text_label = find_widget<scroll_label>("part_text");
317  text_label.set_text_alignment(story_text_alignment);
318  text_label.set_text_alpha(0);
319  text_label.set_label(part_text);
320 
321  // Regenerate any background blur texture
322  panel_canvas.queue_reblur();
323 
324  begin_fade_draw(true);
325  // if the previous page was skipped, it is possible that we already have a timer running.
327  //
328  // Floating images (handle this last)
329  //
330  const auto& floating_images = current_part_->get_floating_images();
331 
332  // If we have images to draw, draw the first one now. A new non-repeating timer is added
333  // after every draw to schedule the next one after the specified interval.
334  //
335  // TODO: in the old GUI1 dialog, floating images delayed the appearance of the story panel until
336  // drawing was finished. Might be worth looking into restoring that.
337  if(!floating_images.empty()) {
338  draw_floating_image(floating_images.begin(), part_index_);
339  }
340 }
341 
342 void story_viewer::draw_floating_image(floating_image_list::const_iterator image_iter, int this_part_index)
343 {
344  const auto& images = current_part_->get_floating_images();
345  canvas& window_canvas = get_canvas(0);
346 
347  // If the current part has changed or we're out of images to draw, exit the draw loop.
348  while((this_part_index == part_index_) && (image_iter != images.end())) {
349  const auto& floating_image = *image_iter;
350  ++image_iter;
351 
352  std::ostringstream x_ss;
353  std::ostringstream y_ss;
354 
355  // Floating images' locations are scaled by the same factor as the background.
356  x_ss << "(trunc(" << floating_image.ref_x() << " * base_scale_x) + base_origin_x";
357  y_ss << "(trunc(" << floating_image.ref_y() << " * base_scale_y) + base_origin_y";
358 
359  if(floating_image.centered()) {
360  x_ss << " - (image_width / 2)";
361  y_ss << " - (image_height / 2)";
362  }
363 
364  x_ss << ")";
365  y_ss << ")";
366 
367  config image;
368  image["x"] = x_ss.str();
369  image["y"] = y_ss.str();
370 
371  // Width and height don't need to be set unless the image needs to be scaled.
372  if(floating_image.resize_with_background()) {
373  image["w"] = "(image_original_width * base_scale_x)";
374  image["h"] = "(image_original_height * base_scale_y)";
375  }
376 
377  image["name"] = floating_image.file();
378  config cfg{"image", std::move(image)};
379 
380  window_canvas.append_shapes(cfg);
381 
382  // Needed to make the background redraw correctly.
383  window_canvas.update_size_variables();
384  queue_redraw();
385 
386  // If a delay is specified, schedule the next image draw and break out of the loop.
387  const auto& draw_delay = floating_image.display_delay();
388  if(draw_delay != std::chrono::milliseconds{0}) {
389  // This must be a non-repeating timer
390  timer_id_ = add_timer(draw_delay, std::bind(&story_viewer::draw_floating_image, this, image_iter, this_part_index), false);
391  return;
392  }
393  }
394 
395  timer_id_ = 0;
396 }
397 
399 {
400  // If a button is pressed while fading in, abort and set alpha to full opaque.
401  if(fade_state_ == FADING_IN) {
402  halt_fade_draw();
403 
404  // Only set full alpha if Forward was pressed.
405  if(direction == DIR_FORWARD) {
406  find_widget<scroll_label>("part_text").set_text_alpha(ALPHA_OPAQUE);
408  return;
409  }
410  }
411 
412  // If a button is pressed while fading out, skip and show next part.
413  if(fade_state_ == FADING_OUT) {
414  display_part();
415  return;
416  }
417 
418  assert(fade_state_ == NOT_FADING);
419 
420  part_index_ = (direction == DIR_FORWARD ? part_index_ + 1 : part_index_ -1);
421 
422  // If we've viewed all the parts, close the dialog.
424  close();
425  return;
426  }
427 
428  if(part_index_ < 0) {
429  part_index_ = 0;
430  }
431 
433 
434  begin_fade_draw(false);
435 }
436 
437 void story_viewer::key_press_callback(const SDL_Keycode key)
438 {
439  const bool next_keydown =
440  key == SDLK_SPACE
441  || key == SDLK_RETURN
442  || key == SDLK_KP_ENTER
443  || key == SDLK_RIGHT;
444 
445  const bool back_keydown =
446  key == SDLK_BACKSPACE
447  || key == SDLK_LEFT;
448 
449  if(next_keydown) {
451  } else if(back_keydown) {
453  }
454 }
455 
457 {
458  next_draw_ = SDL_GetTicks() + 20;
459 }
460 
462 {
463  set_next_draw();
464 
465  fade_step_ = fade_in ? 0 : 10;
466  fade_state_ = fade_in ? FADING_IN : FADING_OUT;
467 }
468 
470 {
471  next_draw_ = 0;
472  fade_step_ = -1;
474 }
475 
477 {
479 
480  if(next_draw_ && SDL_GetTicks() < next_draw_) {
481  return;
482  }
483 
484  if(fade_state_ == NOT_FADING) {
485  return;
486  }
487 
488  // If we've faded fully in...
489  if(fade_state_ == FADING_IN && fade_step_ > 10) {
490  halt_fade_draw();
491  return;
492  }
493 
494  // If we've faded fully out...
495  if(fade_state_ == FADING_OUT && fade_step_ < 0) {
496  halt_fade_draw();
497 
498  display_part();
499  return;
500  }
501 
502  unsigned short new_alpha = std::clamp<short>(fade_step_ * 25.5, 0, ALPHA_OPAQUE);
503  find_widget<scroll_label>("part_text").set_text_alpha(new_alpha);
504 
505  // The text stack also needs to be marked dirty so the background panel redraws correctly.
507 
508  if(fade_state_ == FADING_IN) {
509  fade_step_ ++;
510  } else if(fade_state_ == FADING_OUT) {
511  fade_step_ --;
512  }
513 
514  set_next_draw();
515 }
516 
518 {
519  find_widget<stacked_widget>("text_and_control_stack").queue_redraw();
520 }
521 
522 } // namespace dialogs
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:158
bool empty() const
Definition: config.cpp:845
config & add_child(config_key_type key)
Definition: config.cpp:436
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:638
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:711
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:321
void close()
Requests to close the window.
Definition: window.hpp:218
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.
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:1043
void play_music_config(const config &music_node, bool allow_interrupt_current_track, int i)
Definition: sound.cpp:712
void play_sound_positioned(const std::string &files, int id, int repeats, unsigned int distance)
Definition: sound.cpp:1050
void stop_sound()
Definition: sound.cpp:562
This file contains the settings handling of the widget library.
Contains the gui2 timer routines.