Skip to content

Implement Input type=text UA Shadow DOM#37065

Merged
jdm merged 13 commits intoservo:mainfrom
stevennovaryo:input-type-text
May 30, 2025
Merged

Implement Input type=text UA Shadow DOM#37065
jdm merged 13 commits intoservo:mainfrom
stevennovaryo:input-type-text

Conversation

@stevennovaryo
Copy link
Copy Markdown
Contributor

@stevennovaryo stevennovaryo commented May 21, 2025

Implement Shadow Tree construction for input type=text, adding a text control inner editor container and placeholder container. Subsequently, due to the changes of the DOM tree structure, the changes will add a new NodeFlag IS_TEXT_CONTROL_INNER_EDITOR to handle the following cases.

  • If a mouse click button event hits a text control inner editor, it will redirect the focus target to its shadow host.
  • In text run's construction, the text control inner editor container queries the selection from its shadow host. This is later used to resolve caret and selection painting in the display list.

This will be the first step of fixing input type=text and other single-line text input element widgets. Such as, implementing ::placeholder selector.

Testing: Existing WPT test and new Servo specific appearance WPT.
Fixes: #36307

@stevennovaryo
Copy link
Copy Markdown
Contributor Author

@mrobinson mrobinson added the T-linux-wpt Do a try run of the WPT label May 21, 2025
@github-actions github-actions bot removed the T-linux-wpt Do a try run of the WPT label May 21, 2025
@github-actions
Copy link
Copy Markdown

