1use super::{
2 DEFAULT_NPC, ENEMY_HP_COLOR, FACTION_COLOR, GROUP_COLOR, GROUP_MEMBER, HP_COLOR, LOW_HP_COLOR,
3 QUALITY_EPIC, REGION_COLOR, SAY_COLOR, STAMINA_COLOR, TELL_COLOR, TEXT_BG, TEXT_COLOR,
4 cr_color, img_ids::Imgs,
5};
6use crate::{
7 GlobalState,
8 game_input::GameInput,
9 hud::{BuffIcon, controller_icons as icon_utils},
10 ui::{RichText, fonts::Fonts},
11 window::LastInput,
12};
13use common::{
14 comp::{Buffs, Energy, Health, SpeechBubble, SpeechBubbleType, Stance},
15 resources::Time,
16};
17use conrod_core::{
18 Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon, color,
19 position::Align,
20 widget::{self, Image, Rectangle, RoundedRectangle, Text},
21 widget_ids,
22};
23use i18n::Localization;
24
25const MAX_BUBBLE_WIDTH: f64 = 250.0;
26widget_ids! {
27 struct Ids {
28 speech_bubble_text,
30 speech_bubble_shadow,
31 speech_bubble_top_left,
32 speech_bubble_top,
33 speech_bubble_top_right,
34 speech_bubble_left,
35 speech_bubble_mid,
36 speech_bubble_right,
37 speech_bubble_bottom_left,
38 speech_bubble_bottom,
39 speech_bubble_bottom_right,
40 speech_bubble_tail,
41 speech_bubble_icon,
42
43 name_bg,
45 name,
46
47 level,
49 level_skull,
50 hardcore,
51 health_bar,
52 decay_bar,
53 health_bar_bg,
54 health_txt,
55 mana_bar,
56 health_bar_fg,
57
58 buffs_align,
60 buffs[],
61 buff_timers[],
62
63 interaction_hints_action,
65 interaction_hints_input,
66 interaction_hints_bg,
67 }
68}
69
70pub struct Info<'a> {
71 pub name: Option<String>,
72 pub health: Option<&'a Health>,
73 pub buffs: Option<&'a Buffs>,
74 pub energy: Option<&'a Energy>,
75 pub combat_rating: Option<f32>,
76 pub hardcore: bool,
77 pub stance: Option<&'a Stance>,
78}
79
80pub fn should_show_healthbar(health: &Health) -> bool {
82 (health.current() - health.maximum()).abs() > Health::HEALTH_EPSILON
83 || health.current() < health.base_max()
84}
85pub fn decayed_health_displayed(health: &Health) -> bool {
87 (1.0 - health.maximum() / health.base_max()) > 0.0
88}
89#[derive(WidgetCommon)]
92pub struct Overhead<'a> {
93 info: Option<Info<'a>>,
94 bubble: Option<&'a SpeechBubble>,
95 in_group: bool,
96 pulse: f32,
97 interaction_options: Vec<(GameInput, String)>,
98
99 i18n: &'a Localization,
100 imgs: &'a Imgs,
101 fonts: &'a Fonts,
102 time: &'a Time,
103 global_state: &'a GlobalState,
104
105 #[conrod(common_builder)]
106 common: widget::CommonBuilder,
107}
108
109impl<'a> Overhead<'a> {
110 pub fn new(
111 info: Option<Info<'a>>,
112 bubble: Option<&'a SpeechBubble>,
113 in_group: bool,
114 pulse: f32,
115 interaction_options: Vec<(GameInput, String)>,
116 i18n: &'a Localization,
117 imgs: &'a Imgs,
118 fonts: &'a Fonts,
119 time: &'a Time,
120 global_state: &'a GlobalState,
121 ) -> Self {
122 Self {
123 info,
124 bubble,
125 in_group,
126 pulse,
127 interaction_options,
128 i18n,
129 imgs,
130 fonts,
131 time,
132 global_state,
133 common: widget::CommonBuilder::default(),
134 }
135 }
136}
137
138pub struct State {
139 ids: Ids,
140}
141
142impl Widget for Overhead<'_> {
143 type Event = ();
144 type State = State;
145 type Style = ();
146
147 fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
148 State {
149 ids: Ids::new(id_gen),
150 }
151 }
152
153 fn style(&self) -> Self::Style {}
154
155 fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
156 let widget::UpdateArgs { id, state, ui, .. } = args;
157 const BARSIZE: f64 = 2.0; const MANA_BAR_HEIGHT: f64 = BARSIZE * 1.5;
159 const MANA_BAR_Y: f64 = MANA_BAR_HEIGHT / 2.0;
160 if let Some(Info {
161 ref name,
162 health,
163 buffs,
164 energy,
165 combat_rating,
166 hardcore,
167 stance,
168 }) = self.info
169 {
170 let hp_percentage = health.map_or(100.0, |h| {
172 f64::from(h.current() / h.base_max().max(h.maximum()) * 100.0)
173 });
174 let health_current = health.map_or(1.0, |h| f64::from(h.current()));
176 let health_max = health.map_or(1.0, |h| f64::from(h.maximum()));
177 let name_y = if (health_current - health_max).abs() < 1e-6 {
178 MANA_BAR_Y + 20.0
179 } else {
180 MANA_BAR_Y + 32.0
181 };
182 let font_size = if hp_percentage.abs() > 99.9 { 24 } else { 20 };
183 let health_cur_txt = if self.global_state.settings.interface.use_health_prefixes {
186 match health_current as u32 {
187 0..=999 => format!("{:.0}", health_current.max(1.0)),
188 1000..=999999 => format!("{:.0}K", (health_current / 1000.0).max(1.0)),
189 _ => format!("{:.0}M", (health_current / 1.0e6).max(1.0)),
190 }
191 } else {
192 format!("{:.0}", health_current.max(1.0))
193 };
194 let health_max_txt = if self.global_state.settings.interface.use_health_prefixes {
195 match health_max as u32 {
196 0..=999 => format!("{:.0}", health_max.max(1.0)),
197 1000..=999999 => format!("{:.0}K", (health_max / 1000.0).max(1.0)),
198 _ => format!("{:.0}M", (health_max / 1.0e6).max(1.0)),
199 }
200 } else {
201 format!("{:.0}", health_max.max(1.0))
202 };
203 let buff_icons = buffs
206 .as_ref()
207 .map(|buffs| BuffIcon::icons_vec(buffs, stance))
208 .unwrap_or_default();
209 let buff_count = buff_icons.len().min(11);
210 Rectangle::fill_with([168.0, 100.0], color::TRANSPARENT)
211 .x_y(-1.0, name_y + 60.0)
212 .parent(id)
213 .set(state.ids.buffs_align, ui);
214
215 let generator = &mut ui.widget_id_generator();
216 if state.ids.buffs.len() < buff_count {
217 state.update(|state| state.ids.buffs.resize(buff_count, generator));
218 };
219 if state.ids.buff_timers.len() < buff_count {
220 state.update(|state| state.ids.buff_timers.resize(buff_count, generator));
221 };
222
223 let buff_ani = ((self.pulse * 4.0).cos() * 0.5 + 0.8) + 0.5; let pulsating_col = Color::Rgba(1.0, 1.0, 1.0, buff_ani);
225 let norm_col = Color::Rgba(1.0, 1.0, 1.0, 1.0);
226 if self.bubble.is_none() {
228 state
229 .ids
230 .buffs
231 .iter()
232 .copied()
233 .zip(state.ids.buff_timers.iter().copied())
234 .zip(buff_icons.iter())
235 .enumerate()
236 .for_each(|(i, ((id, timer_id), buff))| {
237 let max_duration = buff.kind.max_duration();
239 let current_duration = buff.end_time.map(|end| end - self.time.0);
240 let duration_percentage = current_duration.map_or(1000.0, |cur| {
241 max_duration.map_or(1000.0, |max| cur / max.0 * 1000.0)
242 }) as u32; let buff_img = buff.kind.image(self.imgs);
244 let buff_widget = Image::new(buff_img).w_h(20.0, 20.0);
245 let x = i % 5;
247 let y = i / 5;
248 let buff_widget = buff_widget.bottom_left_with_margins_on(
249 state.ids.buffs_align,
250 0.0 + y as f64 * (21.0),
251 0.0 + x as f64 * (21.0),
252 );
253 buff_widget
254 .color(if current_duration.is_some_and(|cur| cur < 10.0) {
255 Some(pulsating_col)
256 } else {
257 Some(norm_col)
258 })
259 .set(id, ui);
260
261 Image::new(match duration_percentage as u64 {
262 875..=1000 => self.imgs.nothing, 750..=874 => self.imgs.buff_0, 625..=749 => self.imgs.buff_1, 500..=624 => self.imgs.buff_2, 375..=499 => self.imgs.buff_3, 250..=374 => self.imgs.buff_4, 125..=249 => self.imgs.buff_5, 0..=124 => self.imgs.buff_6, _ => self.imgs.nothing,
271 })
272 .w_h(20.0, 20.0)
273 .middle_of(id)
274 .set(timer_id, ui);
275 });
276 }
277 Text::new(name.as_deref().unwrap_or(""))
279 .font_id(self.fonts.cyri.conrod_id)
281 .font_size(font_size)
282 .color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
283 .x_y(-1.0, name_y)
284 .parent(id)
285 .set(state.ids.name_bg, ui);
286 Text::new(name.as_deref().unwrap_or(""))
287 .font_id(self.fonts.cyri.conrod_id)
289 .font_size(font_size)
290 .color(if self.in_group {
291 GROUP_MEMBER
292 } else {
295 DEFAULT_NPC
296 })
297 .x_y(0.0, name_y + 1.0)
298 .parent(id)
299 .set(state.ids.name, ui);
300
301 match health {
302 Some(health)
303 if should_show_healthbar(health) || decayed_health_displayed(health) =>
304 {
305 let hp_ani = (self.pulse * 4.0).cos() * 0.5 + 1.0; let crit_hp_color: Color = Color::Rgba(0.93, 0.59, 0.03, hp_ani);
308 let decayed_health = f64::from(1.0 - health.maximum() / health.base_max());
309 Image::new(if self.in_group {self.imgs.health_bar_group_bg} else {self.imgs.enemy_health_bg})
311 .w_h(84.0 * BARSIZE, 10.0 * BARSIZE)
312 .x_y(0.0, MANA_BAR_Y + 6.5) .color(Some(Color::Rgba(0.1, 0.1, 0.1, 0.8)))
314 .parent(id)
315 .set(state.ids.health_bar_bg, ui);
316
317 let size_factor = (hp_percentage / 100.0) * BARSIZE;
319 let w = if self.in_group {
320 82.0 * size_factor
321 } else {
322 73.0 * size_factor
323 };
324 let h = 6.0 * BARSIZE;
325 let x = if self.in_group {
326 (0.0 + (hp_percentage / 100.0 * 41.0 - 41.0)) * BARSIZE
327 } else {
328 (4.5 + (hp_percentage / 100.0 * 36.45 - 36.45)) * BARSIZE
329 };
330 Image::new(self.imgs.enemy_bar)
331 .w_h(w, h)
332 .x_y(x, MANA_BAR_Y + 8.0)
333 .color(if self.in_group {
334 Some(match hp_percentage {
336 x if (0.0..25.0).contains(&x) => crit_hp_color,
337 x if (25.0..50.0).contains(&x) => LOW_HP_COLOR,
338 _ => HP_COLOR,
339 })
340 } else {
341 Some(ENEMY_HP_COLOR)
342 })
343 .parent(id)
344 .set(state.ids.health_bar, ui);
345
346 if decayed_health > 0.0 {
347 let x_decayed = if self.in_group {
348 (0.0 - (decayed_health * 41.0 - 41.0)) * BARSIZE
349 } else {
350 (4.5 - (decayed_health * 36.45 - 36.45)) * BARSIZE
351 };
352
353 let decay_bar_len = decayed_health
354 * if self.in_group {
355 82.0 * BARSIZE
356 } else {
357 73.0 * BARSIZE
358 };
359 Image::new(self.imgs.enemy_bar)
360 .w_h(decay_bar_len, h)
361 .x_y(x_decayed, MANA_BAR_Y + 8.0)
362 .color(Some(QUALITY_EPIC))
363 .parent(id)
364 .set(state.ids.decay_bar, ui);
365 }
366 let mut txt = format!("{}/{}", health_cur_txt, health_max_txt);
367 if health.is_dead {
368 txt = self.i18n.get_msg("hud-group-dead").to_string()
369 };
370 Text::new(&txt)
371 .mid_top_with_margin_on(state.ids.health_bar_bg, 2.0)
372 .font_size(10)
373 .font_id(self.fonts.cyri.conrod_id)
374 .color(TEXT_COLOR)
375 .parent(id)
376 .set(state.ids.health_txt, ui);
377
378 if let Some(energy) = energy {
380 let energy_factor = f64::from(energy.current() / energy.maximum());
381 let size_factor = energy_factor * BARSIZE;
382 let w = if self.in_group {
383 80.0 * size_factor
384 } else {
385 72.0 * size_factor
386 };
387 let x = if self.in_group {
388 ((0.0 + (energy_factor * 40.0)) - 40.0) * BARSIZE
389 } else {
390 ((3.5 + (energy_factor * 36.5)) - 36.45) * BARSIZE
391 };
392 Rectangle::fill_with([w, MANA_BAR_HEIGHT], STAMINA_COLOR)
393 .x_y(
394 x, MANA_BAR_Y, )
396 .parent(id)
397 .set(state.ids.mana_bar, ui);
398 }
399
400 Image::new(if self.in_group {self.imgs.health_bar_group} else {self.imgs.enemy_health})
402 .w_h(84.0 * BARSIZE, 10.0 * BARSIZE)
403 .x_y(0.0, MANA_BAR_Y + 6.5) .color(Some(Color::Rgba(1.0, 1.0, 1.0, 0.99)))
405 .parent(id)
406 .set(state.ids.health_bar_fg, ui);
407
408 if let Some(combat_rating) = combat_rating {
409 let indicator_col = cr_color(combat_rating);
410 let artifact_diffculty = 122.0;
411
412 if combat_rating > artifact_diffculty && !self.in_group {
413 let skull_ani =
414 ((self.pulse * 0.7).cos() * 0.5 + 0.5) * 10.0; Image::new(if skull_ani as i32 == 1 && rand::random::<f32>() < 0.9 {
416 self.imgs.skull_2
417 } else {
418 self.imgs.skull
419 })
420 .w_h(18.0 * BARSIZE, 18.0 * BARSIZE)
421 .x_y(-39.0 * BARSIZE, MANA_BAR_Y + 7.0)
422 .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
423 .parent(id)
424 .set(state.ids.level_skull, ui);
425 } else {
426 Image::new(if self.in_group {
427 self.imgs.nothing
428 } else {
429 self.imgs.combat_rating_ico
430 })
431 .w_h(7.0 * BARSIZE, 7.0 * BARSIZE)
432 .x_y(-37.0 * BARSIZE, MANA_BAR_Y + 6.0)
433 .color(Some(indicator_col))
434 .parent(id)
435 .set(state.ids.level, ui);
436 }
437 }
438
439 if hardcore {
440 Image::new(self.imgs.hardcore)
441 .w_h(18.0 * BARSIZE, 18.0 * BARSIZE)
442 .x_y(39.0 * BARSIZE, MANA_BAR_Y + 13.0)
443 .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0)))
444 .parent(id)
445 .set(state.ids.hardcore, ui);
446 }
447 },
448 _ => {},
449 }
450
451 if !self.interaction_options.is_empty() {
453 let scale = 30.0;
454 let btn_rect_size = scale * 0.8;
455 let btn_font_size = scale * 0.6;
456 let btn_radius = btn_rect_size / 5.0;
457 let btn_color = Color::Rgba(0.0, 0.0, 0.0, 0.8);
458 let mut max_w = btn_rect_size;
459 let spacing = 8.0;
460
461 let interactions: Vec<(String, String)> = match self
463 .global_state
464 .window
465 .last_input()
466 {
467 LastInput::KeyboardMouse => self
468 .interaction_options
469 .iter()
470 .map(|(input, action)| {
471 match self.global_state.settings.controls.get_binding(*input) {
472 Some(binding) => (binding.display_string(), action.to_string()),
473 None => (icon_utils::UNBOUND_KEY.to_string(), action.to_string()),
474 }
475 })
476 .collect(),
477
478 LastInput::Controller => self
479 .interaction_options
480 .iter()
481 .map(|(input, action)| {
482 let input_str = icon_utils::get_controller_input_string(
483 *input,
484 &self.global_state.settings,
485 self.global_state.window.controller_type(),
486 );
487
488 match input_str {
489 Some(binding) => (binding, action.to_string()),
490 None => (icon_utils::UNBOUND_KEY.to_string(), action.to_string()),
491 }
492 })
493 .collect(),
494 };
495
496 let mut temp_list: Vec<String> = Vec::new();
500 for i in &interactions {
501 let s = i.0.clone();
503 temp_list.push(s);
504 }
505 let icons_input = temp_list.join("\n");
506
507 let mut temp_list: Vec<String> = Vec::new();
509 for i in &interactions {
510 let s = i.1.clone();
511 temp_list.push(s);
512 }
513 let action_input = temp_list.join("\n");
514
515 let anchor_id = self.info.map_or(state.ids.name, |info| {
516 if info.health.is_some_and(should_show_healthbar) {
517 if info.energy.is_some() {
518 state.ids.mana_bar
519 } else {
520 state.ids.health_bar
521 }
522 } else {
523 state.ids.name
524 }
525 });
526
527 let actions_hint = RichText::new(&action_input, self.imgs)
529 .font_id(self.fonts.cyri.conrod_id)
530 .font_size(btn_font_size as u32)
531 .color(TEXT_COLOR)
532 .parent(id)
533 .justify(conrod_core::text::Justify::Left);
534
535 let [actions_w, actions_h] = actions_hint.get_wh(ui).unwrap_or([btn_rect_size; 2]);
536 max_w += actions_w;
537 let max_h = actions_h;
538
539 let inputs_hint = RichText::new(&icons_input, self.imgs)
541 .font_id(self.fonts.cyri.conrod_id)
542 .font_size(btn_font_size as u32)
543 .color(TEXT_COLOR)
544 .parent(id)
545 .justify(conrod_core::text::Justify::Right);
546
547 let [inputs_w, _inputs_h] = inputs_hint.get_wh(ui).unwrap_or([btn_rect_size; 2]);
548 max_w += inputs_w;
549 let box_offset = -(inputs_w + spacing) / 2.0;
550
551 let centering_offset = (inputs_w + spacing) / 2.0;
554
555 actions_hint
556 .down_from(anchor_id, 12.0)
557 .x_relative_to(anchor_id, centering_offset)
558 .depth(1.0)
559 .set(state.ids.interaction_hints_action, ui);
560
561 inputs_hint
562 .left_from(state.ids.interaction_hints_action, spacing)
563 .depth(1.0)
564 .set(state.ids.interaction_hints_input, ui);
565
566 RoundedRectangle::fill_with(
567 [max_w + btn_radius * 2.0, max_h + btn_radius * 2.0],
568 btn_radius,
569 btn_color,
570 )
571 .depth(2.0)
572 .x_relative_to(state.ids.interaction_hints_action, box_offset)
573 .align_middle_y_of(state.ids.interaction_hints_action)
574 .parent(id)
575 .set(state.ids.interaction_hints_bg, ui);
576 }
577 }
578 if let Some(bubble) = self.bubble {
580 let dark_mode = self.global_state.settings.interface.speech_bubble_dark_mode;
581 let bubble_contents: String = self.i18n.get_content(bubble.content());
582 let (text_color, shadow_color) = bubble_color(bubble, dark_mode);
583 let mut text = Text::new(&bubble_contents)
584 .color(text_color)
585 .font_id(self.fonts.cyri.conrod_id)
586 .font_size(18)
587 .up_from(state.ids.name, 26.0)
588 .x_align_to(state.ids.name, Align::Middle)
589 .parent(id);
590
591 if let Some(w) = text.get_w(ui)
592 && w > MAX_BUBBLE_WIDTH
593 {
594 text = text.w(MAX_BUBBLE_WIDTH);
595 }
596 Image::new(if dark_mode {
597 self.imgs.dark_bubble_top_left
598 } else {
599 self.imgs.speech_bubble_top_left
600 })
601 .w_h(16.0, 16.0)
602 .top_left_with_margin_on(state.ids.speech_bubble_text, -20.0)
603 .parent(id)
604 .set(state.ids.speech_bubble_top_left, ui);
605 Image::new(if dark_mode {
606 self.imgs.dark_bubble_top
607 } else {
608 self.imgs.speech_bubble_top
609 })
610 .h(16.0)
611 .padded_w_of(state.ids.speech_bubble_text, -4.0)
612 .mid_top_with_margin_on(state.ids.speech_bubble_text, -20.0)
613 .parent(id)
614 .set(state.ids.speech_bubble_top, ui);
615 Image::new(if dark_mode {
616 self.imgs.dark_bubble_top_right
617 } else {
618 self.imgs.speech_bubble_top_right
619 })
620 .w_h(16.0, 16.0)
621 .top_right_with_margin_on(state.ids.speech_bubble_text, -20.0)
622 .parent(id)
623 .set(state.ids.speech_bubble_top_right, ui);
624 Image::new(if dark_mode {
625 self.imgs.dark_bubble_left
626 } else {
627 self.imgs.speech_bubble_left
628 })
629 .w(16.0)
630 .padded_h_of(state.ids.speech_bubble_text, -4.0)
631 .mid_left_with_margin_on(state.ids.speech_bubble_text, -20.0)
632 .parent(id)
633 .set(state.ids.speech_bubble_left, ui);
634 Image::new(if dark_mode {
635 self.imgs.dark_bubble_mid
636 } else {
637 self.imgs.speech_bubble_mid
638 })
639 .padded_wh_of(state.ids.speech_bubble_text, -4.0)
640 .top_left_with_margin_on(state.ids.speech_bubble_text, -4.0)
641 .parent(id)
642 .set(state.ids.speech_bubble_mid, ui);
643 Image::new(if dark_mode {
644 self.imgs.dark_bubble_right
645 } else {
646 self.imgs.speech_bubble_right
647 })
648 .w(16.0)
649 .padded_h_of(state.ids.speech_bubble_text, -4.0)
650 .mid_right_with_margin_on(state.ids.speech_bubble_text, -20.0)
651 .parent(id)
652 .set(state.ids.speech_bubble_right, ui);
653 Image::new(if dark_mode {
654 self.imgs.dark_bubble_bottom_left
655 } else {
656 self.imgs.speech_bubble_bottom_left
657 })
658 .w_h(16.0, 16.0)
659 .bottom_left_with_margin_on(state.ids.speech_bubble_text, -20.0)
660 .parent(id)
661 .set(state.ids.speech_bubble_bottom_left, ui);
662 Image::new(if dark_mode {
663 self.imgs.dark_bubble_bottom
664 } else {
665 self.imgs.speech_bubble_bottom
666 })
667 .h(16.0)
668 .padded_w_of(state.ids.speech_bubble_text, -4.0)
669 .mid_bottom_with_margin_on(state.ids.speech_bubble_text, -20.0)
670 .parent(id)
671 .set(state.ids.speech_bubble_bottom, ui);
672 Image::new(if dark_mode {
673 self.imgs.dark_bubble_bottom_right
674 } else {
675 self.imgs.speech_bubble_bottom_right
676 })
677 .w_h(16.0, 16.0)
678 .bottom_right_with_margin_on(state.ids.speech_bubble_text, -20.0)
679 .parent(id)
680 .set(state.ids.speech_bubble_bottom_right, ui);
681 let tail = Image::new(if dark_mode {
682 self.imgs.dark_bubble_tail
683 } else {
684 self.imgs.speech_bubble_tail
685 })
686 .parent(id)
687 .mid_bottom_with_margin_on(state.ids.speech_bubble_text, -32.0);
688
689 if dark_mode {
690 tail.w_h(22.0, 13.0)
691 } else {
692 tail.w_h(22.0, 28.0)
693 }
694 .set(state.ids.speech_bubble_tail, ui);
695
696 let mut text_shadow = Text::new(&bubble_contents)
697 .color(shadow_color)
698 .font_id(self.fonts.cyri.conrod_id)
699 .font_size(18)
700 .x_relative_to(state.ids.speech_bubble_text, 1.0)
701 .y_relative_to(state.ids.speech_bubble_text, -1.0)
702 .parent(id);
703 text.depth(text_shadow.get_depth() - 1.0)
705 .set(state.ids.speech_bubble_text, ui);
706 if let Some(w) = text_shadow.get_w(ui)
707 && w > MAX_BUBBLE_WIDTH
708 {
709 text_shadow = text_shadow.w(MAX_BUBBLE_WIDTH);
710 }
711 text_shadow.set(state.ids.speech_bubble_shadow, ui);
712 let icon = if self.global_state.settings.interface.speech_bubble_icon {
713 bubble_icon(bubble, self.imgs)
714 } else {
715 self.imgs.nothing
716 };
717 Image::new(icon)
718 .w_h(16.0, 16.0)
719 .top_left_with_margin_on(state.ids.speech_bubble_text, -16.0)
720 .set(state.ids.speech_bubble_icon, ui);
723 }
724 }
725}
726
727fn bubble_color(bubble: &SpeechBubble, dark_mode: bool) -> (Color, Color) {
728 let light_color = match bubble.icon {
729 SpeechBubbleType::Tell => TELL_COLOR,
730 SpeechBubbleType::Say => SAY_COLOR,
731 SpeechBubbleType::Region => REGION_COLOR,
732 SpeechBubbleType::Group => GROUP_COLOR,
733 SpeechBubbleType::Faction => FACTION_COLOR,
734 SpeechBubbleType::World
735 | SpeechBubbleType::Quest
736 | SpeechBubbleType::Trade
737 | SpeechBubbleType::None => TEXT_COLOR,
738 };
739 if dark_mode {
740 (light_color, TEXT_BG)
741 } else {
742 (TEXT_BG, light_color)
743 }
744}
745
746fn bubble_icon(sb: &SpeechBubble, imgs: &Imgs) -> conrod_core::image::Id {
747 match sb.icon {
748 SpeechBubbleType::Tell => imgs.chat_tell_small,
750 SpeechBubbleType::Say => imgs.chat_say_small,
751 SpeechBubbleType::Region => imgs.chat_region_small,
752 SpeechBubbleType::Group => imgs.chat_group_small,
753 SpeechBubbleType::Faction => imgs.chat_faction_small,
754 SpeechBubbleType::World => imgs.chat_world_small,
755 SpeechBubbleType::Quest => imgs.nothing, SpeechBubbleType::Trade => imgs.nothing, SpeechBubbleType::None => imgs.nothing, }
759}