diff --git a/assets/ui/icons/roadtypes_avenue.png b/assets/ui/icons/roadtypes_avenue.png new file mode 100644 index 00000000..c61e9007 --- /dev/null +++ b/assets/ui/icons/roadtypes_avenue.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3acddc033751fe516177101dcad36ab106d058d298f37841cdf06033bf21a8da +size 746 diff --git a/assets/ui/icons/roadtypes_avenue_1way.png b/assets/ui/icons/roadtypes_avenue_1way.png new file mode 100644 index 00000000..c7747190 --- /dev/null +++ b/assets/ui/icons/roadtypes_avenue_1way.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5c921519a5f12caec76e57bcec16a294395a68211d3f96316df7baf86132b360 +size 1408 diff --git a/assets/ui/icons/roadtypes_drive.png b/assets/ui/icons/roadtypes_drive.png new file mode 100644 index 00000000..17204866 --- /dev/null +++ b/assets/ui/icons/roadtypes_drive.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:083375ccc5609569c09bb1e0dbdfbce66c3831893b314ac9d57cb5baa4195c56 +size 404 diff --git a/assets/ui/icons/roadtypes_drive_1way.png b/assets/ui/icons/roadtypes_drive_1way.png new file mode 100644 index 00000000..e11ad524 --- /dev/null +++ b/assets/ui/icons/roadtypes_drive_1way.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:886024802a7355cd1f12bcb329d1fd6746c1dd85377afb3b50100dfac0ab3d92 +size 956 diff --git a/assets/ui/icons/roadtypes_highway.png b/assets/ui/icons/roadtypes_highway.png new file mode 100644 index 00000000..c6c3b960 --- /dev/null +++ b/assets/ui/icons/roadtypes_highway.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c90116ea3ff67051354e712f99039384563238c8714503966102b0c4596a7574 +size 717 diff --git a/assets/ui/icons/roadtypes_highway_1way.png b/assets/ui/icons/roadtypes_highway_1way.png new file mode 100644 index 00000000..ccac1207 --- /dev/null +++ b/assets/ui/icons/roadtypes_highway_1way.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4490f865037b59a0273dba04ef6caf92441db49e2078d2410553cc7dceebb754 +size 1594 diff --git a/assets/ui/icons/roadtypes_rail.png b/assets/ui/icons/roadtypes_rail.png new file mode 100644 index 00000000..04b6ba6c --- /dev/null +++ b/assets/ui/icons/roadtypes_rail.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4cbb89a11cdef8ce28270636ed466df478988fad1701379d0e5074f697b6905c +size 8573 diff --git a/assets/ui/icons/roadtypes_rail_1way.png b/assets/ui/icons/roadtypes_rail_1way.png new file mode 100644 index 00000000..3de3da58 --- /dev/null +++ b/assets/ui/icons/roadtypes_rail_1way.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a170267e7d3d82e930ee924dbf3a5b156bb3ec2ba6ac8b9fad6c57d1a9f32a1 +size 8693 diff --git a/assets/ui/icons/roadtypes_street.png b/assets/ui/icons/roadtypes_street.png new file mode 100644 index 00000000..2a95994e --- /dev/null +++ b/assets/ui/icons/roadtypes_street.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:914b29a6895e7c706d31df2cb355613794a8ff9a466f1be4983ce5ab52d8f037 +size 633 diff --git a/assets/ui/icons/roadtypes_street_1way.png b/assets/ui/icons/roadtypes_street_1way.png new file mode 100644 index 00000000..a4bede70 --- /dev/null +++ b/assets/ui/icons/roadtypes_street_1way.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:88a98c44aeba28412b94b4f112bfa581356bdd21cfa2c60714b3f6823170223a +size 1193 diff --git a/assets/ui/icons/snap_grid.png b/assets/ui/icons/snap_grid.png new file mode 100644 index 00000000..f1f6c39b --- /dev/null +++ b/assets/ui/icons/snap_grid.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ab60803afb759c1e5c1ff1463982ec6502a05bd1d75065156526c1e654f74ab5 +size 1157 diff --git a/assets_gui/src/yakui_gui.rs b/assets_gui/src/yakui_gui.rs index 7ff37c5a..b8535b42 100644 --- a/assets_gui/src/yakui_gui.rs +++ b/assets_gui/src/yakui_gui.rs @@ -10,9 +10,9 @@ use engine::{set_cursor_icon, CursorIcon, Drawable, GfxContext, InstancedMesh, M use geom::Matrix4; use goryak::{ background, button_primary, checkbox_value, divider, dragvalue, icon, interact_box_radius, - is_hovered, labelc, on_secondary_container, on_surface, outline_variant, round_rect, - scroll_vertical, secondary_container, set_theme, surface, surface_variant, use_changed, - CountGrid, RoundRect, Theme, + is_hovered, on_secondary_container, on_surface, outline_variant, round_rect, scroll_vertical, + secondary_container, set_theme, surface, surface_variant, textc, use_changed, CountGrid, + RoundRect, Theme, }; use prototypes::{prototypes_iter, GoodsCompanyID, GoodsCompanyPrototype}; @@ -149,7 +149,7 @@ impl State { let triangle = if v { "caret-down" } else { "caret-right" }; icon(on_surface(), triangle); } - labelc(on_surface(), name); + textc(on_surface(), name); }); }); }, @@ -162,13 +162,13 @@ impl State { fn model_properties(&mut self) { Self::model_properties_container(|| { let tc = on_secondary_container(); // text color - labelc(tc, "Model properties"); + textc(tc, "Model properties"); match self.gui.shown { Shown::None => { - labelc(tc, "No model selected"); + textc(tc, "No model selected"); } Shown::Error(ref e) => { - labelc(tc, e.clone()); + textc(tc, e.clone()); } Shown::Model((ref mesh, _, ref mut props)) => { let params = use_state(|| LodGenerateParams { @@ -185,13 +185,13 @@ impl State { .main_axis_size(MainAxisSize::Min) .show(|| { params.modify(|mut params| { - labelc(tc, "n_lods"); + textc(tc, "n_lods"); dragvalue().min(1.0).max(4.0).show(&mut params.n_lods); - labelc(tc, "quality"); + textc(tc, "quality"); dragvalue().min(0.0).max(1.0).show(&mut params.quality); - labelc(tc, "sloppy"); + textc(tc, "sloppy"); checkbox_value(&mut params.sloppy); params @@ -208,35 +208,35 @@ impl State { let mut lod_details = CountGrid::col(1 + mesh.lods.len()); lod_details.main_axis_size = MainAxisSize::Min; lod_details.show(|| { - labelc(tc, ""); + textc(tc, ""); for (i, _) in mesh.lods.iter().enumerate() { - labelc(tc, format!("LOD{}", i)); + textc(tc, format!("LOD{}", i)); } - labelc(tc, "Vertices"); + textc(tc, "Vertices"); for lod in &*mesh.lods { - labelc(tc, format!("{}", lod.n_vertices)); + textc(tc, format!("{}", lod.n_vertices)); } - labelc(tc, "Triangles"); + textc(tc, "Triangles"); for lod in &*mesh.lods { - labelc(tc, format!("{}", lod.n_indices / 3)); + textc(tc, format!("{}", lod.n_indices / 3)); } - labelc(tc, "Draw calls"); + textc(tc, "Draw calls"); for lod in &*mesh.lods { - labelc(tc, format!("{}", lod.primitives.len())); + textc(tc, format!("{}", lod.primitives.len())); } - labelc(tc, "Coverage"); + textc(tc, "Coverage"); for lod in &*mesh.lods { - labelc(tc, format!("{:.3}", lod.screen_coverage)); + textc(tc, format!("{:.3}", lod.screen_coverage)); } }); }); } Shown::Sprite(ref _sprite) => { - labelc(tc, "Sprite"); + textc(tc, "Sprite"); } } }); diff --git a/goryak/src/dragvalue.rs b/goryak/src/dragvalue.rs index 99134d6b..03f8f133 100644 --- a/goryak/src/dragvalue.rs +++ b/goryak/src/dragvalue.rs @@ -4,7 +4,7 @@ use yakui_widgets::widgets::{List, Pad}; use yakui_widgets::{draggable, pad, use_state}; use crate::roundrect::RoundRect; -use crate::{labelc, on_primary, outline, secondary}; +use crate::{on_primary, outline, secondary, textc}; pub trait Draggable: Copy { const DEFAULT_STEP: f64; @@ -95,7 +95,7 @@ impl DragValue { .color(secondary()) .show_children(|| { pad(Pad::horizontal(10.0), || { - labelc(on_primary(), format!("{:.3}", T::to_f64(*value))); + textc(on_primary(), format!("{:.3}", T::to_f64(*value))); }); }); }); diff --git a/goryak/src/icon.rs b/goryak/src/icon.rs index a2155c1d..a6a263dc 100644 --- a/goryak/src/icon.rs +++ b/goryak/src/icon.rs @@ -1,7 +1,8 @@ use phf::phf_map; +use std::borrow::Cow; use yakui_core::geometry::{Color, Constraints, Vec2}; use yakui_widgets::font::FontName; -use yakui_widgets::widgets::Text; +use yakui_widgets::widgets::{Button, Text}; use yakui_widgets::{center, constrained}; pub fn icon_map(name: &str) -> (&'static str, FontName) { @@ -9,6 +10,18 @@ pub fn icon_map(name: &str) -> (&'static str, FontName) { (mapped, FontName::new("icons")) } +#[must_use = "call show() to show the widget"] +pub fn icon_button(mut b: Button) -> Button { + let (mapped, name) = icon_map(&b.text); + + b.text = Cow::Borrowed(mapped); + b.style.text.font = name.clone(); + b.hover_style.text.font = name.clone(); + b.down_style.text.font = name; + + b +} + pub fn icon(c: Color, name: &str) { let (mapped, fontname) = icon_map(name); let mut t = Text::new(20.0, mapped); diff --git a/goryak/src/imagebutton.rs b/goryak/src/imagebutton.rs index 9685bcf5..9043cbd8 100644 --- a/goryak/src/imagebutton.rs +++ b/goryak/src/imagebutton.rs @@ -1,9 +1,14 @@ +use std::time::Instant; + use yakui_core::event::{EventInterest, EventResponse, WidgetEvent}; -use yakui_core::geometry::{Color, Constraints, Rect, Vec2}; +use yakui_core::geometry::{Color, Constraints, Dim2, Rect, Vec2}; use yakui_core::input::MouseButton; use yakui_core::paint::PaintRect; use yakui_core::widget::{EventContext, LayoutContext, PaintContext, Widget}; -use yakui_core::{Response, TextureId}; +use yakui_core::{Alignment, Response, TextureId}; +use yakui_widgets::{offset, reflow}; + +use crate::{on_primary, padxy, primary, round_rect, textc}; /** A button based on an image @@ -17,6 +22,7 @@ pub struct ImageButton { pub color: Color, pub hover_color: Color, pub active_color: Color, + pub tooltip: &'static str, } impl ImageButton { @@ -27,6 +33,7 @@ impl ImageButton { color: Color::WHITE, hover_color: Color::WHITE, active_color: Color::WHITE, + tooltip: "", } } @@ -43,6 +50,7 @@ impl ImageButton { color, hover_color, active_color, + tooltip: "", } } @@ -57,6 +65,7 @@ pub fn image_button( color: Color, hover_color: Color, active_color: Color, + tooltip: &'static str, ) -> Response { ImageButton { texture: Some(texture), @@ -64,6 +73,7 @@ pub fn image_button( color, hover_color, active_color, + tooltip, } .show() } @@ -72,6 +82,8 @@ pub fn image_button( pub struct ImageButtonWidget { props: ImageButton, resp: ImageButtonResponse, + stopped_moving: Option, + show_tooltip: bool, } #[derive(Copy, Clone, Debug, Default)] @@ -90,6 +102,8 @@ impl Widget for ImageButtonWidget { Self { props: ImageButton::empty(), resp: ImageButtonResponse::default(), + stopped_moving: None, + show_tooltip: false, } } @@ -98,6 +112,26 @@ impl Widget for ImageButtonWidget { let resp = self.resp; self.resp.mouse_entered = false; self.resp.clicked = false; + + if !self.props.tooltip.is_empty() { + if let Some(i) = self.stopped_moving { + if i.elapsed().as_millis() > 500 { + self.show_tooltip = true; + } + if self.show_tooltip { + reflow(Alignment::TOP_LEFT, Dim2::pixels(0.0, 0.0), || { + offset(Vec2::new(-10.0, -50.0), || { + round_rect(5.0, primary(), || { + padxy(5.0, 4.0, || { + textc(on_primary(), self.props.tooltip); + }); + }); + }); + }); + } + } + } + resp } @@ -130,11 +164,20 @@ impl Widget for ImageButtonWidget { } fn layout(&self, _ctx: LayoutContext<'_>, input: Constraints) -> Vec2 { + let _ = self.default_layout(_ctx, input); // tooltip is reflowed + input.constrain_min(self.props.size) } fn event(&mut self, _: EventContext<'_>, event: &WidgetEvent) -> EventResponse { - match event { + match *event { + WidgetEvent::MouseMoved(Some(_)) => { + if self.resp.hovering { + self.stopped_moving = Some(Instant::now()); + } + self.show_tooltip = false; + EventResponse::Bubble + } WidgetEvent::MouseEnter => { self.resp.mouse_entered = true; self.resp.hovering = true; @@ -142,6 +185,8 @@ impl Widget for ImageButtonWidget { } WidgetEvent::MouseLeave => { self.resp.hovering = false; + self.show_tooltip = false; + self.stopped_moving = None; EventResponse::Bubble } WidgetEvent::MouseButtonChanged { @@ -150,7 +195,7 @@ impl Widget for ImageButtonWidget { inside, .. } => { - if *down && *inside { + if down && inside { self.resp.clicked = true; self.resp.mouse_down = true; } else { diff --git a/goryak/src/lib.rs b/goryak/src/lib.rs index 4b9ee861..bfdcfa29 100644 --- a/goryak/src/lib.rs +++ b/goryak/src/lib.rs @@ -13,6 +13,7 @@ mod roundrect; mod scroll; mod text; mod theme; +mod tooltip; mod util; pub use blur_bg::*; diff --git a/goryak/src/text.rs b/goryak/src/text.rs index f8de5e06..36dbcab3 100644 --- a/goryak/src/text.rs +++ b/goryak/src/text.rs @@ -1,5 +1,6 @@ use crate::DEFAULT_FONT_SIZE; use std::borrow::Cow; +use yakui_core::geometry::Color; use yakui_core::Response; use yakui_widgets::font::FontName; use yakui_widgets::widgets::{Text, TextResponse}; @@ -8,8 +9,9 @@ pub fn text>>(text: S) -> Response { Text::new(DEFAULT_FONT_SIZE, text.into()).show() } -pub fn monospace>>(text: S) -> Response { +pub fn monospace>>(col: Color, text: S) -> Response { let mut t = Text::new(DEFAULT_FONT_SIZE, text.into()); t.style.font = FontName::new("monospace"); + t.style.color = col; t.show() } diff --git a/goryak/src/tooltip.rs b/goryak/src/tooltip.rs new file mode 100644 index 00000000..68b219d1 --- /dev/null +++ b/goryak/src/tooltip.rs @@ -0,0 +1,76 @@ +/* +use lazy_static::lazy_static; +use std::sync::Mutex; +use yakui_core::geometry::{Constraints, Vec2}; +use yakui_core::widget::{LayoutContext, Widget}; +use yakui_core::Response; +use yakui_widgets::util::widget_children; + +struct TooltipState(Option); + +struct TooltipStateInner { + tooltip: &'static str, + position: Vec2, +} + +lazy_static! { + static ref TOOLTIP_STATE: Mutex = Mutex::new(TooltipState(None)); +} + +/// Call this at the end of the gui where the "reflow" refers to the root widget +/// so that we can put the tooltip at the right place but the tooltip is necessarily +/// above everything +pub fn render_tooltip() { + let mut tooltip_state = TOOLTIP_STATE.lock().unwrap(); + if let Some(tooltip_state_inner) = tooltip_state.0 {} +} + +/// Positions the children of this widget as close to the mouse as possible while staying +/// within the bounds of the window. +#[derive(Debug)] +struct PositionTooltip { + position: Vec2, +} + +impl PositionTooltip { + pub fn new(position: Vec2) -> Self { + Self { position } + } + + pub fn show(self, children: F) -> Response<()> { + widget_children::(children, self) + } +} + +#[derive(Debug)] +struct PositionTooltipWidget { + props: PositionTooltip, +} + +impl Widget for PositionTooltipWidget { + type Props<'a> = PositionTooltip; + type Response = (); + + fn new() -> Self { + Self { + props: PositionTooltip::new(Vec2::ZERO), + } + } + + fn update(&mut self, props: Self::Props<'_>) -> Self::Response { + self.props = props; + } + + fn layout(&self, mut ctx: LayoutContext<'_>, constraints: Constraints) -> Vec2 { + let node = ctx.dom.get_current(); + let mut size = Vec2::ZERO; + for &child in &node.children { + let child_size = ctx.calculate_layout(child, constraints); + size = size.max(child_size); + } + let v = ctx.layout.viewport(); + + Vec2::ZERO + } +} + */ diff --git a/goryak/src/util.rs b/goryak/src/util.rs index db8a4c44..741e330a 100644 --- a/goryak/src/util.rs +++ b/goryak/src/util.rs @@ -37,9 +37,10 @@ pub fn padx(x: f32, children: impl FnOnce()) -> Response { Pad::horizontal(x).show(children) } -pub fn labelc(c: Color, text: impl Into>) { +pub fn textc(c: Color, text: impl Into>) { let mut t = Text::label(text.into()); t.style.color = c; + t.padding = Pad::all(0.0); t.show(); } @@ -87,6 +88,7 @@ where r } +#[must_use = "call show() to show the widget"] pub fn button_primary(text: impl Into) -> Button { let mut b = Button::styled(text.into()); b.style.fill = primary(); @@ -98,6 +100,7 @@ pub fn button_primary(text: impl Into) -> Button { b } +#[must_use = "call show() to show the widget"] pub fn button_secondary(text: impl Into) -> Button { let mut b = Button::styled(text.into()); b.style.fill = secondary(); diff --git a/native_app/src/gui/topgui.rs b/native_app/src/gui/hud.rs similarity index 99% rename from native_app/src/gui/topgui.rs rename to native_app/src/gui/hud.rs index 62036b9e..78591c9b 100644 --- a/native_app/src/gui/topgui.rs +++ b/native_app/src/gui/hud.rs @@ -71,7 +71,7 @@ impl Gui { /// Root GUI entrypoint pub fn render(&mut self, ui: &Context, uiworld: &mut UiWorld, sim: &Simulation) { - profiling::scope!("topgui::render"); + profiling::scope!("hud::render"); self.auto_save(uiworld); if self.hidden { @@ -149,7 +149,7 @@ impl Gui { } pub fn toolbox(ui: &Context, uiworld: &mut UiWorld, _sim: &Simulation) { - profiling::scope!("topgui::toolbox"); + profiling::scope!("hud::toolbox"); #[derive(Copy, Clone)] pub enum Tab { Hand, @@ -634,7 +634,7 @@ impl Gui { } pub fn menu_bar(&mut self, ui: &Context, uiworld: &mut UiWorld, sim: &Simulation) { - profiling::scope!("topgui::menu_bar"); + profiling::scope!("hud::menu_bar"); //let _t = ui.push_style_var(StyleVar::ItemSpacing([3.0, 0.0])); egui::TopBottomPanel::top("top_menu").show(ui, |ui| { diff --git a/native_app/src/gui/inspect/mod.rs b/native_app/src/gui/inspect/mod.rs index d4bad089..52834633 100644 --- a/native_app/src/gui/inspect/mod.rs +++ b/native_app/src/gui/inspect/mod.rs @@ -18,7 +18,7 @@ mod inspect_train; mod inspect_vehicle; pub fn inspector(ui: &Context, uiworld: &mut UiWorld, sim: &Simulation) { - profiling::scope!("topgui::inspector"); + profiling::scope!("hud::inspector"); let inspected_building = *uiworld.read::(); if let Some(b) = inspected_building.e { inspect_building(uiworld, sim, ui, b); diff --git a/native_app/src/gui/mod.rs b/native_app/src/gui/mod.rs index 964cc774..48c207c7 100644 --- a/native_app/src/gui/mod.rs +++ b/native_app/src/gui/mod.rs @@ -16,15 +16,15 @@ use simulation::{AnyEntity, Simulation}; pub mod chat; pub mod follow; +pub mod hud; pub mod inspect; pub mod inspected_aura; mod tools; -pub mod topgui; pub mod windows; pub use follow::FollowEntity; +pub use hud::*; pub use tools::*; -pub use topgui::*; pub fn run_ui_systems(sim: &Simulation, uiworld: &mut UiWorld) { profiling::scope!("gui::run_ui_systems"); diff --git a/native_app/src/newgui/hud/mod.rs b/native_app/src/newgui/hud/mod.rs new file mode 100644 index 00000000..c1a20e77 --- /dev/null +++ b/native_app/src/newgui/hud/mod.rs @@ -0,0 +1,79 @@ +use ordered_float::OrderedFloat; +use yakui::{reflow, Alignment, Color, Dim2, Vec2}; + +use simulation::map_dynamic::ElectricityFlow; +use simulation::Simulation; + +use crate::gui::{Gui, UiTextures}; +use crate::newgui::hud::time_controls::time_controls; +use crate::newgui::hud::toolbox::new_toolbox; +use crate::uiworld::UiWorld; + +mod time_controls; +mod toolbox; + +impl Gui { + /// Root GUI entrypoint + pub fn render_newgui(&mut self, uiworld: &mut UiWorld, sim: &Simulation) { + profiling::scope!("hud::render"); + self.auto_save(uiworld); + + if self.hidden { + return; + } + + yakui::column(|| { + time_controls(self, uiworld, sim); + self.power_errors(uiworld, sim); + new_toolbox(uiworld, sim); + }); + } + + fn power_errors(&mut self, uiworld: &UiWorld, sim: &Simulation) { + profiling::scope!("hud::power_errors"); + let map = sim.map(); + let flow = sim.read::(); + + let no_power_img = uiworld.read::().get_yakui("no_power"); + + for network in map.electricity.networks() { + if !flow.blackout(network.id) { + continue; + } + + let mut buildings_with_issues = Vec::with_capacity(network.buildings.len()); + + for &building in &network.buildings { + let Some(b) = map.get(building) else { + continue; + }; + + let center = b.obb.center(); + + let pos = center.z(b.height + + 20.0 + + 1.0 * f32::cos(uiworld.time_always() + center.mag() * 0.05)); + let (screenpos, depth) = uiworld.camera().project(pos); + + let size = 10000.0 / depth; + + buildings_with_issues.push((screenpos, size)); + } + + buildings_with_issues.sort_by_key(|x| OrderedFloat(x.1)); + + for (screenpos, size) in buildings_with_issues { + reflow( + Alignment::TOP_LEFT, + Dim2::pixels(screenpos.x - size * 0.5, screenpos.y - size * 0.5), + || { + let mut image = + yakui::widgets::Image::new(no_power_img, Vec2::new(size, size)); + image.color = Color::WHITE.with_alpha(0.7); + image.show(); + }, + ); + } + } + } +} diff --git a/native_app/src/newgui/hud/time_controls.rs b/native_app/src/newgui/hud/time_controls.rs new file mode 100644 index 00000000..c66d2073 --- /dev/null +++ b/native_app/src/newgui/hud/time_controls.rs @@ -0,0 +1,109 @@ +use yakui::widgets::{List, Pad}; +use yakui::{ + constrained, reflow, row, spacer, Alignment, Color, Constraints, CrossAxisAlignment, Dim2, + MainAxisAlignment, MainAxisSize, Vec2, +}; + +use goryak::{ + blur_bg, button_primary, button_secondary, constrained_viewport, icon_button, monospace, + on_primary, on_secondary, on_secondary_container, padx, padxy, secondary_container, +}; +use prototypes::GameTime; +use simulation::Simulation; + +use crate::gui::windows::settings::Settings; +use crate::gui::Gui; +use crate::inputmap::{InputAction, InputMap}; +use crate::uiworld::UiWorld; + +pub fn time_controls(gui: &mut Gui, uiworld: &mut UiWorld, sim: &Simulation) { + profiling::scope!("hud::time_controls"); + let time = sim.read::().daytime; + let warp = &mut uiworld.write::().time_warp; + let depause_warp = &mut gui.depause_warp; + if uiworld + .read::() + .just_act + .contains(&InputAction::PausePlay) + { + if *warp == 0 { + *warp = *depause_warp; + } else { + *depause_warp = *warp; + *warp = 0; + } + } + + if *warp == 0 { + yakui::canvas(|ctx| { + let w = ctx.layout.viewport().size().length() * 0.002; + yakui::shapes::outline( + ctx.paint, + ctx.layout.viewport(), + w, + Color::rgba(255, 0, 0, 128), + ); + }); + } + + let mut time_text = || { + padx(5.0, || { + row(|| { + monospace(on_secondary_container(), format!("Day {}", time.day)); + spacer(1); + monospace( + on_secondary_container(), + format!("{:02}:{:02}:{:02}", time.hour, time.minute, time.second), + ); + }); + }); + let mut l = List::row(); + l.main_axis_alignment = MainAxisAlignment::SpaceBetween; + l.show(|| { + let mut time_button = |text: &str, b_warp: u32| { + let mut b = if *warp == b_warp { + icon_button(button_primary(text)) + } else { + icon_button(button_secondary(text)) + }; + + b.padding = Pad::balanced(10.0, 3.0); + if b.show().clicked { + if b_warp == 0 { + if *warp == 0 { + *warp = *depause_warp; + } else { + *depause_warp = *warp; + } + } + *warp = b_warp; + } + }; + + time_button("pause", 0); + time_button("play", 1); + time_button("forward", 3); + time_button("fast-forward", 1000); + }); + }; + + reflow(Alignment::TOP_LEFT, Dim2::pixels(-10.0, 30.0), || { + constrained_viewport(|| { + let mut l = List::row(); + l.main_axis_alignment = MainAxisAlignment::End; + l.show(|| { + blur_bg(secondary_container().with_alpha(0.5), 10.0, || { + padxy(10.0, 5.0, || { + constrained(Constraints::loose(Vec2::new(170.0, f32::INFINITY)), || { + let mut l = List::column(); + l.cross_axis_alignment = CrossAxisAlignment::Stretch; + l.main_axis_size = MainAxisSize::Min; + l.item_spacing = tweak!(5.0); + l.show(|| time_text()); + }); + }); + }); + }); + }); + }); +} diff --git a/native_app/src/newgui/hud/toolbox.rs b/native_app/src/newgui/hud/toolbox.rs new file mode 100644 index 00000000..d320d73d --- /dev/null +++ b/native_app/src/newgui/hud/toolbox.rs @@ -0,0 +1,292 @@ +use yakui::widgets::{List, Pad}; +use yakui::{ + colored_box_container, column, image, reflow, spacer, Alignment, Color, CrossAxisAlignment, + Dim2, MainAxisAlignment, MainAxisSize, Vec2, +}; + +use goryak::{ + blur_bg, button_primary, constrained_viewport, fixed_spacer, icon_button, image_button, + monospace, on_primary, outline, outline_variant, padxy, primary, primary_container, round_rect, + secondary, secondary_container, textc, +}; +use simulation::map::LanePatternBuilder; +use simulation::Simulation; + +use crate::gui::roadbuild::RoadBuildResource; +use crate::gui::{Tool, UiTextures}; +use crate::inputmap::{InputAction, InputMap}; +use crate::uiworld::UiWorld; + +pub fn new_toolbox(uiworld: &mut UiWorld, sim: &Simulation) { + if uiworld + .read::() + .just_act + .contains(&InputAction::Close) + { + *uiworld.write::() = Tool::Hand; + } + + reflow(Alignment::TOP_LEFT, Dim2::ZERO, || { + constrained_viewport(|| { + let mut l = List::column(); + l.cross_axis_alignment = CrossAxisAlignment::Stretch; + l.show(|| { + spacer(1); + yakui::opaque(|| { + let mut l = List::column(); + l.cross_axis_alignment = CrossAxisAlignment::Stretch; + l.show(|| { + let mut needs_outline = false; + blur_bg(primary_container().with_alpha(0.3), 0.0, || { + needs_outline = tool_properties(uiworld, sim); + }); + if needs_outline { + colored_box_container(outline().with_alpha(0.5), || { + fixed_spacer((1.0, 1.0)); + }); + } + blur_bg(secondary_container().with_alpha(0.3), 0.0, || { + padxy(0.0, 10.0, || { + let mut l = List::row(); + l.main_axis_alignment = MainAxisAlignment::Center; + l.item_spacing = 10.0; + l.show(|| { + tools_list(uiworld); + }); + }); + }); + }); + }); + }); + }); + }); +} + +fn tools_list(uiworld: &mut UiWorld) { + let tools = [ + ("toolbar_straight_road", Tool::RoadbuildStraight), + ("toolbar_curved_road", Tool::RoadbuildCurved), + ("toolbar_road_edit", Tool::RoadEditor), + ("toolbar_housetool", Tool::LotBrush), + ("toolbar_companies", Tool::SpecialBuilding), + ("toolbar_bulldozer", Tool::Bulldozer), + ("toolbar_train", Tool::Train), + ("toolbar_terraform", Tool::Terraforming), + ]; + + for (name, tool) in &tools { + column(|| { + let (default_col, hover_col) = if *tool == *uiworld.read::() { + let c = primary().lerp(&Color::WHITE, 0.3); + (c, c) + } else { + (Color::WHITE, Color::WHITE.with_alpha(0.7)) + }; + if image_button( + uiworld.read::().get_yakui(name), + Vec2::new(64.0, 64.0), + default_col, + hover_col, + primary(), + "", + ) + .clicked + { + *uiworld.write::() = *tool; + } + + if *tool == *uiworld.read::() { + reflow(Alignment::CENTER_LEFT, Dim2::pixels(0.0, 32.0), || { + image( + uiworld + .read::() + .get_yakui("select_triangle_under"), + Vec2::new(64.0, 10.0), + ); + }); + } + }); + } +} + +fn tool_properties(uiw: &UiWorld, _sim: &Simulation) -> bool { + let tool = *uiw.read::(); + + match tool { + Tool::Hand => return false, + Tool::Bulldozer => return false, + Tool::LotBrush => return false, + Tool::RoadbuildStraight | Tool::RoadbuildCurved => { + roadbuild_properties(uiw); + } + Tool::RoadEditor => {} + Tool::SpecialBuilding => {} + Tool::Train => {} + Tool::Terraforming => {} + } + true +} + +fn roadbuild_properties(uiw: &UiWorld) { + let mut state = uiw.write::(); + + padxy(0.0, 10.0, || { + let mut l = List::row(); + l.main_axis_alignment = MainAxisAlignment::Center; + l.cross_axis_alignment = CrossAxisAlignment::Center; + l.item_spacing = 10.0; + l.show(|| { + // Snap to grid + let (default_col, hover_col) = if state.snap_to_grid { + let c = primary().lerp(&Color::WHITE, 0.3); + (c, c.with_alpha(0.7)) + } else { + (Color::WHITE.with_alpha(0.3), Color::WHITE.with_alpha(0.5)) + }; + if image_button( + uiw.read::().get_yakui("snap_grid"), + Vec2::new(32.0, 32.0), + default_col, + hover_col, + primary(), + "snap to grid", + ) + .clicked + { + state.snap_to_grid = !state.snap_to_grid; + } + + // Road elevation + let mut l = List::column(); + l.cross_axis_alignment = CrossAxisAlignment::Center; + l.main_axis_size = MainAxisSize::Min; + l.item_spacing = 3.0; + l.show(|| { + let updown_button = |text| { + let mut b = icon_button(button_primary(text)); + b.padding = Pad::balanced(5.0, 3.0); + b.style.text.font_size = 13.0; + b.down_style.text.font_size = 13.0; + b.hover_style.text.font_size = 13.0; + b + }; + + if updown_button("caret-up").show().clicked { + state.height_offset += 2.0; + } + round_rect(3.0, primary(), || { + padxy(5.0, 2.0, || { + monospace(on_primary(), format!("{:.0}m", state.height_offset)); + }); + }); + if updown_button("caret-down").show().clicked { + state.height_offset -= 2.0; + } + }); + + // image name, label, builder + let builders: &[(&str, &str, LanePatternBuilder)] = &[ + ("roadtypes_street", "Street", LanePatternBuilder::new()), + ( + "roadtypes_street_1way", + "Street one-way", + LanePatternBuilder::new().one_way(true), + ), + ( + "roadtypes_avenue", + "Avenue", + LanePatternBuilder::new().n_lanes(2).speed_limit(13.0), + ), + ( + "roadtypes_avenue_1way", + "Avenue one-way", + LanePatternBuilder::new() + .n_lanes(2) + .one_way(true) + .speed_limit(13.0), + ), + ( + "roadtypes_drive", + "Drive", + LanePatternBuilder::new() + .parking(false) + .sidewalks(false) + .speed_limit(13.0), + ), + ( + "roadtypes_drive_1way", + "Drive one-way", + LanePatternBuilder::new() + .parking(false) + .sidewalks(false) + .one_way(true) + .speed_limit(13.0), + ), + ( + "roadtypes_highway", + "Highway", + LanePatternBuilder::new() + .n_lanes(3) + .speed_limit(25.0) + .parking(false) + .sidewalks(false), + ), + ( + "roadtypes_highway_1way", + "Highway one-way", + LanePatternBuilder::new() + .n_lanes(3) + .speed_limit(25.0) + .parking(false) + .sidewalks(false) + .one_way(true), + ), + ( + "roadtypes_rail", + "Rail", + LanePatternBuilder::new().rail(true), + ), + ( + "roadtypes_rail_1way", + "Rail one-way", + LanePatternBuilder::new().rail(true).one_way(true), + ), + ]; + + for (icon, label, builder) in builders { + let mut l = List::column(); + l.main_axis_size = MainAxisSize::Min; + l.show(|| { + let is_active = &state.pattern_builder == builder; + let (default_col, hover_col) = if is_active { + let c = Color::WHITE.adjust(0.5); + (c, c) + } else { + (Color::WHITE, Color::WHITE.with_alpha(0.7)) + }; + if image_button( + uiw.read::().get_yakui(icon), + Vec2::new(64.0, 64.0), + default_col, + hover_col, + primary(), + label, + ) + .clicked + { + state.pattern_builder = *builder; + } + + if is_active { + reflow(Alignment::CENTER_LEFT, Dim2::pixels(0.0, 32.0), || { + image( + uiw.read::().get_yakui("select_triangle_under"), + Vec2::new(64.0, 10.0), + ); + }); + } + }); + } + }); + }); +} diff --git a/native_app/src/newgui/mod.rs b/native_app/src/newgui/mod.rs index c421d01e..0fc1d96a 100644 --- a/native_app/src/newgui/mod.rs +++ b/native_app/src/newgui/mod.rs @@ -1 +1 @@ -mod topgui; +mod hud; diff --git a/native_app/src/newgui/topgui.rs b/native_app/src/newgui/topgui.rs deleted file mode 100644 index 8ec82d7e..00000000 --- a/native_app/src/newgui/topgui.rs +++ /dev/null @@ -1,266 +0,0 @@ -use ordered_float::OrderedFloat; -use yakui::widgets::{List, Pad}; -use yakui::{ - column, constrained, image, reflow, row, spacer, Alignment, Color, Constraints, - CrossAxisAlignment, Dim2, MainAxisAlignment, MainAxisSize, Vec2, -}; - -use goryak::{ - blur_bg, button_primary, button_secondary, constrained_viewport, icon_map, image_button, - monospace, padx, padxy, primary, secondary_container, -}; -use prototypes::GameTime; -use simulation::map_dynamic::ElectricityFlow; -use simulation::Simulation; - -use crate::gui::windows::settings::Settings; -use crate::gui::{Gui, Tool, UiTextures}; -use crate::inputmap::{InputAction, InputMap}; -use crate::uiworld::UiWorld; - -impl Gui { - /// Root GUI entrypoint - pub fn render_newgui(&mut self, uiworld: &mut UiWorld, sim: &Simulation) { - profiling::scope!("topgui::render"); - self.auto_save(uiworld); - - if self.hidden { - return; - } - - yakui::column(|| { - self.time_controls(uiworld, sim); - self.power_errors(uiworld, sim); - self.new_toolbox(uiworld, sim); - }); - } - - pub fn time_controls(&mut self, uiworld: &mut UiWorld, sim: &Simulation) { - profiling::scope!("topgui::time_controls"); - let time = sim.read::().daytime; - let warp = &mut uiworld.write::().time_warp; - let depause_warp = &mut self.depause_warp; - if uiworld - .read::() - .just_act - .contains(&InputAction::PausePlay) - { - if *warp == 0 { - *warp = *depause_warp; - } else { - *depause_warp = *warp; - *warp = 0; - } - } - - if *warp == 0 { - yakui::canvas(|ctx| { - let w = ctx.layout.viewport().size().length() * 0.002; - yakui::shapes::outline( - ctx.paint, - ctx.layout.viewport(), - w, - Color::rgba(255, 0, 0, 128), - ); - }); - } - - let mut time_text = || { - padx(5.0, || { - row(|| { - monospace(format!("Day {}", time.day)); - spacer(1); - monospace(format!( - "{:02}:{:02}:{:02}", - time.hour, time.minute, time.second - )); - }); - }); - let mut l = List::row(); - l.main_axis_alignment = MainAxisAlignment::SpaceBetween; - l.show(|| { - let mut time_button = |text: &str, b_warp: u32| { - let (mapped, name) = icon_map(text); - let mut b = if *warp == b_warp { - button_primary(mapped) - } else { - button_secondary(mapped) - }; - - b.style.text.font = name.clone(); - b.hover_style.text.font = name.clone(); - b.down_style.text.font = name; - - b.padding = Pad::balanced(10.0, 3.0); - if b.show().clicked { - if b_warp == 0 { - if *warp == 0 { - *warp = *depause_warp; - } else { - *depause_warp = *warp; - } - } - *warp = b_warp; - } - }; - - time_button("pause", 0); - time_button("play", 1); - time_button("forward", 3); - time_button("fast-forward", 1000); - }); - }; - - reflow(Alignment::TOP_LEFT, Dim2::pixels(-10.0, 30.0), || { - constrained_viewport(|| { - let mut l = List::row(); - l.main_axis_alignment = MainAxisAlignment::End; - l.show(|| { - blur_bg(secondary_container().with_alpha(0.5), 10.0, || { - padxy(10.0, 5.0, || { - constrained( - Constraints::loose(Vec2::new(170.0, f32::INFINITY)), - || { - let mut l = List::column(); - l.cross_axis_alignment = CrossAxisAlignment::Stretch; - l.main_axis_size = MainAxisSize::Min; - l.item_spacing = tweak!(5.0); - l.show(|| time_text()); - }, - ); - }); - }); - }); - }); - }); - } - - fn new_toolbox(&self, uiworld: &mut UiWorld, _sim: &Simulation) { - if uiworld - .read::() - .just_act - .contains(&InputAction::Close) - { - *uiworld.write::() = Tool::Hand; - } - - let tools = [ - ("toolbar_straight_road", Tool::RoadbuildStraight), - ("toolbar_curved_road", Tool::RoadbuildCurved), - ("toolbar_road_edit", Tool::RoadEditor), - ("toolbar_housetool", Tool::LotBrush), - ("toolbar_companies", Tool::SpecialBuilding), - ("toolbar_bulldozer", Tool::Bulldozer), - ("toolbar_train", Tool::Train), - ("toolbar_terraform", Tool::Terraforming), - ]; - - yakui::reflow(Alignment::TOP_LEFT, Dim2::ZERO, || { - constrained_viewport(|| { - let mut l = List::column(); - l.cross_axis_alignment = CrossAxisAlignment::Stretch; - - l.show(|| { - spacer(1); - yakui::opaque(|| { - blur_bg(secondary_container().with_alpha(0.3), 0.0, || { - padxy(0.0, 10.0, || { - let mut l = List::row(); - l.main_axis_alignment = MainAxisAlignment::Center; - l.item_spacing = 10.0; - l.show(|| { - for (name, tool) in &tools { - let (default_col, hover_col) = - if *tool == *uiworld.read::() { - let c = primary().lerp(&Color::WHITE, 0.3); - (c, c) - } else { - (Color::WHITE, Color::WHITE.with_alpha(0.7)) - }; - - column(|| { - if image_button( - uiworld.read::().get_yakui(name), - Vec2::new(64.0, 64.0), - default_col, - hover_col, - primary(), - ) - .clicked - { - *uiworld.write::() = *tool; - } - - if *tool == *uiworld.read::() { - reflow( - Alignment::CENTER_LEFT, - Dim2::pixels(0.0, 32.0), - || { - image( - uiworld - .read::() - .get_yakui("select_triangle_under"), - Vec2::new(64.0, 10.0), - ); - }, - ); - } - }); - } - }); - }); - }); - }); - }); - }); - }); - } - - fn power_errors(&mut self, uiworld: &UiWorld, sim: &Simulation) { - profiling::scope!("topgui::power_errors"); - let map = sim.map(); - let flow = sim.read::(); - - let no_power_img = uiworld.read::().get_yakui("no_power"); - - for network in map.electricity.networks() { - if !flow.blackout(network.id) { - continue; - } - - let mut buildings_with_issues = Vec::with_capacity(network.buildings.len()); - - for &building in &network.buildings { - let Some(b) = map.get(building) else { - continue; - }; - - let center = b.obb.center(); - - let pos = center.z(b.height - + 20.0 - + 1.0 * f32::cos(uiworld.time_always() + center.mag() * 0.05)); - let (screenpos, depth) = uiworld.camera().project(pos); - - let size = 10000.0 / depth; - - buildings_with_issues.push((screenpos, size)); - } - - buildings_with_issues.sort_by_key(|x| OrderedFloat(x.1)); - - for (screenpos, size) in buildings_with_issues { - reflow( - Alignment::TOP_LEFT, - Dim2::pixels(screenpos.x - size * 0.5, screenpos.y - size * 0.5), - || { - let mut image = - yakui::widgets::Image::new(no_power_img, Vec2::new(size, size)); - image.color = Color::WHITE.with_alpha(0.7); - image.show(); - }, - ); - } - } - } -}