feat: better text inputs (#854)

This commit is contained in:
LynithDev 2026-09-19 01:18:49 +02:00 • committed by GitHub
parent 45a970c38a
commit a8cc4e8b7f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 154 additions and 18 deletions

View file

@ -1,10 +1,15 @@
use freya::prelude::*;
use crate::components::{Button, Icon, IconType, OverlayPopup};
use crate::theme::colors;
use crate::ui::clamp_to_window;
const GAP_VERT: f32 = 8.0;
const GAP_HORI: f32 = 12.0;
const EXPANDED_WIDTH: f32 = 440.;
const EXPANDED_HEIGHT: f32 = 180.;
fn text_input(value: impl Into<Writable<String>>) -> Input {
Input::new(value)
.inner_margin(Gaps::new_symmetric(GAP_VERT, GAP_HORI))
@ -40,6 +45,10 @@ pub fn validate_memory(validator: InputValidator) {
#[derive(Clone, PartialEq)]
pub struct TextInput {
input: Input,
value: Writable<String>,
on_submit: Option<EventHandler<String>>,
multiline: bool,
expandable: bool,
layout: LayoutData,
style: StyleState,
text_style: TextStyleData,
@ -50,12 +59,13 @@ pub struct TextInput {
#[allow(dead_code)]
impl TextInput {
pub fn new(value: impl Into<Writable<String>>) -> Self {
Self::from_input(text_input(value))
}
pub fn from_input(input: Input) -> Self {
let value = value.into();
Self {
input,
input: text_input(value.clone()),
value,
on_submit: None,
multiline: false,
expandable: false,
layout: LayoutData::default(),
style: StyleState::default(),
text_style: TextStyleData::default(),
@ -81,7 +91,20 @@ impl TextInput {
}
pub fn on_submit(mut self, on_submit: impl Into<EventHandler<String>>) -> Self {
self.input = self.input.on_submit(on_submit);
let on_submit = on_submit.into();
self.input = self.input.on_submit(on_submit.clone());
self.on_submit = Some(on_submit);
self
}
pub fn multiline(mut self, multiline: bool) -> Self {
self.input = self.input.multiline(multiline);
self.multiline = multiline;
self
}
pub fn expandable(mut self, expandable: bool) -> Self {
self.expandable = expandable;
self
}
@ -147,18 +170,114 @@ impl StyleExt for TextInput {
}
}
fn submit_on_modifier_enter(
value: Writable<String>,
on_submit: EventHandler<String>,
) -> Callback<Event<KeyboardEventData>, bool> {
Callback::new(move |e: Event<KeyboardEventData>| match &e.key {
Key::Named(NamedKey::Enter) if e.modifiers.contains(Modifiers::ctrl_or_meta()) => {
e.stop_propagation();
e.prevent_default();
on_submit.call(value.read().clone());
false
}
Key::Named(NamedKey::Escape) | Key::Named(NamedKey::Shift) => true,
Key::Named(NamedKey::Tab) => false,
_ => {
e.stop_propagation();
e.prevent_default();
true
}
})
}
impl Component for TextInput {
fn render(&self) -> impl IntoElement {
let mut expanded = use_state(|| false);
let mut area = use_state(Area::default);
let mut card = rect()
.layout(self.layout.clone())
.text_style(self.text_style.clone())
.children(self.elements.clone());
.children(self.elements.clone())
.maybe(self.expandable, |el| {
el.on_sized(move |e: Event<SizedEventData>| area.set_if_modified(e.area))
});
card.get_style().clone_from(&self.style);
card.child(
self.input
.clone()
let mut input = self.input.clone();
if self.multiline {
if let Some(on_submit) = self.on_submit.clone() {
input =
input.on_pre_key_down(submit_on_modifier_enter(self.value.clone(), on_submit));
}
if !matches!(self.layout.height, Size::Inner) {
input = input.height(Size::fill());
}
}
let expanded_input = (self.expandable && expanded()).then(|| {
let area = area();
let (left, top) = clamp_to_window(
area.max_x() - EXPANDED_WIDTH,
area.min_y(),
EXPANDED_WIDTH,
EXPANDED_HEIGHT,
);
OverlayPopup::new()
.backdrop(false)
.position(Position::new_global().left(left).top(top))
.on_close(move |_| expanded.set(false))
.child(
rect()
.corner_radius(CornerRadius::new_all(8.))
.shadow(Shadow::from((
0.,
8.,
32.,
0.,
Color::from_argb(120, 0, 0, 0),
)))
.child(
self.input
.clone()
.multiline(true)
.auto_focus(true)
.width(Size::px(EXPANDED_WIDTH))
.height(Size::px(EXPANDED_HEIGHT)),
),
)
});
if self.expandable {
input = input.trailing(
rect()
.on_pointer_down(|e: Event<PointerEventData>| e.stop_propagation())
.child(
Button::new()
.ghost()
.icon()
.width(Size::px(22.))
.height(Size::px(22.))
.padding(3.)
.tooltip("Expand")
.on_press(move |e: Event<PressEventData>| {
e.stop_propagation();
expanded.set(true);
})
.child(
Icon::new(IconType::Maximize01)
.size(14.)
.color(colors::fg_secondary()),
),
),
);
}
card.maybe_child(expanded_input).child(
input
.map(self.text_style.text_align, |mut el, text_align| {
el = el.text_align(text_align);

View file

@ -627,6 +627,7 @@ impl Component for TextRow {
let control = TextInput::new(text)
.placeholder(placeholder)
.expandable(true)
.width(Size::px(220.));
settings_row(

View file

@ -15,7 +15,7 @@ use crate::notifications::{
OptionalModsGroup,
};
use crate::routes::Route;
use crate::theme::colors;
use crate::theme::{self, colors};
use crate::ui::border_all_color;
type SqlResult = Option<Result<ConsoleQueryResult, String>>;
@ -1093,25 +1093,37 @@ impl Component for SqlConsole {
.vertical()
.width(Size::fill())
.spacing(10.)
.child(
TextInput::new(query)
.multiline(true)
.placeholder("SELECT * FROM …")
.font_family(theme::MONO_FONT)
.width(Size::fill())
.height(Size::px(140.))
.on_submit(move |_| run_sql(query, result, running)),
)
.child(
rect()
.horizontal()
.width(Size::fill())
.cross_align(Alignment::Center)
.spacing(12.)
.child(
rect().width(Size::flex(1.0)).child(
TextInput::new(query)
.placeholder("SELECT * FROM …")
.on_submit(move |_| run_sql(query, result, running)),
),
)
.child(
Button::new()
.primary()
.child(Icon::new(IconType::Terminal).size(16.))
.text(if *running.read() { "Running…" } else { "Run" })
.on_press(move |_| run_sql(query, result, running)),
)
.child(
label()
.text(if cfg!(target_os = "macos") {
"⌘ Enter"
} else {
"Ctrl+Enter"
})
.font_size(12.)
.color(colors::fg_secondary()),
),
)
.child(sql_result(&result.read()))

View file

@ -108,6 +108,7 @@ impl Component for SettingsMinecraft {
"Extra arguments passed to Java. Separate them with spaces; quote values containing spaces.",
TextInput::new(jvm_args)
.placeholder("-XX:+UseG1GC")
.expandable(true)
.width(Size::px(220.)),
))
.child(section_header("CONTENT"))
@ -127,6 +128,7 @@ impl Component for SettingsMinecraft {
"Command to run before launching the game.",
TextInput::new(pre_launch_command)
.placeholder("echo 'Game started'")
.expandable(true)
.width(Size::px(220.)),
))
.child(settings_row(
@ -135,6 +137,7 @@ impl Component for SettingsMinecraft {
"Command to run when launching the game.",
TextInput::new(wrapper_command)
.placeholder("gamescope")
.expandable(true)
.width(Size::px(220.)),
))
.child(settings_row(
@ -143,6 +146,7 @@ impl Component for SettingsMinecraft {
"Command to run after exiting the game.",
TextInput::new(post_exit_command)
.placeholder("echo 'Game exited'")
.expandable(true)
.width(Size::px(220.)),
));