master
Mark 2024-03-26 14:27:49 -07:00
parent 7272f2a00a
commit 6bd22e2051
Signed by: Mark
GPG Key ID: C6D63995FE72FD80
9 changed files with 210 additions and 12 deletions

11
TODO.md
View File

@ -1,25 +1,30 @@
# Specific projects # Specific projects
## Now: ## Now:
- outfitter
- Fix input (turn and multipliers)
- Indicator for "can't buy / sell"
- Buy multiple outfits
- Show money
- Show owned outfits
- init on resize - init on resize
- hide ui element - hide ui element
- zoom limits - zoom limits
- text scrolling
- scrollbox scroll limits - scrollbox scroll limits
- clean up content
- Clean up state api - Clean up state api
- Clean up & document UI api - Clean up & document UI api
- Clean up scripting errors - Clean up scripting errors
- Mouse colliders - Mouse colliders
- Fade sprites and text in scrollbox - Fade sprites and text in scrollbox
- Selection while flying - Selection while flying
- outfitter
- fps textbox positioning - fps textbox positioning
- shield generation curve - shield generation curve
- clippy & rules - clippy & rules
- reject unknown toml - reject unknown toml
- styled text & better content formatting
## Small jobs ## Small jobs
- Scroll direction config
- Better planet icons in radar - Better planet icons in radar
- Clean up sprite content (and content in general) - Clean up sprite content (and content in general)
- Check game version in config - Check game version in config

View File

@ -26,6 +26,16 @@ of the human species.
Earth is also the capital of the Republic. Representative government becomes complicated when Earth is also the capital of the Republic. Representative government becomes complicated when
one planet has a greater population than a hundred planets elsewhere. As a result, one planet has a greater population than a hundred planets elsewhere. As a result,
settlements of less than a million are grouped together into planetary districts that settlements of less than a million are grouped together into planetary districts that
elect a single representative between them - a source of much frustration in the frontier worlds.
The ancestral home world of humanity, Earth has a population twice that of any other inhabited planet.
Sprawling cities cover large portions of its surface, many of them overcrowded and dangerous.
Some people work to scrape together enough money to leave, while at the same time others, born
on distant worlds, make a pilgrimage of sorts to see this planet that once cradled the entirety
of the human species.
<br><br>
Earth is also the capital of the Republic. Representative government becomes complicated when
one planet has a greater population than a hundred planets elsewhere. As a result,
settlements of less than a million are grouped together into planetary districts that
elect a single representative between them - a source of much frustration in the frontier worlds. elect a single representative between them - a source of much frustration in the frontier worlds.
""" """
object.earth.landable.image = "ui::landscape::test" object.earth.landable.image = "ui::landscape::test"

View File

@ -69,6 +69,7 @@ fn init(state) {
) )
); );
textbox::font_sans("desc"); textbox::font_sans("desc");
textbox::enable_scroll("desc", true);
if state.player_ship().is_landed() { if state.player_ship().is_landed() {
textbox::set_text("desc", state.player_ship().landed_on().desc()); textbox::set_text("desc", state.player_ship().landed_on().desc());
} }

View File

@ -99,6 +99,8 @@ fn init(state) {
) )
); );
textbox::font_mono("ship_stats"); textbox::font_mono("ship_stats");
textbox::enable_scroll("ship_stats", true);
sprite::add( sprite::add(
"outfit_bg", "outfit_bg",
@ -146,6 +148,8 @@ fn init(state) {
); );
textbox::font_serif("outfit_desc"); textbox::font_serif("outfit_desc");
textbox::set_text("outfit_desc", ""); textbox::set_text("outfit_desc", "");
textbox::enable_scroll("outfit_desc", true);
textbox::add( textbox::add(
@ -159,6 +163,7 @@ fn init(state) {
); );
textbox::font_mono("outfit_stats"); textbox::font_mono("outfit_stats");
textbox::set_text("outfit_stats", ""); textbox::set_text("outfit_stats", "");
textbox::enable_scroll("outfit_stats", true);
sprite::add( sprite::add(
@ -348,7 +353,7 @@ fn init(state) {
fn event(state, event) { fn event(state, event) {
// TODO: update on ship outfit change only // TODO: update on ship outfit change only
update_ship_info(state); //update_ship_info(state);
if type_of(event) == "MouseHoverEvent" { if type_of(event) == "MouseHoverEvent" {
let element = event.element(); let element = event.element();
@ -572,6 +577,9 @@ fn update_outfit_info(selected_outfit) {
textbox::set_text("outfit_name", outfit.display_name()); textbox::set_text("outfit_name", outfit.display_name());
textbox::set_text("outfit_desc", outfit.desc()); textbox::set_text("outfit_desc", outfit.desc());
textbox::set_text("outfit_stats", stats); textbox::set_text("outfit_stats", stats);
textbox::reset_scroll("outfit_stats");
textbox::reset_scroll("outfit_name");
} }
} }

View File

@ -1,5 +1,18 @@
use rhai::{CustomType, ImmutableString, TypeBuilder}; use rhai::{CustomType, ImmutableString, TypeBuilder};
/// "Do-nothing" event, used to stop processing input without triggering user code.
/// This allows us to scroll a textbox inside a scrollbox without moving the scrollbox,
/// for example.
#[derive(Debug, Clone)]
pub struct NullEvent {}
impl CustomType for NullEvent {
// We implement build, but NullEvents do NOT trigger the event callback.
fn build(mut builder: TypeBuilder<Self>) {
builder.with_name("NullEvent");
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct MouseClickEvent { pub struct MouseClickEvent {
pub down: bool, pub down: bool,

View File

@ -374,5 +374,53 @@ pub fn build_textbox_module(
e.set_style(&mut font.borrow_mut(), Style::Italic); e.set_style(&mut font.borrow_mut(), Style::Italic);
}); });
let state = state_src.clone();
let _ = FuncRegistration::new("enable_scroll")
.with_namespace(FnNamespace::Internal)
.set_into_module(&mut module, move |name: ImmutableString, enable: bool| {
let mut ui_state = state.borrow_mut();
let e = match ui_state.get_mut_by_name(&name) {
Some(UiElement::SubElement { element, .. }) => match &mut **element {
UiElement::Text(x) => x,
_ => {
error!("called `textbox::enable_scroll` on an invalid name `{name}`");
return;
}
},
Some(UiElement::Text(x)) => x,
_ => {
error!("called `textbox::enable_scroll` on an invalid name `{name}`");
return;
}
};
e.set_scroll(enable);
});
let state = state_src.clone();
let _ = FuncRegistration::new("reset_scroll")
.with_namespace(FnNamespace::Internal)
.set_into_module(&mut module, move |name: ImmutableString| {
let mut ui_state = state.borrow_mut();
let e = match ui_state.get_mut_by_name(&name) {
Some(UiElement::SubElement { element, .. }) => match &mut **element {
UiElement::Text(x) => x,
_ => {
error!("called `textbox::reset_scroll` on an invalid name `{name}`");
return;
}
},
Some(UiElement::Text(x)) => x,
_ => {
error!("called `textbox::reset_scroll` on an invalid name `{name}`");
return;
}
};
e.reset_scroll();
});
return module; return module;
} }

View File

@ -2,7 +2,10 @@ use nalgebra::Vector2;
use rhai::{Dynamic, ImmutableString}; use rhai::{Dynamic, ImmutableString};
use super::super::api::Rect; use super::super::api::Rect;
use crate::{InputEvent, RenderInput, RenderState}; use crate::{
ui::api::{MouseHoverEvent, NullEvent},
InputEvent, RenderInput, RenderState,
};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct UiScrollbox { pub struct UiScrollbox {
@ -52,16 +55,25 @@ impl UiScrollbox {
InputEvent::MouseMove(pos) => { InputEvent::MouseMove(pos) => {
if r.contains_mouse(state, pos) && !self.has_mouse { if r.contains_mouse(state, pos) && !self.has_mouse {
self.has_mouse = true; self.has_mouse = true;
return Some(Dynamic::from(MouseHoverEvent {
enter: true,
element: self.name.clone(),
}));
} }
if !r.contains_mouse(state, pos) && self.has_mouse { if !r.contains_mouse(state, pos) && self.has_mouse {
self.has_mouse = false; self.has_mouse = false;
return Some(Dynamic::from(MouseHoverEvent {
enter: false,
element: self.name.clone(),
}));
} }
} }
InputEvent::Scroll(x) => { InputEvent::Scroll(x) => {
if self.has_mouse { if self.has_mouse {
self.offset.y -= x; self.offset.y -= x;
return Some(Dynamic::from(NullEvent {}));
} }
} }

View File

@ -3,17 +3,28 @@ use glyphon::{
Shaping, Style, TextBounds, Weight, Shaping, Style, TextBounds, Weight,
}; };
use nalgebra::Vector2; use nalgebra::Vector2;
use rhai::ImmutableString; use rhai::{Dynamic, ImmutableString};
use std::rc::Rc; use std::rc::Rc;
use winit::window::Window; use winit::window::Window;
use super::{super::api::Rect, OwnedTextArea}; use super::{super::api::Rect, OwnedTextArea};
use crate::{ui::api, RenderInput}; use crate::{
ui::api::{self, MouseHoverEvent, NullEvent},
InputEvent, RenderInput, RenderState,
};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct UiTextBox { pub struct UiTextBox {
pub name: ImmutableString, pub name: ImmutableString,
/// Vertical scroll.
/// We don't use native cosmic_text scroll, since that only allows us
/// to scroll by individual lines.
v_offset: f32,
enable_scroll: bool,
has_mouse: bool,
text: String, text: String,
justify: Align, justify: Align,
rect: Rect, rect: Rect,
@ -34,7 +45,7 @@ impl UiTextBox {
let mut buffer = Buffer::new(font, Metrics::new(font_size, line_height)); let mut buffer = Buffer::new(font, Metrics::new(font_size, line_height));
// Do NOT apply UI scale here, that's only done when we make a TextArea // Do NOT apply UI scale here, that's only done when we make a TextArea
buffer.set_size(font, rect.dim.x, rect.dim.y); buffer.set_size(font, rect.dim.x, rect.dim.y + 100.0);
Self { Self {
name, name,
@ -44,6 +55,9 @@ impl UiTextBox {
justify: Align::Left, justify: Align::Left,
attrs: AttrsOwned::new(Attrs::new()), attrs: AttrsOwned::new(Attrs::new()),
text: String::new(), text: String::new(),
v_offset: 0.0,
has_mouse: false,
enable_scroll: false,
} }
} }
@ -88,6 +102,83 @@ impl UiTextBox {
self.attrs.style = style; self.attrs.style = style;
self.reflow(font); self.reflow(font);
} }
pub fn set_scroll(&mut self, scroll: bool) {
self.enable_scroll = scroll
}
pub fn reset_scroll(&mut self) {
self.v_offset = 0.0;
}
pub fn handle_event(
&mut self,
input: &RenderInput,
state: &mut RenderState,
event: &InputEvent,
) -> Option<Dynamic> {
self.handle_event_with_offset(input, state, event, Vector2::new(0.0, 0.0))
}
pub fn handle_event_with_offset(
&mut self,
input: &RenderInput,
state: &mut RenderState,
event: &InputEvent,
offset: Vector2<f32>,
) -> Option<Dynamic> {
let mut r = self
.rect
.to_centered(&state.window, input.ct.config.ui_scale);
r.pos += offset;
match event {
InputEvent::MouseMove(pos) => {
if r.contains_mouse(state, pos) && !self.has_mouse {
self.has_mouse = true;
return Some(Dynamic::from(MouseHoverEvent {
enter: true,
element: self.name.clone(),
}));
}
if !r.contains_mouse(state, pos) && self.has_mouse {
self.has_mouse = false;
return Some(Dynamic::from(MouseHoverEvent {
enter: false,
element: self.name.clone(),
}));
}
}
InputEvent::Scroll(x) => {
if self.has_mouse && self.enable_scroll {
self.v_offset -= x;
println!("{:?}", self.buffer.layout_runs().len());
let max_scroll = 0f32.max(
(self.buffer.metrics().line_height
* self.buffer.layout_runs().len() as f32)
- self.buffer.size().1,
);
// Top scroll bound
if self.v_offset <= 0.0 {
self.v_offset = 0.0;
} else if self.v_offset >= max_scroll {
self.v_offset = max_scroll;
}
return Some(Dynamic::from(NullEvent {}));
}
}
_ => return None,
}
return None;
}
} }
impl<'a, 'b: 'a> UiTextBox { impl<'a, 'b: 'a> UiTextBox {
@ -106,6 +197,7 @@ impl<'a, 'b: 'a> UiTextBox {
// Glypon works with physical pixels, so we must do some conversion // Glypon works with physical pixels, so we must do some conversion
let fac = window.scale_factor() as f32; let fac = window.scale_factor() as f32;
let scroll_offset = self.v_offset * fac;
let corner_ne = Vector2::new( let corner_ne = Vector2::new(
(rect.pos.x - rect.dim.x / 2.0) * fac + window.inner_size().width as f32 / 2.0, (rect.pos.x - rect.dim.x / 2.0) * fac + window.inner_size().width as f32 / 2.0,
window.inner_size().height as f32 / 2.0 - (rect.pos.y * fac + rect.dim.y / 2.0), window.inner_size().height as f32 / 2.0 - (rect.pos.y * fac + rect.dim.y / 2.0),
@ -115,7 +207,7 @@ impl<'a, 'b: 'a> UiTextBox {
OwnedTextArea { OwnedTextArea {
buffer: self.buffer.clone(), buffer: self.buffer.clone(),
top: corner_ne.y, top: corner_ne.y - scroll_offset,
left: corner_ne.x, left: corner_ne.x,
scale: input.ct.config.ui_scale, scale: input.ct.config.ui_scale,
bounds: TextBounds { bounds: TextBounds {

View File

@ -8,7 +8,7 @@ use std::{cell::RefCell, num::NonZeroU32, rc::Rc, sync::Arc};
use winit::event::VirtualKeyCode; use winit::event::VirtualKeyCode;
use super::{ use super::{
api::{self, KeyboardEvent, PlayerShipStateEvent, ScrollEvent}, api::{self, KeyboardEvent, NullEvent, PlayerShipStateEvent, ScrollEvent},
UiConfig, UiElement, UiState, UiConfig, UiElement, UiState,
}; };
use crate::{ui::api::State, InputEvent, RenderInput, RenderState}; use crate::{ui::api::State, InputEvent, RenderInput, RenderState};
@ -75,8 +75,9 @@ impl UiScriptExecutor {
let arg = match ui_state.get_mut_by_idx(i).unwrap() { let arg = match ui_state.get_mut_by_idx(i).unwrap() {
UiElement::Sprite(sprite) => sprite.handle_event(&input, state, &event), UiElement::Sprite(sprite) => sprite.handle_event(&input, state, &event),
UiElement::Scrollbox(sbox) => sbox.handle_event(&input, state, &event), UiElement::Scrollbox(sbox) => sbox.handle_event(&input, state, &event),
UiElement::Text(text) => text.handle_event(&input, state, &event),
UiElement::RadialBar(_) | UiElement::Text(..) => None, UiElement::RadialBar(_) => None,
UiElement::SubElement { parent, element } => { UiElement::SubElement { parent, element } => {
// Be very careful here, to avoid // Be very careful here, to avoid
// borrowing mutably twice... // borrowing mutably twice...
@ -94,7 +95,10 @@ impl UiScriptExecutor {
UiElement::Scrollbox(sbox) => { UiElement::Scrollbox(sbox) => {
sbox.handle_event_with_offset(&input, state, &event, offset) sbox.handle_event_with_offset(&input, state, &event, offset)
} }
UiElement::RadialBar(_) | UiElement::Text(..) => None, UiElement::Text(text) => {
text.handle_event_with_offset(&input, state, &event, offset)
}
UiElement::RadialBar(_) => None,
UiElement::SubElement { .. } => unreachable!(), UiElement::SubElement { .. } => unreachable!(),
}, },
_ => unreachable!(), _ => unreachable!(),
@ -109,6 +113,11 @@ impl UiScriptExecutor {
// Return if event hook returns any PlayerDirective (including None), // Return if event hook returns any PlayerDirective (including None),
// continue to the next element if the hook returns (). // continue to the next element if the hook returns ().
if let Some(arg) = arg { if let Some(arg) = arg {
// Ignore NullEvents
if arg.clone().try_cast::<NullEvent>().is_some() {
return Ok(PlayerDirective::None);
}
let result = self.run_event_callback(state, &input, arg)?; let result = self.run_event_callback(state, &input, arg)?;
if let Some(result) = result { if let Some(result) = result {