Edits
parent
7272f2a00a
commit
6bd22e2051
11
TODO.md
11
TODO.md
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue