diff --git a/moli-layout/src/form.rs b/moli-layout/src/form.rs index 74fd90c387..808c9cae79 100644 --- a/moli-layout/src/form.rs +++ b/moli-layout/src/form.rs @@ -21,11 +21,12 @@ where let controls = (0..world.boxes.len()) .map(LayoutBoxId::from_index) .filter_map(|id| { - form_control_texts(&world.boxes[id.index()].element_semantics).map(|texts| (id, texts)) + form_control_texts(&world.boxes[id.index()].element_semantics) + .map(|(texts, placeholder)| (id, texts, placeholder)) }) - .filter(|(_, texts)| !texts.is_empty()) + .filter(|(_, texts, _)| !texts.is_empty()) .collect::>(); - for (control, texts) in controls { + for (control, texts, placeholder) in controls { let Some(owner) = world.boxes[control.index()].source else { continue; }; @@ -39,11 +40,22 @@ where .clip_form_control_contents(); } let owner_label = world.boxes[control.index()].source_label.clone(); - let wrapper_style = styles.anonymous_style( - owner, - &world.boxes[control.index()].style, - LayoutDisplay::Block, - )?; + let placeholder_style = if placeholder { + styles.placeholder_style(owner)? + } else { + None + }; + let wrapper_style = if let Some(style) = placeholder_style { + let mut style = style.into_style(); + style.force_layout_display(LayoutDisplay::Block); + style + } else { + styles.anonymous_style( + owner, + &world.boxes[control.index()].style, + LayoutDisplay::Block, + )? + }; let text_style = ResolvedLayoutStyle::text_leaf_from(&wrapper_style); for (row, text) in texts.into_iter().enumerate() { let mut wrapper = LayoutWorld::new_box( @@ -79,7 +91,7 @@ where Ok(()) } -fn form_control_texts(semantics: &Option) -> Option>> { +fn form_control_texts(semantics: &Option) -> Option<(Vec>, bool)> { let semantics = semantics.as_ref()?; let LayoutElementCategory::FormControl(kind) = semantics.category else { return None; @@ -94,8 +106,23 @@ fn form_control_texts(semantics: &Option) -> Option match input { LayoutInputControlKind::Button => data.value.to_string(), @@ -161,11 +188,14 @@ fn form_control_texts(semantics: &Option) -> Option String::new(), }; - Some(if text.is_empty() { - Vec::new() - } else { - vec![text.into()] - }) + Some(( + if text.is_empty() { + Vec::new() + } else { + vec![text.into()] + }, + placeholder, + )) } pub(crate) fn select_row_height(font_size: f32, line_height: f32) -> f32 { diff --git a/moli-layout/src/source.rs b/moli-layout/src/source.rs index 7580b77c4b..9c94ed2f8b 100644 --- a/moli-layout/src/source.rs +++ b/moli-layout/src/source.rs @@ -653,6 +653,13 @@ where Ok(None) } + fn placeholder_style( + &mut self, + _node: N, + ) -> Result, LayoutError> { + Ok(None) + } + fn anonymous_style( &mut self, _owner: N, diff --git a/moli-renderer-v8/src/layout_renderer/style_resolver.rs b/moli-renderer-v8/src/layout_renderer/style_resolver.rs index cbadd94faf..ded3173a2e 100644 --- a/moli-renderer-v8/src/layout_renderer/style_resolver.rs +++ b/moli-renderer-v8/src/layout_renderer/style_resolver.rs @@ -204,6 +204,19 @@ impl LayoutStyleResolver for NativeLayoutStyleResolver<'_> { Ok(resolved) } + fn placeholder_style( + &mut self, + node: DomHandle, + ) -> Result, LayoutError> { + Ok(self + .reads + .read(node) + .pseudo_computed_values("placeholder") + .map(|computed| { + ResolvedLayoutPseudoStyle::new(ResolvedLayoutStyle::from_stylo(computed)) + })) + } + fn anonymous_style( &mut self, owner: DomHandle, diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update/form_controls.rs b/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update/form_controls.rs index 445f9282a7..a8c124f169 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update/form_controls.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update/form_controls.rs @@ -11,6 +11,85 @@ fn dark_pixels(image: &moli_paint::RasterImage, x: u32, y: u32, width: u32, heig .count() } +#[tokio::test(flavor = "current_thread")] +async fn placeholder_pseudo_style_tracks_empty_and_filled_form_controls() { + run_page_vm_async_test(async move { + let loader = + crate::network::ResourceRequestClient::new(&FetchConfig::default()).expect("loader"); + let mut page_vm = test_page_vm_with_loader_and_document_url( + &loader, + Vec::new(), + Url::parse("https://example.com/placeholder-paint.html")?, + ); + // Like Blink's TextFieldInputType::UpdatePlaceholderText, placeholder + // content has its own pseudo style, independently of the input value. + page_vm.vm_mut().eval( + r#" +document.head.innerHTML = ``; +document.body.innerHTML=` +`; +'installed' +"#, + )?; + page_vm.vm_mut().sync_live_document_style_sources(); + for (value, expected_color, expected_size) in [ + ( + "", + moli_layout::PaintColor::new(31.0 / 255.0, 63.0 / 255.0, 127.0 / 255.0, 1.0), + 24.0, + ), + ( + "Example", + moli_layout::PaintColor::new(1.0, 0.0, 0.0, 1.0), + 20.0, + ), + ( + "", + moli_layout::PaintColor::new(31.0 / 255.0, 63.0 / 255.0, 127.0 / 255.0, 1.0), + 24.0, + ), + ] { + page_vm.vm_mut().eval(&format!( + "document.querySelectorAll('input,textarea').forEach(el=>el.value={});'updated'", + serde_json::to_string(value)?, + ))?; + let snapshot = page_vm + .vm_mut() + .screenshot_layout_snapshot(moli_layout::PaintViewport::new(240, 180, 1.0))? + .expect("placeholder layout"); + let runs = snapshot + .fragments + .iter() + .filter_map(|fragment| match fragment { + moli_layout::PaintFragment::GlyphRun(run) => Some(run), + _ => None, + }) + .collect::>(); + assert_eq!(runs.len(), 3, "each control paints one label"); + for run in runs { + assert_eq!(run.color, expected_color, "value={value:?}"); + assert_eq!(run.font_size, expected_size); + } + let placeholder_layers = snapshot.fragments.iter().filter(|fragment| matches!( + fragment, moli_layout::PaintFragment::PushLayer { opacity, .. } if *opacity == 0.5 + )).count(); + assert_eq!( + placeholder_layers, + if value.is_empty() { 3 } else { 0 }, + "placeholder opacity must not affect typed text" + ); + } + Ok::<_, anyhow::Error>(()) + }) + .await + .expect("placeholder paint fixture should run"); +} + #[tokio::test(flavor = "current_thread")] async fn screenshot_select_listboxes_paint_every_option_and_clip_their_contents() { run_page_vm_async_test(async move {