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
## Now:
- outfitter
- Fix input (turn and multipliers)
- Indicator for "can't buy / sell"
- Buy multiple outfits
- Show money
- Show owned outfits
- init on resize
- hide ui element
- zoom limits
- text scrolling
- scrollbox scroll limits
- clean up content
- Clean up state api
- Clean up & document UI api
- Clean up scripting errors
- Mouse colliders
- Fade sprites and text in scrollbox
- Selection while flying
- outfitter
- fps textbox positioning
- shield generation curve
- clippy & rules
- reject unknown toml
- styled text & better content formatting
## Small jobs
- Scroll direction config
- Better planet icons in radar
- Clean up sprite content (and content in general)
- 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
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.
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.
"""
object.earth.landable.image = "ui::landscape::test"

View File

@ -69,6 +69,7 @@ fn init(state) {
)
);
textbox::font_sans("desc");
textbox::enable_scroll("desc", true);
if state.player_ship().is_landed() {
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::enable_scroll("ship_stats", true);
sprite::add(
"outfit_bg",
@ -146,6 +148,8 @@ fn init(state) {
);
textbox::font_serif("outfit_desc");
textbox::set_text("outfit_desc", "");
textbox::enable_scroll("outfit_desc", true);
textbox::add(
@ -159,6 +163,7 @@ fn init(state) {
);
textbox::font_mono("outfit_stats");
textbox::set_text("outfit_stats", "");
textbox::enable_scroll("outfit_stats", true);
sprite::add(
@ -348,7 +353,7 @@ fn init(state) {
fn event(state, event) {
// TODO: update on ship outfit change only
update_ship_info(state);
//update_ship_info(state);
if type_of(event) == "MouseHoverEvent" {
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_desc", outfit.desc());
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};
/// "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)]
pub struct MouseClickEvent {
pub down: bool,

View File

@ -374,5 +374,53 @@ pub fn build_textbox_module(
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;
}

View File

@ -2,7 +2,10 @@ use nalgebra::Vector2;
use rhai::{Dynamic, ImmutableString};
use super::super::api::Rect;
use crate::{InputEvent, RenderInput, RenderState};
use crate::{
ui::api::{MouseHoverEvent, NullEvent},
InputEvent, RenderInput, RenderState,
};
#[derive(Debug, Clone)]
pub struct UiScrollbox {
@ -52,16 +55,25 @@ impl UiScrollbox {
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.offset.y -= x;
return Some(Dynamic::from(NullEvent {}));
}
}

View File

@ -3,17 +3,28 @@ use glyphon::{
Shaping, Style, TextBounds, Weight,
};
use nalgebra::Vector2;
use rhai::ImmutableString;
use rhai::{Dynamic, ImmutableString};
use std::rc::Rc;
use winit::window::Window;
use super::{super::api::Rect, OwnedTextArea};
use crate::{ui::api, RenderInput};
use crate::{
ui::api::{self, MouseHoverEvent, NullEvent},
InputEvent, RenderInput, RenderState,
};
#[derive(Debug, Clone)]
pub struct UiTextBox {
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,
justify: Align,
rect: Rect,
@ -34,7 +45,7 @@ impl UiTextBox {
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
buffer.set_size(font, rect.dim.x, rect.dim.y);
buffer.set_size(font, rect.dim.x, rect.dim.y + 100.0);
Self {
name,
@ -44,6 +55,9 @@ impl UiTextBox {
justify: Align::Left,
attrs: AttrsOwned::new(Attrs::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.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 {
@ -106,6 +197,7 @@ impl<'a, 'b: 'a> UiTextBox {
// Glypon works with physical pixels, so we must do some conversion
let fac = window.scale_factor() as f32;
let scroll_offset = self.v_offset * fac;
let corner_ne = Vector2::new(
(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),
@ -115,7 +207,7 @@ impl<'a, 'b: 'a> UiTextBox {
OwnedTextArea {
buffer: self.buffer.clone(),
top: corner_ne.y,
top: corner_ne.y - scroll_offset,
left: corner_ne.x,
scale: input.ct.config.ui_scale,
bounds: TextBounds {

View File

@ -8,7 +8,7 @@ use std::{cell::RefCell, num::NonZeroU32, rc::Rc, sync::Arc};
use winit::event::VirtualKeyCode;
use super::{
api::{self, KeyboardEvent, PlayerShipStateEvent, ScrollEvent},
api::{self, KeyboardEvent, NullEvent, PlayerShipStateEvent, ScrollEvent},
UiConfig, UiElement, UiState,
};
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() {
UiElement::Sprite(sprite) => sprite.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 } => {
// Be very careful here, to avoid
// borrowing mutably twice...
@ -94,7 +95,10 @@ impl UiScriptExecutor {
UiElement::Scrollbox(sbox) => {
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!(),
},
_ => unreachable!(),
@ -109,6 +113,11 @@ impl UiScriptExecutor {
// Return if event hook returns any PlayerDirective (including None),
// continue to the next element if the hook returns ().
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)?;
if let Some(result) = result {