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