// Copyright 2026 Ling71671
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
/// Standalone single-line text input widget supporting fluent configuration.
pub struct TextEdit {
id_salt : String
text : String
placeholder : String
size : @math.Vec2
} derive(Eq, Debug)
///|
/// Create a new text edit widget with initial text.
pub fn TextEdit::new(id_salt : String, text : String) -> TextEdit {
{ id_salt, text, placeholder: "", size: @math.Vec2::zero(), }
}
///|
/// Configure placeholder text shown when the input field is empty.
pub fn TextEdit::placeholder(self : TextEdit, placeholder : String) -> TextEdit {
{ ..self, placeholder, }
}
///|
/// Explicitly override the text edit dimensions.
pub fn TextEdit::size(self : TextEdit, size : @math.Vec2) -> TextEdit {
{ ..self, size, }
}
///|
/// Reference-bound text edit widget for uniform `ui.add(...)` dispatch.
pub struct TextEditRef {
id_salt : String
target : Ref[String]
placeholder : String
size : @math.Vec2
}
///|
/// Create a text edit bound directly to a `Ref[String]` variable.
pub fn TextEdit::from_ref(
id_salt : String,
target : Ref[String],
) -> TextEditRef {
{ id_salt, target, placeholder: "", size: @math.Vec2::zero(), }
}
///|
/// Configure placeholder text for reference-bound text edit.
pub fn TextEditRef::placeholder(
self : TextEditRef,
placeholder : String,
) -> TextEditRef {
{ ..self, placeholder, }
}
///|
/// Explicitly override the dimensions for reference-bound text edit.
pub fn TextEditRef::size(self : TextEditRef, size : @math.Vec2) -> TextEditRef {
{ ..self, size, }
}
///|
/// Render the reference-bound text edit into the provided UIContext.
pub impl @core.Widget for TextEditRef with fn ui(self, ctx : UIContext) -> Response {
let (new_text, resp) = if self.size.x > 0.0 && self.size.y > 0.0 {
text_edit_sized(
ctx,
self.id_salt,
self.target.val,
self.placeholder,
self.size,
)
} else {
text_edit_val(
ctx,
self.id_salt,
self.target.val,
placeholder=self.placeholder,
)
}
self.target.val = new_text
resp
}
///|
/// Render a single-line text input with explicit dimensions in Studio Light porcelain style.
pub fn text_edit_sized(
ctx : UIContext,
id_salt : String,
text : String,
placeholder : String,
size : @math.Vec2,
) -> (String, Response) {
ctx.push_id(id_salt)
let (id, rect, resp) = ctx.allocate_space(size)
ctx.pop_id()
ctx.register_focusable(id)
if resp.hovered {
ctx.set_cursor_icon("text")
}
let scale = ctx.style.scale
let padding_x = (ctx.style.spacing_sm + ctx.style.radius_xs) * scale
let font_size = ctx.style.font_normal * scale
let caret_h = ctx.style.spacing_lg * scale
let radius = ctx.style.radius_md * scale
let mut cursor_pos = ctx.get_text_cursor_pos(id, text.length())
let mut current_text = text
let mut scroll_x = ctx.memory().get_double_or_default(id, 0.0, tag="scroll_x")
if resp.pressed() || resp.clicked() {
ctx.request_focus(id)
ctx.reset_caret_blink()
}
if ctx.input.mouse_pressed && (resp.hovered() || resp.pressed()) {
ctx.reset_caret_blink()
let click_x = ctx.input.mouse_pos.x - (rect.x + padding_x) + scroll_x
let total_text_w = ctx.measure_text(current_text, font_size).x
if click_x <= 0.0 {
cursor_pos = 0
} else if click_x >= total_text_w {
cursor_pos = current_text.length()
} else {
let mut best_i = 0
let mut best_dist = click_x
let mut i = 0
while i <= current_text.length() {
let sub = @core.string_slice(current_text, 0, i)
let w = ctx.measure_text(sub, font_size).x
let dist = if click_x > w { click_x - w } else { w - click_x }
if dist < best_dist {
best_dist = dist
best_i = i
}
if i >= current_text.length() {
break
}
i = @core.next_char_boundary(current_text, i)
}
cursor_pos = best_i
}
}
let is_focused = ctx.has_focus(id)
if is_focused {
if ctx.input.key_pressed(Key::ArrowLeft) ||
ctx.input.key_pressed(Key::ArrowRight) ||
ctx.input.key_pressed(Key::Home) ||
ctx.input.key_pressed(Key::End) ||
ctx.input.key_pressed(Key::Backspace) ||
ctx.input.key_pressed(Key::Delete) ||
!ctx.input.text_input().is_empty() {
ctx.reset_caret_blink()
}
if ctx.input.key_pressed(Key::ArrowLeft) {
ctx.input.consume_key(Key::ArrowLeft)
cursor_pos = @core.prev_char_boundary(current_text, cursor_pos)
}
if ctx.input.key_pressed(Key::ArrowRight) {
ctx.input.consume_key(Key::ArrowRight)
cursor_pos = @core.next_char_boundary(current_text, cursor_pos)
}
if ctx.input.key_pressed(Key::Home) {
ctx.input.consume_key(Key::Home)
cursor_pos = 0
}
if ctx.input.key_pressed(Key::End) {
ctx.input.consume_key(Key::End)
cursor_pos = current_text.length()
}
if ctx.input.key_pressed(Key::Backspace) {
ctx.input.consume_key(Key::Backspace)
if cursor_pos > 0 && current_text.length() > 0 {
let (new_text, new_pos) = @core.delete_prev_char(
current_text, cursor_pos,
)
current_text = new_text
cursor_pos = new_pos
}
}
if ctx.input.key_pressed(Key::Delete) {
ctx.input.consume_key(Key::Delete)
if cursor_pos < current_text.length() {
current_text = @core.delete_next_char(current_text, cursor_pos)
}
}
let input_str = ctx.input.text_input()
if !input_str.is_empty() {
let left = @core.string_slice(current_text, 0, cursor_pos)
let right = @core.string_slice(
current_text,
cursor_pos,
current_text.length(),
)
current_text = left + input_str + right
cursor_pos = cursor_pos + input_str.length()
}
}
ctx.set_text_cursor_pos(id, cursor_pos)
let visible_w = rect.w - padding_x * 2.0
let total_text_w = ctx.measure_text(current_text, font_size).x
let sub = @core.string_slice(current_text, 0, cursor_pos)
let caret_text_x = ctx.measure_text(sub, font_size).x
if total_text_w <= visible_w {
scroll_x = 0.0
} else {
let margin = 8.0 * scale
if caret_text_x - scroll_x < 0.0 {
scroll_x = caret_text_x
} else if caret_text_x - scroll_x > visible_w - margin {
scroll_x = caret_text_x - (visible_w - margin)
}
let max_scroll = total_text_w - visible_w + margin
if scroll_x > max_scroll {
scroll_x = max_scroll
}
if scroll_x < 0.0 {
scroll_x = 0.0
}
}
ctx
.memory()
.set_double(id, scroll_x, tag="scroll_x", frame=ctx.frame_counter())
let theme = ctx.theme
let bg_color = theme.bg_window
let border_color = if is_focused {
theme.accent_primary
} else if resp.hovered() {
theme.border_strong
} else {
theme.border_default
}
let border_width = if is_focused { 1.5 * scale } else { 1.0 * scale }
ctx.painter().add_rect(rect, bg_color, radius)
ctx.painter().add_rect_stroke(rect, border_color, border_width, radius)
let inner_clip = @math.Rect::new(
rect.x + padding_x - 2.0 * scale,
rect.y,
rect.w - (padding_x * 2.0 - 4.0 * scale),
rect.h,
)
ctx.push_clip(inner_clip)
let text_y = rect.y + (rect.h - font_size) * 0.5
if current_text.is_empty() {
if !placeholder.is_empty() {
ctx
.painter()
.add_text(
@math.Vec2::new(rect.x + padding_x, text_y),
placeholder,
font_size,
theme.text_disabled,
)
}
} else {
ctx
.painter()
.add_text(
@math.Vec2::new(rect.x + padding_x - scroll_x, text_y),
current_text,
font_size,
theme.text_primary,
)
}
if is_focused && ctx.is_caret_visible() {
let caret_x = rect.x + padding_x + caret_text_x - scroll_x
let caret_top = rect.y + (rect.h - caret_h) * 0.5
let caret_bottom = caret_top + caret_h
ctx
.painter()
.add_line(
@math.Vec2::new(caret_x, caret_top),
@math.Vec2::new(caret_x, caret_bottom),
theme.accent_primary,
1.5 * scale,
)
}
ctx.pop_clip()
(current_text, resp)
}
///|
/// Render a single-line text input with automatic width layout.
pub fn text_edit_val(
ctx : UIContext,
id_salt : String,
text : String,
placeholder? : String = "",
) -> (String, Response) {
let default_w = 200.0 * ctx.style.scale
let w = if ctx.available_width() > default_w {
default_w
} else {
ctx.available_width()
}
let h = ctx.style.control_h_md * ctx.style.scale
text_edit_sized(ctx, id_salt, text, placeholder, @math.Vec2::new(w, h))
}
///|
/// Render a single-line text input bound to a `Ref[String]`.
pub fn text_edit(
ctx : UIContext,
id_salt : String,
text : Ref[String],
placeholder? : String = "",
) -> Response {
ctx.add(TextEdit::from_ref(id_salt, text).placeholder(placeholder))
}