diff --git a/examples/widget-gallery/src/inputs.rs b/examples/widget-gallery/src/inputs.rs index 59250b1f1..3f8f7bd14 100644 --- a/examples/widget-gallery/src/inputs.rs +++ b/examples/widget-gallery/src/inputs.rs @@ -1,6 +1,10 @@ use floem::{ - peniko::Color, reactive::create_rw_signal, style::CursorStyle, view::View, views::Decorators, - widgets::text_input, + cosmic_text::{self, Weight}, + peniko::Color, + reactive::create_rw_signal, + style::CursorStyle, + view::View, + views::Decorators, widgets::{text_input, PlaceholderTextClass}, }; use crate::form::{form, form_item}; @@ -11,10 +15,13 @@ pub fn text_input_view() -> impl View { form({ ( form_item("Simple Input:".to_string(), 120.0, move || { - text_input(text).keyboard_navigatable() + text_input(text) + .placeholder("Placeholder text") + .keyboard_navigatable() }), form_item("Styled Input:".to_string(), 120.0, move || { text_input(text) + .placeholder("Placeholder text") .style(|s| { s.border(1.5) .width(250.0) @@ -31,6 +38,11 @@ pub fn text_input_view() -> impl View { s.border_color(Color::LIGHT_SKY_BLUE.with_alpha_factor(0.8)) .hover(|s| s.border_color(Color::LIGHT_SKY_BLUE)) }) + .class(PlaceholderTextClass, |s| { + s.color(Color::LIGHT_SKY_BLUE) + .font_style(cosmic_text::Style::Italic) + .font_weight(Weight::BOLD) + }) }) .keyboard_navigatable() }), diff --git a/src/views/text_input.rs b/src/views/text_input.rs index 0387b437c..0f44dc316 100644 --- a/src/views/text_input.rs +++ b/src/views/text_input.rs @@ -1,10 +1,11 @@ use crate::action::exec_after; use crate::keyboard::{self, KeyEvent}; use crate::reactive::{create_effect, RwSignal}; -use crate::style::{CursorStyle, TextColor}; +use crate::style::{CursorStyle, FontStyle, FontWeight, TextColor}; use crate::style::{FontProps, PaddingLeft}; use crate::unit::{PxPct, PxPctAuto}; use crate::view::ViewData; +use crate::widgets::PlaceholderTextClass; use crate::{prop_extracter, EventPropagation}; use clipboard::{ClipboardContext, ClipboardProvider}; use taffy::prelude::{Layout, Node}; @@ -38,6 +39,15 @@ prop_extracter! { } } +prop_extracter! { + PlaceholderStyle { + pub color: TextColor, + //TODO: pub font_size: FontSize, + pub font_weight: FontWeight, + pub font_style: FontStyle, + } +} + enum InputKind { SingleLine, #[allow(unused)] @@ -51,6 +61,9 @@ enum InputKind { pub struct TextInput { data: ViewData, buffer: RwSignal, + pub(crate) placeholder_text: Option, + placeholder_buff: Option, + placeholder_style: PlaceholderStyle, // Where are we in the main buffer cursor_glyph_idx: usize, // This can be retrieved from the glyph, but we store it for efficiency @@ -109,6 +122,9 @@ pub fn text_input(buffer: RwSignal) -> TextInput { TextInput { data: ViewData::new(id), cursor_glyph_idx: 0, + placeholder_text: None, + placeholder_buff: None, + placeholder_style: Default::default(), buffer, text_buf: None, text_node: None, @@ -387,6 +403,29 @@ impl TextInput { self.font.size().unwrap_or(DEFAULT_FONT_SIZE) } + pub fn get_placeholder_text_attrs(&self) -> AttrsList { + let mut attrs = Attrs::new().color(self.placeholder_style.color().unwrap_or(Color::BLACK)); + + //TODO: + // self.placeholder_style + // .font_size() + // .unwrap_or(self.font_size()) + attrs = attrs.font_size(self.font_size()); + + if let Some(font_style) = self.placeholder_style.font_style() { + attrs = attrs.style(font_style); + } else if let Some(font_style) = self.font.style() { + attrs = attrs.style(font_style); + } + + if let Some(font_weight) = self.placeholder_style.font_weight() { + attrs = attrs.weight(font_weight); + } else if let Some(font_weight) = self.font.weight() { + attrs = attrs.weight(font_weight); + } + AttrsList::new(attrs) + } + pub fn get_text_attrs(&self) -> AttrsList { let mut attrs = Attrs::new().color(self.style.color().unwrap_or(Color::BLACK)); @@ -684,6 +723,18 @@ impl TextInput { self.selection = Some(new_selection); } } + + fn paint_placeholder_text( + &self, + placeholder_buff: &TextLayout, + cx: &mut crate::context::PaintCx, + ) { + let text_node = self.text_node.unwrap(); + let layout = *cx.app_state.taffy.layout(text_node).unwrap(); + let node_location = layout.location; + let text_start_point = Point::new(node_location.x as f64, node_location.y as f64); + cx.draw_text(placeholder_buff, text_start_point); + } } fn replace_range(buff: &mut String, del_range: Range, replacement: Option<&str>) { @@ -803,6 +854,7 @@ impl View for TextInput { } fn style(&mut self, cx: &mut crate::context::StyleCx<'_>) { + let style = cx.style(); if self.font.read(cx) || self.text_buf.is_none() { self.update_text_layout(); cx.app_state_mut().request_layout(self.id()); @@ -810,6 +862,9 @@ impl View for TextInput { if self.style.read(cx) { cx.app_state_mut().request_paint(self.id()); } + + let placeholder_style = style.clone().apply_class(PlaceholderTextClass); + self.placeholder_style.read_style(cx, &placeholder_style); } fn layout(&mut self, cx: &mut crate::context::LayoutCx) -> taffy::prelude::Node { @@ -831,6 +886,15 @@ impl View for TextInput { let style = cx.app_state_mut().get_builtin_style(self.id()); let node_width = layout.size.width; + if self.placeholder_buff.is_none() { + if let Some(placeholder_text) = &self.placeholder_text { + let mut placeholder_buff = TextLayout::new(); + let attrs_list = self.get_placeholder_text_attrs(); + placeholder_buff.set_text(placeholder_text, attrs_list); + self.placeholder_buff = Some(placeholder_buff); + } + } + let style_width = style.width(); let width_px = match style_width { crate::unit::PxPctAuto::Px(px) => px as f32, @@ -873,6 +937,9 @@ impl View for TextInput { if !cx.app_state.is_focused(&self.id()) && self.buffer.with_untracked(|buff| buff.is_empty()) { + if let Some(placeholder_buff) = &self.placeholder_buff { + self.paint_placeholder_text(placeholder_buff, cx); + } return; } diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index bba1b4681..48cbb6709 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -173,6 +173,10 @@ pub(crate) fn default_theme() -> Theme { .set(slider::CircleRad, PxPct::Pct(100.)) .set(slider::BarExtends, false) }) + .class(PlaceholderTextClass, |s| { + s.color(Color::rgba8(158, 158, 158, 30)) + .font_size(FONT_SIZE) + }) .class(TooltipClass, |s| { s.border(0.5) .border_color(Color::rgb8(140, 140, 140)) diff --git a/src/widgets/text_input.rs b/src/widgets/text_input.rs index bf379fb75..99c868cb9 100644 --- a/src/widgets/text_input.rs +++ b/src/widgets/text_input.rs @@ -1,12 +1,19 @@ use crate::{ style_class, - view::View, - views::{self, Decorators}, + views::{self, Decorators, TextInput}, }; use floem_reactive::RwSignal; style_class!(pub TextInputClass); +style_class!(pub PlaceholderTextClass); -pub fn text_input(buffer: RwSignal) -> impl View { +pub fn text_input(buffer: RwSignal) -> TextInput { views::text_input(buffer).class(TextInputClass) } + +impl TextInput { + pub fn placeholder(mut self, text: impl Into) -> Self { + self.placeholder_text = Some(text.into()); + self + } +}