🔨 Triggering try run (#15159140690) for Linux (WPT)

Comment on lines +224 to +235
} else if parent_element.is_text_editing_root() {
let info = NodeAndStyleInfo::new(
parent_element,
parent_element.style(context.shared_context()),
);
for child in iter_child_nodes(parent_element) {
if child.is_text_node() {
handler.handle_text(&info, child.to_threadsafe().node_text_content());
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain why it is necessary to skip non-text children of text editing roots? It looks like in the current version of the shadow tree there aren't even any other children there?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In terms of text input, it would be incorrect for a text editing root to contain other type of nodes. I suppose I could put a debug log here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we have full control over what is inside the shadow tree I don't think we should modify dom_traversal.rs at all.

Copy link
Copy Markdown
Contributor Author

@stevennovaryo stevennovaryo May 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we still need to handle this kind of style context passing. Since the current implementation of caret and selection depends on parent's style. Once all input type's shadow tree is supported, this part of the code will actually replace the "input" element specific if condition above. Which, also handle empty input text. I should add a documentation about this.

@github-actions
Copy link
Copy Markdown

Test results for linux-wpt from try job (#15159140690):

Flaky unexpected result (20)
  • OK /content-security-policy/frame-ancestors/frame-ancestors-path-ignored.window.html (#36468)
    • PASS [expected FAIL] subtest: A 'frame-ancestors' CSP directive with a URL that includes a path should be ignored.
  • OK /css/css-cascade/layer-font-face-override.html (#35935)
    • PASS [expected FAIL] subtest: @font-face override update with appended sheet 1
    • PASS [expected FAIL] subtest: @font-face override update with appended sheet 2
  • TIMEOUT /fetch/metadata/generated/css-images.sub.tentative.html (#29047)
    • TIMEOUT [expected PASS] subtest: background-image sec-fetch-user - Not sent to non-trustworthy same-origin destination

      Test timed out
      

  • TIMEOUT [expected OK] /html/browsers/browsing-the-web/navigating-across-documents/anchor-fragment-form-submit.html
  • OK /html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/iframe-nosrc.html (#34819)
    • PASS [expected FAIL] subtest: link click
    • FAIL [expected PASS] subtest: form submission

      assert_equals: expected "http://web-platform.test:8000/common/blank.html?1=" but got "about:blank"
      

  • OK /html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/iframe-src-aboutblank-navigate-immediately.html (#29048)
    • FAIL [expected PASS] subtest: Navigating to a different document with link click

      assert_equals: expected "http://web-platform.test:8000/common/blank.html?1" but got "about:blank"
      

    • FAIL [expected PASS] subtest: Navigating to a different document with form submission

      assert_equals: expected "http://web-platform.test:8000/common/blank.html?1=" but got "about:blank"
      

  • OK /html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/a-click.html (#28697)
    • FAIL [expected PASS] subtest: aElement.click() before the load event must NOT replace

      assert_equals: expected "http://web-platform.test:8000/common/blank.html?thereplacement" but got "http://web-platform.test:8000/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/resources/code-injector.html?pipe=sub(none)&code=%0A%20%20%20%20const%20a%20%3D%20document.createElement(%22a%22)%3B%0A%20%20%20%20a.href%20%3D%20%22%2Fcommon%2Fblank.html%3Fthereplacement%22%3B%0A%20%20%20%20document.currentScript.before(a)%3B%0A%20%20%20%20a.click()%3B%0A%20%20"
      

  • OK /html/browsers/history/the-history-interface/traverse_the_history_3.html (#21383)
    • FAIL [expected PASS] subtest: Multiple history traversals, last would be aborted

      assert_array_equals: Pages opened during history navigation expected property 1 to be 3 but got 2 (expected array [6, 3] got [6, 2])
      

  • PASS [expected FAIL] /html/canvas/element/manual/drawing-text-to-the-canvas/canvas.2d.disconnected-font-size-math.html (#30063)
  • CRASH [expected TIMEOUT] /html/canvas/element/manual/wide-gamut-canvas/canvas-display-p3-drawImage.https.html
  • TIMEOUT [expected OK] /html/interaction/focus/the-autofocus-attribute/update-the-rendering.html (#24145)
    • TIMEOUT [expected FAIL] subtest: "Flush autofocus candidates" should be happen before a scroll event and animation frame callbacks

      Test timed out
      

  • OK [expected TIMEOUT] /html/semantics/embedded-content/media-elements/playing-the-media-resource/loop-from-ended.tentative.html (#33778)
    • FAIL [expected TIMEOUT] subtest: play() with loop set to true after playback ended

      this argument is not a finite floating-point value
      

  • OK /html/semantics/embedded-content/the-iframe-element/iframe-loading-lazy-nav-form-submit.html (#32607)
    • FAIL [expected PASS] subtest: Navigating iframe loading='lazy' before it is loaded: form submit

      uncaught exception: Error: assert_equals: expected "http://web-platform.test:8000/html/semantics/embedded-content/the-iframe-element/support/blank.htm?" but got "http://web-platform.test:8000/html/semantics/embedded-content/the-iframe-element/support/blank.htm?src"
      

  • OK [expected TIMEOUT] /html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_escaping-1.html (#22647)
    • FAIL [expected TIMEOUT] subtest: Check that popups from a sandboxed iframe escape the sandbox if allow-popups-to-escape-sandbox is used

      assert_equals: It came from a sandboxed iframe expected "null" but got "http://web-platform.test:8000"
      

  • OK [expected TIMEOUT] /html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_nonescaping-2.html (#22154)
    • FAIL [expected NOTRUN] subtest: Check that popups from a sandboxed iframe do not escape the sandbox

      assert_equals: It came from a sandboxed iframe expected "null" but got "http://web-platform.test:8000"
      

  • TIMEOUT [expected OK] /html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_nonescaping-3.html (#24066)
    • NOTRUN [expected FAIL] subtest: Check that popups from a sandboxed iframe do not escape the sandbox
  • OK /html/semantics/forms/form-submission-0/form-submit-iframe-then-location-navigate.html (#29634)
    • PASS [expected FAIL] subtest: Verifies that location navigations take precedence when following form submissions.
  • TIMEOUT [expected OK] /html/semantics/forms/form-submission-0/reparent-form-during-planned-navigation-task.html (#29724)
    • TIMEOUT [expected PASS] subtest: reparent-form-during-planned-navigation-task

      Test timed out
      

  • OK /navigation-timing/test-navigation-type-reload.html (#33334)
    • PASS [expected FAIL] subtest: Reload domContentLoadedEventStart > Original domContentLoadedEventStart
    • PASS [expected FAIL] subtest: Reload fetchStart > Original fetchStart
  • ERROR /service-workers/idlharness.https.any.html (#36250)
    • PASS [expected TIMEOUT] subtest: ServiceWorkerContainer interface: operation register((TrustedScriptURL or USVString), optional RegistrationOptions)
    • PASS [expected TIMEOUT] subtest: NavigationPreloadManager interface: operation enable()
    • PASS [expected TIMEOUT] subtest: NavigationPreloadManager interface: operation disable()
    • PASS [expected TIMEOUT] subtest: NavigationPreloadManager interface: operation setHeaderValue(ByteString)
    • PASS [expected TIMEOUT] subtest: NavigationPreloadManager interface: operation getState()
Stable unexpected results that are known to be intermittent (17)
  • OK /FileAPI/url/url-with-fetch.any.worker.html (#21517)
    • PASS [expected FAIL] subtest: Revoke blob URL after calling fetch, fetch should succeed
  • TIMEOUT [expected OK] /_mozilla/mozilla/window_resizeTo.html (#36741)
    • TIMEOUT [expected PASS] subtest: Popup onresize event fires after resizeTo

      Test timed out
      

  • OK /_webgl/conformance/textures/misc/texture-upload-size.html (#21770)
    • FAIL [expected PASS] subtest: WebGL test #52

      assert_true: could not create image (SVG) expected true got false
      

  • FAIL [expected PASS] /css/css-sizing/dynamic-available-size-iframe.html (#31559)
  • ERROR /fetch/metadata/generated/serviceworker.https.sub.html (#36247)
    • PASS [expected FAIL] subtest: sec-fetch-site - Same origin, no options - registration
  • OK /fetch/private-network-access/worker-blob-fetch.tentative.window.html (#30064)
    • PASS [expected FAIL] subtest: private to private: success.
  • OK /html/browsers/browsing-the-web/navigating-across-documents/008.html (#24456)
    • PASS [expected FAIL] subtest: Link with onclick form submit to javascript url and href navigation
  • OK /html/browsers/browsing-the-web/navigating-across-documents/refresh/same-document-refresh.html (#34597)
    • FAIL [expected PASS] subtest: Same-Document Referrer from Refresh

      assert_equals: original page loads expected "http://web-platform.test:8000/html/browsers/browsing-the-web/navigating-across-documents/refresh/resources/refresh-with-section.sub.html?url=%23section" but got "http://web-platform.test:8000/html/browsers/browsing-the-web/navigating-across-documents/refresh/resources/refresh-with-section.sub.html?url=%23section#section"
      

  • OK /html/browsers/windows/browsing-context-names/duplicate-name-order.html (#34623)
    • PASS [expected FAIL] subtest: Duplicate name lookup order
  • TIMEOUT /html/canvas/element/manual/imagebitmap/createImageBitmap-origin.sub.html (#31931)
    • TIMEOUT [expected FAIL] subtest: redirected to cross-origin HTMLVideoElement: origin unclear getImageData

      Test timed out
      

    • NOTRUN [expected FAIL] subtest: redirected to cross-origin HTMLVideoElement: origin unclear 2dContext.drawImage
    • NOTRUN [expected FAIL] subtest: redirected to cross-origin HTMLVideoElement: origin unclear bitmaprenderer.transferFromImageBitmap
    • NOTRUN [expected FAIL] subtest: redirected to same-origin HTMLVideoElement: origin unclear getImageData
    • NOTRUN [expected FAIL] subtest: redirected to same-origin HTMLVideoElement: origin unclear 2dContext.drawImage
    • NOTRUN [expected FAIL] subtest: redirected to same-origin HTMLVideoElement: origin unclear bitmaprenderer.transferFromImageBitmap
    • NOTRUN [expected FAIL] subtest: unclean HTMLCanvasElement: origin unclear getImageData
    • NOTRUN [expected FAIL] subtest: unclean HTMLCanvasElement: origin unclear 2dContext.drawImage
    • NOTRUN [expected FAIL] subtest: unclean HTMLCanvasElement: origin unclear bitmaprenderer.transferFromImageBitmap
    • NOTRUN [expected FAIL] subtest: unclean ImageBitmap: origin unclear getImageData
    • And 2 more unexpected results...
  • TIMEOUT /html/interaction/focus/the-autofocus-attribute/supported-elements.html (#24145)
    • TIMEOUT [expected PASS] subtest: Non-HTMLElement should not support autofocus

      Test timed out
      

  • OK [expected CRASH] /html/semantics/embedded-content/media-elements/media_fragment_seek.html (#24114)
  • TIMEOUT /html/semantics/embedded-content/the-canvas-element/security.pattern.fillStyle.sub.html (#36989)
    • FAIL [expected TIMEOUT] subtest: redirected to same-origin HTMLVideoElement: Setting fillStyle to an origin-unclean pattern makes the canvas origin-unclean

      promise_test: Unhandled rejection with value: object "TypeError: argument could not be converted to any of: HTMLImageElement, HTMLCanvasElement, OffscreenCanvas, CSSStyleValue"
      

    • PASS [expected NOTRUN] subtest: unclean HTMLCanvasElement: Setting fillStyle to an origin-unclean pattern makes the canvas origin-unclean
    • FAIL [expected NOTRUN] subtest: unclean ImageBitmap: Setting fillStyle to an origin-unclean pattern makes the canvas origin-unclean

      promise_test: Unhandled rejection with value: object "NotSupportedError: The operation is not supported."
      

    • PASS [expected NOTRUN] subtest: cross-origin HTMLImageElement: Setting fillStyle to an origin-unclean offscreen canvas pattern makes the canvas origin-unclean
    • FAIL [expected NOTRUN] subtest: cross-origin SVGImageElement: Setting fillStyle to an origin-unclean offscreen canvas pattern makes the canvas origin-unclean

      promise_test: Unhandled rejection with value: object "[object Event]"
      

    • FAIL [expected NOTRUN] subtest: cross-origin HTMLVideoElement: Setting fillStyle to an origin-unclean offscreen canvas pattern makes the canvas origin-unclean

      promise_test: Unhandled rejection with value: object "TypeError: argument could not be converted to any of: HTMLImageElement, HTMLCanvasElement, OffscreenCanvas, CSSStyleValue"
      

    • FAIL [expected NOTRUN] subtest: redirected to cross-origin HTMLVideoElement: Setting fillStyle to an origin-unclean offscreen canvas pattern makes the canvas origin-unclean

      promise_test: Unhandled rejection with value: object "TypeError: argument could not be converted to any of: HTMLImageElement, HTMLCanvasElement, OffscreenCanvas, CSSStyleValue"
      

    • TIMEOUT [expected NOTRUN] subtest: redirected to same-origin HTMLVideoElement: Setting fillStyle to an origin-unclean offscreen canvas pattern makes the canvas origin-unclean

      Test timed out
      

  • OK /html/semantics/forms/form-submission-0/urlencoded2.window.html (#28687)
    • PASS [expected FAIL] subtest: application/x-www-form-urlencoded: Basic test (normal form)
    • PASS [expected FAIL] subtest: application/x-www-form-urlencoded: Basic test (formdata event)
  • OK /resize-observer/change-layout-in-error.html (#32629)
    • FAIL [expected PASS] subtest: Changing layout in window error handler should not result in lifecyle loop when resize observer loop limit is reached.

      assert_equals: expected 1 but got 2
      

  • OK /webaudio/the-audio-api/the-audiobuffersourcenode-interface/sub-sample-buffer-stitching.html (#22849)
    • FAIL [expected PASS] subtest: X Stitched sine-wave buffers at sample rate 43800 does not equal [0,0.06264832615852356,0.12505052983760834,0.18696144223213196,0.24813786149024963,0.308339387178421,0.36732959747314453,0.4248766601085663,0.480754554271698,0.5347436666488647,0.5866320133209229,0.6362156271934509,0.6832997798919678,0.7276994585990906,0.7692402601242065,0.8077589869499207...] with an element-wise tolerance of {"absoluteThreshold":0.0038986,"relativeThreshold":0}. Index Actual Expected AbsError RelError Test threshold [14650] -6.2267988108646952e-15 8.6956524848937988e-1 8.6956524848938610e-1 1.0000000000000071e+0 3.8985999999999999e-3 [14651] 3.0547976493835449e-1 8.9879405498504639e-1 5.9331429004669189e-1 6.6012262403823208e-1 3.8985999999999999e-3 Max AbsError of 8.6956524848938610e-1 at index of 14650. Max RelError of 1.0000000000000071e+0 at index of 14650.

      assert_true: expected true got false
      

    • FAIL [expected PASS] subtest: X SNR (42.96525288004421 dB) is not greater than or equal to 65.737. Got 42.96525288004421.

      assert_true: expected true got false
      

  • ERROR [expected OK] /workers/constructors/Worker/Worker-constructor.html (#22991)
Stable unexpected results (4)
  • FAIL [expected PASS] /_mozilla/css/input_placeholder.html
  • OK /content-security-policy/form-action/form-action-src-default-ignored.sub.html
    • FAIL [expected PASS] subtest: Expecting logs: ["PASS","TEST COMPLETE"]

      assert_unreached: Fail Reached unreachable code
      

  • FAIL [expected PASS] /html/editing/editing-0/spelling-and-grammar-checking/spelling-markers-009.html
  • FAIL [expected PASS] /html/editing/editing-0/spelling-and-grammar-checking/spelling-markers-010.html

@github-actions
Copy link
Copy Markdown

⚠️ Try run (#15159140690) failed.

@stevennovaryo
Copy link
Copy Markdown
Contributor Author

For the WPT test:

  • /_mozilla/css/input_placeholder.html
    I am not sure about the nature of this test, but the changes will set placeholder's text color to be grey and the test check whether the input type=text element that show placeholder is matching with the one with a value.
  • /html/editing/editing-0/spelling-and-grammar-checking/spelling-markers-009.html &
    /html/editing/editing-0/spelling-and-grammar-checking/spelling-markers-010.html
    Not sure about the details, but text rendered in spelling-markers-001-ref.html seems to inaccurate (i.e. letter "f" being clipped).
    • spelling-markers-001-ref.html
      image
    • spelling-markers-010.html
      image
  • /content-security-policy/form-action/form-action-src-default-ignored.sub.html
    This happens due to csp style: self rejecting the style that is parsed inside <style> element. Internal pseudo element selector would solve this, but I wonder if we should change how we are modifying the style of UA shadow DOM or possibly exclude that from CSP checking.

@stevennovaryo stevennovaryo marked this pull request as ready for review May 22, 2025 05:25
Copy link
Copy Markdown
Contributor

@xiaochengh xiaochengh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't done a full review yet. Just some initial comments.

let text_shadow_tree = self.text_shadow_tree(can_gc);
let value = self.Value();

let placeholder_text = match value.is_empty() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we don't need to dynamically set the placeholder's text content.

We can use :host(:placeholder-shown) #input-placeholder { visibility: hidden !important } instead.

Copy link
Copy Markdown
Contributor Author

@stevennovaryo stevennovaryo May 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, Thanks!

We still need to change its content whenever placeholder is modified by JavaScript though. But I guess it is much better to cache this.

// TODO: Is there a less hacky way to do this?
let value_text = match value.is_empty() {
false => value,
true => "\u{200B}".into(),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A linebreak (\n) is enough.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found that line break \n wouldn't trigger caret to render right now.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So in Servo <div><br></div> has zero height? That seems like a bug.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Height for <div><br></div> seems normal. For node with line break \n as a text content, I suppose it happens because that kind of whitespace is trimmed somewhere in layout.


/// Whether this node has a serve as the text container for editable content of
/// <input> or <textarea> element.
const IS_TEXT_CONTROL_INNER_EDITOR = 1 << 13;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const IS_TEXT_CONTROL_INNER_EDITOR = 1 << 13;
const IS_TEXT_CONTROL_INNER_EDITOR = 1 << 12;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!


// FIXME: These styles should be inside UA stylesheet, but it is not possible without internal pseudo element support.
const TEXT_TREE_STYLE: &str = "
#input-editor::selection, #input-placeholder::selection {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

placeholder should not be selectable and therefore should not have ::selection style.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, thanks!

}

#input-container {
position: relative !important;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can drop !important if no external CSS can match this element.

I think only the placeholder can be matched via ::placeholder and therefore needs !important

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, thanks!

true => "\u{200B}".into(),
};

// TODO(stevennovaryo): Introduce caching to prevent costly update.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What kind of caching are you suggesting here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The most basic is storing "placeholder" as an attribute in HTMLInputElement. After some thoughts, the current placeholder does have that kind of role for type=text input. I will move the placeholder update into attribute mutated method instead.

// TODO: Is there a less hacky way to do this?
let value_text = match value.is_empty() {
false => value,
true => "\u{200B}".into(),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So in Servo <div><br></div> has zero height? That seems like a bug.

} else if parent_element.is_text_control_inner_editor() {
// We are using parent's style for caret and selection color.
// FIXME: Once textarea and input element's shadow tree is supported, this would replace the if condition above.
let info = NodeAndStyleInfo::new(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this. Shouldn't child style be the same as parent_element?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also don't understand why we need this if branch at all

Copy link
Copy Markdown
Contributor Author

@stevennovaryo stevennovaryo May 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part is also quite strange for me. It does looks like some workarounds for handling selection. At first, I thought that this would affect some selection styling.

But I have been looking at it again, and it seems that only selection query is depending on this one.

let selection_range = info.get_selection_range();
.

@mrobinson
Copy link
Copy Markdown
Member

I think this should be approved by @simonwuelker before landing as he is the one who has the most knowledge about this part of Servo.

@stevennovaryo
Copy link
Copy Markdown
Contributor Author

I think this should be approved by @simonwuelker before landing as he is the one who has the most knowledge about this part of Servo.

Definitely! I am also looking forward for the review.

Signed-off-by: stevennovaryo <[email protected]>
Copy link
Copy Markdown
Contributor

@simonwuelker simonwuelker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks mostly good to me, though I'm not sure if we actually need the IS_TEXT_CONTROL_INNER_EDITOR flag (see my question about the if branch in dom_traversal.rs.

Also, right now all input types that don't have their own dedicated widget render as a text input. Could we fall back to your type=text shadow tree for those too? That way we don't end up with multiple text input implementations.

} else if parent_element.is_text_control_inner_editor() {
// We are using parent's style for caret and selection color.
// FIXME: Once textarea and input element's shadow tree is supported, this would replace the if condition above.
let info = NodeAndStyleInfo::new(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also don't understand why we need this if branch at all

@stevennovaryo
Copy link
Copy Markdown
Contributor Author

stevennovaryo commented May 26, 2025

though I'm not sure if we actually need the IS_TEXT_CONTROL_INNER_EDITOR

There are three usage of IS_TEXT_CONTROL_INNER_EDITOR in the changes, that is marking a specific node to:

  • delegate focus for the element inside input shadow DOM;
  • resolve special case of line height for an input element; and,
  • retrieve selection range of its shadow host.

I suppose it could be assumed as a temporary flag, as pseudo element marking could replace this.

Also, right now all input types that don't have their own dedicated widget render as a text input. Could we fall back to your type=text shadow tree for those too? That way we don't end up with multiple text input implementations.

There should be a incompatibility with input that was implemented halfway through, like password, file, option, and checkbox. I was planning to tackle this more on the implementation of type=password input's widget.

@stevennovaryo stevennovaryo requested a review from xiaochengh May 29, 2025 08:03
@stevennovaryo stevennovaryo force-pushed the input-type-text branch 2 times, most recently from a57f22f to a7175c5 Compare May 29, 2025 15:55
Signed-off-by: stevennovaryo <[email protected]>
Copy link
Copy Markdown
Contributor

@xiaochengh xiaochengh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with a nit

Signed-off-by: stevennovaryo <[email protected]>
Signed-off-by: stevennovaryo <[email protected]>
@xiaochengh xiaochengh added this pull request to the merge queue May 30, 2025
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks May 30, 2025
@stevennovaryo
Copy link
Copy Markdown
Contributor Author

I'm not sure what happened with the merge queue. All WPT tests seem to be canceled.

@jdm jdm added this pull request to the merge queue May 30, 2025
@mrobinson
Copy link
Copy Markdown
Member

I'm not sure what happened with the merge queue. All WPT tests seem to be canceled.

I believe what happened was that the unit test job failed, and job failures have the ability to cancel future jobs.

Merged via the queue into servo:main with commit 5580704 May 30, 2025
21 checks passed
vlindhol added a commit to vlindhol/servo that referenced this pull request Jun 1, 2025
* main: (510 commits)
  DevTools: Fix empty `debugger > source` panel (servo#37197)
  dom: implement signal abort on controller and signal (servo#37192)
  build(deps): bump parking_lot from 0.12.3 to 0.12.4 (servo#37199)
  layout: Split overflow calculation after fragment tree construction (servo#37203)
  build(deps): bump parking_lot_core from 0.9.10 to 0.9.11 (servo#37202)
  build(deps): bump lock_api from 0.4.12 to 0.4.13 (servo#37201)
  build(deps): bump cc from 1.2.24 to 1.2.25 (servo#37198)
  Constellation can now optionally report memory usage when the page is loaded. (servo#37151)
  Implement Input `type=text` UA Shadow DOM (servo#37065)
  constellation: Wait for canvas thread to shut down before shutting down system font service (servo#37182)
  Add slot default display style test (servo#37189)
  Send synthetic keydown/keyup at ime_insert_text (servo#37175)
  script: Let canvas serialization to image fail gracefully (servo#37184)
  Implement basics of link preloading (servo#37036)
  compositor: Add an initial RefreshDriver (servo#37169)
  pixels: Add limitation to max image total bytes length (servo#37172)
  Chore: Remove unused variable in `transition-zero-duration-with-delay.html` (servo#37179)
  build(deps): bump ohos-ime from 0.2.0 to 0.3.0 (servo#37180)
  Add a user agent style for the `<slot>` element (servo#37174)
  build(deps): bump hitrace from 0.1.4 to 0.1.5 (servo#37170)
  ...
webbeef added a commit to webbeef/servo that referenced this pull request Jun 6, 2025
github-merge-queue bot pushed a commit that referenced this pull request Jun 6, 2025
This reverts commit 5580704.

Let's re-land that fix when a working solution is found. Keeping that
regression makes it hard to evaluate other potential improvements.

Signed-off-by: webbeef <[email protected]>
stevennovaryo added a commit to stevennovaryo/servo that referenced this pull request Jun 9, 2025
Implement Shadow Tree construction for input `type=text`, adding a text
control inner editor container and placeholder container. Subsequently,
due to the changes of the DOM tree structure, the changes will add a new
NodeFlag `IS_TEXT_CONTROL_INNER_EDITOR` to handle the following cases.
- If a mouse click button event hits a text control inner editor, it
will redirect the focus target to its shadow host.
- In text run's construction, the text control inner editor container
queries the selection from its shadow host. This is later used to
resolve caret and selection painting in the display list.

This will be the first step of fixing input `type=text` and other
single-line text input element widgets. Such as, implementing
`::placeholder` selector.



Testing: Existing WPT test and new Servo specific appearance WPT.
Fixes: servo#36307

---------

Signed-off-by: stevennovaryo <[email protected]>
stevennovaryo added a commit to stevennovaryo/servo that referenced this pull request Jun 9, 2025
stevennovaryo added a commit to stevennovaryo/servo that referenced this pull request Jun 10, 2025
Implement Shadow Tree construction for input `type=text`, adding a text
control inner editor container and placeholder container. Subsequently,
due to the changes of the DOM tree structure, the changes will add a new
NodeFlag `IS_TEXT_CONTROL_INNER_EDITOR` to handle the following cases.
- If a mouse click button event hits a text control inner editor, it
will redirect the focus target to its shadow host.
- In text run's construction, the text control inner editor container
queries the selection from its shadow host. This is later used to
resolve caret and selection painting in the display list.

This will be the first step of fixing input `type=text` and other
single-line text input element widgets. Such as, implementing
`::placeholder` selector.



Testing: Existing WPT test and new Servo specific appearance WPT.
Fixes: servo#36307

---------

Signed-off-by: stevennovaryo <[email protected]>
github-merge-queue bot pushed a commit that referenced this pull request Jul 23, 2025
)

Depends on #37427.

In addition to the changes introduced by
#37065, there are several performance
improvements and nits as follows:
- Use the internal pseudo element for style matching, this will reduce
the performance regression by ~66%.
- Manual construction of the `Text` node inside a text container. This
is followed by the modification of the inner `Text` node instead of
using `SetTextContent` which is more expensive.
- Use `implemented_pseudo_element` instead of
`text_control_inner_editor` `NodeFlag` to handle the special cases that
these elements should follow, specifically the:
  - focus delegation workaround;
  - selections; and
  - line height resolving.
- More documentation.

Servo's side of: servo/stylo#217

Testing: No new unexpected WPT failure, except for the one introduced by
#37065.
Fixes: #36307 #37205

---------

Signed-off-by: stevennovaryo <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Misalignment of Input type="text" Content

5 participants