// A small immediate-mode interface for the editors, drawn with the game's font and frames. // Each frame: begin() reads the input, the screens call the widgets (which draw themselves and // report clicks), finish() draws an open dropdown list and the tooltip on top. use crate::gfx::*; use crate::ui::*; use macroquad::prelude::*; #[allow(unused_imports)] use crate::ui::{BLUE, GOLD, GREEN, RED}; pub const PANEL: Color = Color::new(0.07, 0.05, 0.06, 0.96); pub const FIELD: Color = Color::new(0.12, 0.09, 0.08, 1.0); pub const HOT: Color = Color::new(0.35, 0.22, 0.1, 1.0); struct Popup { id: u64, rect: Rect, // where the list is drawn options: Vec, scroll: usize, rows: usize, } #[derive(Default)] pub struct Im { pub mx: f32, pub my: f32, pub pressed: bool, // left button went down this frame (and no popup took it) pub released: bool, // left button went up this frame pub down: bool, pub rpressed: bool, pub wheel: f32, pub chars: Vec, pub ctrl: bool, pub shift: bool, pub focus: Option, // the text field that gets the typing pub caret: usize, // caret position (characters) in the focused field popup: Option, picked: Option<(u64, usize)>, // a dropdown choice made this frame tip: Option, pub typing: bool, // a text field has the focus (keyboard shortcuts are off) } // a stable id from a name and an index pub fn id(name: &str, k: usize) -> u64 { let mut h: u64 = 1469598103934665603; for b in name.bytes() { h = (h ^ b as u64).wrapping_mul(1099511628211); } (h ^ k as u64).wrapping_mul(1099511628211) } impl Im { pub fn begin(&mut self) { let (mx, my) = mouse(); self.mx = mx; self.my = my; self.pressed = is_mouse_button_pressed(MouseButton::Left); self.released = is_mouse_button_released(MouseButton::Left); self.down = is_mouse_button_down(MouseButton::Left); self.rpressed = is_mouse_button_pressed(MouseButton::Right); self.wheel = mouse_wheel().1; self.ctrl = is_key_down(KeyCode::LeftControl) || is_key_down(KeyCode::RightControl) || is_key_down(KeyCode::LeftSuper); self.shift = is_key_down(KeyCode::LeftShift) || is_key_down(KeyCode::RightShift); self.chars.clear(); while let Some(c) = get_char_pressed() { if !c.is_control() { self.chars.push(c); } } self.picked = None; self.tip = None; self.typing = self.focus.is_some(); // an open dropdown takes clicks and the wheel if let Some(p) = self.popup.as_mut() { let row_h = CH() * 1.5; if self.wheel != 0.0 && p.rect.contains(vec2(mx, my)) { let d = if self.wheel > 0.0 { -1 } else { 1 }; p.scroll = (p.scroll as i32 + d).clamp(0, p.options.len().saturating_sub(p.rows) as i32) as usize; self.wheel = 0.0; } if self.pressed { if p.rect.contains(vec2(mx, my)) { let k = p.scroll + ((my - p.rect.y) / row_h).floor() as usize; if k < p.options.len() { self.picked = Some((p.id, k)); } } self.popup = None; self.pressed = false; } } // a click elsewhere ends the typing if self.pressed { self.focus = None; } } pub fn over(&self, r: Rect) -> bool { r.contains(vec2(self.mx, self.my)) } pub fn popup_open(&self) -> bool { self.popup.is_some() } // a click on r (it is taken: later widgets do not see it) pub fn clicked(&mut self, r: Rect) -> bool { if self.pressed && self.over(r) { self.pressed = false; true } else { false } } pub fn tip(&mut self, r: Rect, text: &str) { if self.over(r) && !text.is_empty() { self.tip = Some(text.to_string()); } } pub fn button(&mut self, gfx: &Gfx, r: Rect, label: &str) -> bool { self.button_c(gfx, r, label, FRAME) } pub fn button_c(&mut self, gfx: &Gfx, r: Rect, label: &str, accent: Color) -> bool { let hot = self.over(r) && !self.popup_open(); draw_rectangle(r.x, r.y, r.w, r.h, if hot { HOT } else { Color::new(0.13, 0.09, 0.08, 1.0) }); draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0, if hot { FRAME_HI } else { accent }); let s = S(); let maxc = ((r.w / (GW * s)) as usize).saturating_sub(1).max(1); let label: String = label.chars().take(maxc).collect(); gfx.text(&label, (r.x + (r.w - gfx.measure(&label, s)) * 0.5).floor(), (r.y + (r.h - GH * s) * 0.5).floor(), s, if hot { FRAME_HI } else { TXT }); self.clicked(r) } // a selectable entry (list rows, tabs): highlighted when selected pub fn select(&mut self, gfx: &Gfx, r: Rect, label: &str, selected: bool, col: Color) -> bool { let hot = self.over(r) && !self.popup_open(); if selected || hot { draw_rectangle(r.x, r.y, r.w, r.h, if hot { HOT } else { Color::new(0.22, 0.15, 0.09, 1.0) }); } let maxc = ((r.w / CW()) as usize).saturating_sub(1).max(1); let label: String = label.chars().take(maxc).collect(); gfx.text(&label, r.x + CW() * 0.5, (r.y + (r.h - CH()) * 0.5).floor(), S(), if selected { FRAME_HI } else { col }); self.clicked(r) } // [x] label; returns true when it was flipped pub fn toggle(&mut self, gfx: &Gfx, x: f32, y: f32, label: &str, v: &mut bool) -> bool { let w = gfx.measure(label, S()) + CW() * 3.0; let r = Rect::new(x, y, w, CH() * 1.5); let hot = self.over(r) && !self.popup_open(); let bx = Rect::new(x, y + CH() * 0.1, CW() * 1.6, CH() * 1.3); draw_rectangle(bx.x, bx.y, bx.w, bx.h, FIELD); draw_rectangle_lines(bx.x, bx.y, bx.w, bx.h, 2.0, if hot { FRAME_HI } else { FRAME }); if *v { gfx.glyph('√', bx.x + CW() * 0.3, bx.y + CH() * 0.15, S(), GOLD); } gfx.text(label, x + CW() * 2.2, y + CH() * 0.25, S(), if hot { FRAME_HI } else { TXT }); if self.clicked(r) { *v = !*v; true } else { false } } // < value >: -1, +1 or 0 (shift: steps of 10) pub fn stepper(&mut self, gfx: &Gfx, r: Rect, value: &str) -> i32 { let bw = CW() * 2.0; let l = Rect::new(r.x, r.y, bw, r.h); let rr = Rect::new(r.x + r.w - bw, r.y, bw, r.h); draw_rectangle(r.x, r.y, r.w, r.h, FIELD); let mut d = 0; if self.button(gfx, l, "<") { d = -1; } if self.button(gfx, rr, ">") { d = 1; } let v: String = value.chars().take(((r.w - bw * 2.0) / CW()) as usize).collect(); gfx.text(&v, (r.x + (r.w - gfx.measure(&v, S())) * 0.5).floor(), (r.y + (r.h - CH()) * 0.5).floor(), S(), GOLD); if self.over(r) && self.wheel != 0.0 && !self.popup_open() { d = if self.wheel > 0.0 { 1 } else { -1 }; self.wheel = 0.0; } if self.shift { d * 10 } else { d } } // one line of text; returns true when it changed pub fn text_field(&mut self, gfx: &Gfx, fid: u64, r: Rect, text: &mut String, max: usize) -> bool { let focused = self.focus == Some(fid); draw_rectangle(r.x, r.y, r.w, r.h, FIELD); draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0, if focused { FRAME_HI } else if self.over(r) { FRAME } else { Color::new(0.3, 0.24, 0.18, 1.0) }); if self.clicked(r) { self.focus = Some(fid); self.caret = text.chars().count(); } let mut changed = false; if self.focus == Some(fid) { changed = self.edit(text, max, false); } let cols = ((r.w - CW()) / CW()).max(1.0) as usize; let n = text.chars().count(); let first = if self.focus == Some(fid) { self.caret.saturating_sub(cols.saturating_sub(1)) } else { 0 }; let shown: String = text.chars().skip(first).take(cols).collect(); let ty = (r.y + (r.h - CH()) * 0.5).floor(); gfx.text(&shown, r.x + CW() * 0.5, ty, S(), TXT); if self.focus == Some(fid) && (get_time() * 2.0) as i64 % 2 == 0 { let cx = r.x + CW() * 0.5 + (self.caret.min(n) - first) as f32 * CW(); draw_rectangle(cx, ty, (S()).max(2.0), CH(), FRAME_HI); } changed } // several lines (wrapped); Enter starts a new line pub fn text_area(&mut self, gfx: &Gfx, fid: u64, r: Rect, text: &mut String, max: usize) -> bool { let focused = self.focus == Some(fid); draw_rectangle(r.x, r.y, r.w, r.h, FIELD); draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0, if focused { FRAME_HI } else { Color::new(0.3, 0.24, 0.18, 1.0) }); if self.clicked(r) { self.focus = Some(fid); self.caret = text.chars().count(); } let mut changed = false; if self.focus == Some(fid) { changed = self.edit(text, max, true); } let cols = ((r.w - CW()) / CW()).max(4.0) as usize; let rows = ((r.h - CH() * 0.5) / CH()).max(1.0) as usize; // wrap paragraph by paragraph; the caret is shown at the end of the text let mut lines: Vec = Vec::new(); for para in text.split('\n') { let w = wrap(para, cols); if w.is_empty() { lines.push(String::new()); } else { lines.extend(w); } } let first = lines.len().saturating_sub(rows); for (k, l) in lines.iter().skip(first).take(rows).enumerate() { gfx.text(l, r.x + CW() * 0.5, r.y + CH() * 0.25 + k as f32 * CH(), S(), TXT); } if self.focus == Some(fid) && (get_time() * 2.0) as i64 % 2 == 0 { let last = lines.last().map_or(0, |l| l.chars().count()); let k = lines.len().saturating_sub(1) - first; draw_rectangle(r.x + CW() * 0.5 + last as f32 * CW(), r.y + CH() * 0.25 + k as f32 * CH(), S().max(2.0), CH(), FRAME_HI); } changed } // typing into the focused text fn edit(&mut self, text: &mut String, max: usize, multiline: bool) -> bool { let mut chars: Vec = text.chars().collect(); let mut changed = false; self.caret = self.caret.min(chars.len()); if multiline { self.caret = chars.len(); } for &c in self.chars.iter() { if chars.len() < max { chars.insert(self.caret, c); self.caret += 1; changed = true; } } if is_key_pressed(KeyCode::Backspace) && self.caret > 0 { chars.remove(self.caret - 1); self.caret -= 1; changed = true; } if is_key_pressed(KeyCode::Delete) && self.caret < chars.len() { chars.remove(self.caret); changed = true; } if !multiline { if is_key_pressed(KeyCode::Left) { self.caret = self.caret.saturating_sub(1); } if is_key_pressed(KeyCode::Right) { self.caret = (self.caret + 1).min(chars.len()); } if is_key_pressed(KeyCode::Home) { self.caret = 0; } if is_key_pressed(KeyCode::End) { self.caret = chars.len(); } } if is_key_pressed(KeyCode::Enter) || is_key_pressed(KeyCode::KpEnter) { if multiline && chars.len() < max { chars.push('\n'); self.caret = chars.len(); changed = true; } else if !multiline { self.focus = None; } } if is_key_pressed(KeyCode::Escape) { self.focus = None; } if changed { *text = chars.into_iter().collect(); } changed } // a dropdown field; Some(k) when an option was picked pub fn dropdown(&mut self, gfx: &Gfx, did: u64, r: Rect, current: &str, options: &[String]) -> Option { let open = self.popup.as_ref().map_or(false, |p| p.id == did); let hot = self.over(r) && !self.popup_open() || open; draw_rectangle(r.x, r.y, r.w, r.h, if hot { HOT } else { FIELD }); draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0, if hot { FRAME_HI } else { FRAME }); let maxc = ((r.w / CW()) as usize).saturating_sub(3).max(1); let cur: String = current.chars().take(maxc).collect(); gfx.text(&cur, r.x + CW() * 0.5, (r.y + (r.h - CH()) * 0.5).floor(), S(), if hot { FRAME_HI } else { TXT }); gfx.glyph('▼', r.x + r.w - CW() * 1.5, (r.y + (r.h - CH()) * 0.5).floor(), S(), DIM); if self.clicked(r) && !options.is_empty() { let row_h = CH() * 1.5; let w = r.w.max(options.iter().map(|o| gfx.measure(o, S())).fold(0.0, f32::max) + CW() * 2.0); let below = ((sh() - r.y - r.h - CH()) / row_h).floor().max(0.0) as usize; let above = ((r.y - CH()) / row_h).floor().max(0.0) as usize; let down = below >= options.len().min(8) || below >= above; let rows = options.len().min(if down { below } else { above }).clamp(1, 16); let y = if down { r.y + r.h } else { r.y - rows as f32 * row_h }; let cur_k = options.iter().position(|o| o == current).unwrap_or(0); self.popup = Some(Popup { id: did, rect: Rect::new(r.x.min(sw() - w).max(0.0), y, w, rows as f32 * row_h), options: options.to_vec(), scroll: cur_k.saturating_sub(rows / 2).min(options.len() - rows), rows }); } match self.picked { Some((p, k)) if p == did => Some(k), _ => None, } } // draws what lies on top: the open dropdown list, the tooltip pub fn finish(&mut self, gfx: &Gfx) { if let Some(p) = self.popup.as_ref() { let row_h = CH() * 1.5; draw_rectangle(p.rect.x, p.rect.y, p.rect.w, p.rect.h, Color::new(0.05, 0.035, 0.04, 0.99)); for (k, o) in p.options.iter().enumerate().skip(p.scroll).take(p.rows) { let r = Rect::new(p.rect.x, p.rect.y + (k - p.scroll) as f32 * row_h, p.rect.w, row_h); if self.over(r) { draw_rectangle(r.x, r.y, r.w, r.h, HOT); } gfx.text(o, r.x + CW() * 0.5, (r.y + (row_h - CH()) * 0.5).floor(), S(), if self.over(r) { FRAME_HI } else { TXT }); } if p.scroll > 0 { gfx.glyph('▲', p.rect.x + p.rect.w - CW() * 1.3, p.rect.y, S(), DIM); } if p.scroll + p.rows < p.options.len() { gfx.glyph('▼', p.rect.x + p.rect.w - CW() * 1.3, p.rect.y + p.rect.h - CH(), S(), DIM); } draw_rectangle_lines(p.rect.x, p.rect.y, p.rect.w, p.rect.h, 2.0, FRAME_HI); } if let Some(t) = self.tip.take() { let lines = wrap(&t, 46); let w = lines.iter().map(|l| gfx.measure(l, SMALL())).fold(0.0, f32::max) + CW() * 2.0; let h = lines.len() as f32 * GH * SMALL() + CH(); let x = (self.mx + CW() * 1.5).min(sw() - w - 2.0); let y = (self.my + CH() * 1.5).min(sh() - h - 2.0); draw_rectangle(x, y, w, h, Color::new(0.04, 0.03, 0.03, 0.97)); draw_rectangle_lines(x, y, w, h, 2.0, FRAME); for (k, l) in lines.iter().enumerate() { gfx.text(l, x + CW(), y + CH() * 0.5 + k as f32 * GH * SMALL(), SMALL(), TXT); } } } } // a framed panel with an optional title pub fn panel(gfx: &Gfx, r: Rect, title: &str) { draw_rectangle(r.x, r.y, r.w, r.h, PANEL); draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0, FRAME); if !title.is_empty() { gfx.text(title, r.x + CW(), r.y + CH() * 0.4, S(), FRAME_HI); } } pub fn label(gfx: &Gfx, x: f32, y: f32, text: &str, col: Color) { gfx.text(text, x, y, S(), col); }