Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions docs/validation/code-oss-object-fit-789.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Code OSS object-fit and object-position implementation (#789)

## Product trigger

Unchanged Code OSS uses `object-fit` in avatars, account imagery, thumbnails,
extension surfaces, and session UI. WebScene previously exposed neither
`object-fit` nor `object-position`, and every retained `<img>` stretched to its
layout box.

## Implemented slice

- Adds generated native and managed property identities plus CSSOM aliases.
- Supports `fill`, `contain`, `cover`, `none`, and `scale-down`.
- Supports one- and two-component positions with horizontal/vertical keywords,
percentages, supported lengths, and existing math-function parsing.
- Serializes computed position keywords as browser-shaped percentages while
preserving the authored inline declaration.
- Computes image destination geometry from intrinsic dimensions and the content
box without changing layout geometry.
- Publishes an explicit content clip around retained raster and SVG image
commands, including the inner rounded corners.
- Preserves the active rectangular clip when Flutter defers retained SVG
rasterization.
- Treats CSSOM mutations as paint-only scene invalidations.

The bounded position parser deliberately leaves three- and four-component
edge-offset syntax outside this slice because it is not present in the current
Code OSS inventory.

## Authored gates

- `contracts/css-object-fit-position.html`: CSSOM defaults, cascade, mutation,
removal, intrinsic dimensions, and unchanged element geometry.
- `test_image_elements_load_and_reach_scene`: exact `cover`/right-bottom and
`contain`/center destination and clip commands.
- `test_percentage_radius_reaches_raster_image_scene_clip`: rounded content clip
regression.
- `object-fit-performance`: 4,096 images, ten style transitions, zero added
layout passes, and a five-second guardrail.

## Evidence state

Implementation and gates are authored from source review. Per the active fast
merge direction, no build, test, WPT, visual, package, or performance command
was executed for this change. Runtime, exact-pixel, and benchmark evidence is
therefore zero until the later full-validation pass.
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,9 @@
{"id": "container_name", "name": "container-name"},
{"id": "content_visibility", "name": "content-visibility"},
{"id": "contain_intrinsic_size", "name": "contain-intrinsic-size"},
{"id": "aspect_ratio", "name": "aspect-ratio"}
{"id": "aspect_ratio", "name": "aspect-ratio"},
{"id": "object_fit", "name": "object-fit", "aliases": ["objectfit"]},
{"id": "object_position", "name": "object-position", "aliases": ["objectposition"]}
],
"nativeStorageOnlyProperties": [
"accent-color", "align-content", "animation-direction", "animation-fill-mode", "animation-play-state",
Expand All @@ -170,12 +172,12 @@
"all", "display", "position", "floating", "flex_direction", "flex_wrap", "align_items", "align_self",
"justify_content", "box_sizing", "vertical_align", "grid_auto_flow", "border_collapse", "table_layout", "border_style", "background_repeat",
"visibility", "pointer_events", "text_anchor", "font_smoothing", "text_align", "text_transform", "white_space", "word_break", "overflow_wrap", "list_style_position",
"list_style_type", "scrollbar_width", "container", "container_type", "content_visibility"
"list_style_type", "scrollbar_width", "container", "container_type", "content_visibility", "object_fit"
],
"componentList": [
"container_name", "background_position", "background_size", "transition", "transition_property", "transition_duration",
"transition_delay", "transition_timing_function", "animation", "animation_name", "animation_duration", "animation_delay",
"animation_timing_function", "animation_iteration_count", "scrollbar_color"
"animation_timing_function", "animation_iteration_count", "scrollbar_color", "object_position"
],
"length": [
"width", "height", "min_width", "min_height", "max_width", "max_height", "left", "top",
Expand Down Expand Up @@ -213,7 +215,8 @@
"content", "aspect_ratio", "vertical_align", "grid_auto_columns", "grid_auto_flow", "table_layout", "border_inline", "border_block",
"outline", "outline_width", "outline_color", "animation", "animation_name", "animation_duration", "animation_delay",
"animation_timing_function", "animation_iteration_count", "background_repeat", "background_position", "background_size",
"font", "text_transform", "word_break", "overflow_wrap", "list_style", "list_style_position", "list_style_type"
"font", "text_transform", "word_break", "overflow_wrap", "list_style", "list_style_position", "list_style_type",
"object_fit", "object_position"
],
"managedKnownProperties": [
"align-content", "align-items", "align-self", "all", "background", "background-color",
Expand All @@ -234,7 +237,7 @@
"stroke-linecap", "stroke-linejoin", "stroke-width", "text-align", "text-indent", "text-transform",
"top", "transform", "visibility", "white-space", "word-break", "overflow-wrap", "width", "word-spacing", "z-index",
"outline", "outline-color", "outline-offset", "outline-style", "outline-width",
"grid-template-areas", "aspect-ratio"
"grid-template-areas", "aspect-ratio", "object-fit", "object-position"
],
"supportedPropertyExtras": [
"animation", "animation-delay", "animation-direction", "animation-duration", "animation-fill-mode",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,10 @@ inline constexpr std::array native_typed_property_identity_catalog{
native_typed_property_identity{"content-visibility", css_property_id::content_visibility},
native_typed_property_identity{"contain-intrinsic-size", css_property_id::contain_intrinsic_size},
native_typed_property_identity{"aspect-ratio", css_property_id::aspect_ratio},
native_typed_property_identity{"object-fit", css_property_id::object_fit},
native_typed_property_identity{"objectfit", css_property_id::object_fit},
native_typed_property_identity{"object-position", css_property_id::object_position},
native_typed_property_identity{"objectposition", css_property_id::object_position},
};

inline constexpr std::array<std::string_view, 62> native_storage_only_property_catalog{
Expand Down Expand Up @@ -430,6 +434,8 @@ inline constexpr std::array native_modeled_property_mask_catalog{
true,
true,
false,
false,
false,
};

inline constexpr bool generated_property_has_modeled_mask(
Expand Down Expand Up @@ -626,6 +632,8 @@ inline constexpr std::array native_property_grammar_catalog{
native_property_grammar::keyword,
native_property_grammar::length,
native_property_grammar::complex,
native_property_grammar::keyword,
native_property_grammar::component_list,
};

inline constexpr native_property_grammar generated_property_grammar(
Expand Down Expand Up @@ -847,5 +855,9 @@ inline css_property_id generated_property_id_lowercase(std::string_view name) no
if (name == "content-visibility") return css_property_id::content_visibility;
if (name == "contain-intrinsic-size") return css_property_id::contain_intrinsic_size;
if (name == "aspect-ratio") return css_property_id::aspect_ratio;
if (name == "object-fit") return css_property_id::object_fit;
if (name == "objectfit") return css_property_id::object_fit;
if (name == "object-position") return css_property_id::object_position;
if (name == "objectposition") return css_property_id::object_position;
return css_property_id::unknown;
}
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,5 @@ container_name,
content_visibility,
contain_intrinsic_size,
aspect_ratio,
object_fit,
object_position,
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ inline constexpr std::array cssom_supported_property_catalog{
cssom_supported_property_metadata{"min-height", "minHeight"},
cssom_supported_property_metadata{"min-width", "minWidth"},
cssom_supported_property_metadata{"moz-transform", "mozTransform"},
cssom_supported_property_metadata{"object-fit", "objectFit"},
cssom_supported_property_metadata{"object-position", "objectPosition"},
cssom_supported_property_metadata{"opacity", "opacity"},
cssom_supported_property_metadata{"order", "order"},
cssom_supported_property_metadata{"orphans", "orphans"},
Expand Down Expand Up @@ -249,4 +251,4 @@ inline constexpr std::array cssom_supported_property_catalog{
cssom_supported_property_metadata{"zoom", "zoom"},
};

inline constexpr auto cssom_style_template_property_accessor_count = 441U;
inline constexpr auto cssom_style_template_property_accessor_count = 445U;
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "webscene_css_reset.h"
#include "webscene_css_variables.h"
#include "webscene_css_scrollbar_values.h"
#include "webscene_css_replaced_values.h"

namespace webscene_native::css {
// Apply an already-resolved declaration. The caller owns variable resolution,
Expand Down Expand Up @@ -255,6 +256,7 @@ void apply_resolved_declaration(native_document& document,dom_node& node,
} else if (css::apply_paint_value(node,name,value,decision,is_inline,
load_svg)) {
} else if (css::apply_visibility_value(document,node,name,value,is_inline)) {
} else if (css::apply_replaced_value(node,name,value,decision)) {
} else if (css::apply_text_value(node,name,value,decision,is_inline)) {
} else if (name == "border-style") {
decision.classification = "partially-supported";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,8 @@ inline void reset_cascaded_style(dom_node& node,
}
textual->word_break.clear();
textual->overflow_wrap.clear();
textual->object_fit.clear();
textual->object_position.clear();
}
if ((node.style.inline_property_mask & inline_font_weight) == 0U) node.style.font_weight = 0;
if ((node.style.inline_property_mask & inline_line_height) == 0U) node.style.line_height = -1;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
#pragma once
#include "webscene_css_property_mask.h"
#include <cmath>
#include <cstdlib>
#include <sstream>
#include <utility>

namespace webscene_native::css {
inline bool valid_object_position_component(std::string_view token)
{
if (token == "left" || token == "right" || token == "top"
|| token == "bottom" || token == "center") return true;
if (token.starts_with("calc(") || token.starts_with("min(")
|| token.starts_with("max(")) return true;
const auto value = std::string(token);
char* end = nullptr;
const auto number = std::strtof(value.c_str(), &end);
if (end == value.c_str() || !std::isfinite(number)) return false;
const std::string_view unit(end);
return (number == 0.0F && unit.empty()) || unit == "%" || unit == "px"
|| unit == "em" || unit == "rem" || unit == "vw" || unit == "vh"
|| unit == "cqw" || unit == "cqh" || unit == "cqi" || unit == "cqb"
|| unit == "cqmin" || unit == "cqmax" || unit == "in" || unit == "cm"
|| unit == "mm" || unit == "pt" || unit == "pc" || unit == "q";
}

inline bool horizontal_object_position_keyword(std::string_view token)
{
return token == "left" || token == "right";
}

inline bool vertical_object_position_keyword(std::string_view token)
{
return token == "top" || token == "bottom";
}

inline std::string canonical_object_position_component(std::string value)
{
if (value == "left" || value == "top") return "0%";
if (value == "center") return "50%";
if (value == "right" || value == "bottom") return "100%";
return value;
}

inline bool normalize_object_position(std::string_view raw_value,
std::string& normalized)
{
std::istringstream tokens(std::string{raw_value});
std::string first;
std::string second;
std::string extra;
tokens >> first >> second >> extra;
if (first.empty() || !extra.empty() || !valid_object_position_component(first)
|| (!second.empty() && !valid_object_position_component(second))) {
return false;
}
if (second.empty()) {
if (vertical_object_position_keyword(first)) {
second = first;
first = "50%";
} else {
second = "50%";
}
} else {
if ((horizontal_object_position_keyword(first)
&& horizontal_object_position_keyword(second))
|| (vertical_object_position_keyword(first)
&& vertical_object_position_keyword(second))) {
return false;
}
if (vertical_object_position_keyword(first)
|| horizontal_object_position_keyword(second)) {
std::swap(first, second);
}
}
normalized = canonical_object_position_component(std::move(first)) + " "
+ canonical_object_position_component(std::move(second));
return true;
}

inline bool valid_object_position_value(std::string_view raw_value)
{
const auto value = ascii_lower(trim_value(raw_value));
if (value.find("var(") != std::string::npos) return true;
if (value == "inherit" || value == "initial" || value == "unset"
|| value == "revert" || value == "revert-layer") return true;
std::string normalized;
return normalize_object_position(value, normalized);
}

template<typename Decision>
bool apply_replaced_value(dom_node& node,const std::string& name,
const std::string& raw_value,Decision& decision)
{
if (name != "object-fit" && name != "object-position") return false;
auto value = ascii_lower(trim_value(raw_value));
if (value == "inherit") {
value = node.parent == nullptr ? std::string{}
: name == "object-fit" ? node.parent->style.textual().object_fit
: node.parent->style.textual().object_position;
} else if (value == "initial" || value == "unset" || value == "revert"
|| value == "revert-layer") {
value.clear();
}
if (name == "object-fit") {
if (!value.empty() && value != "fill" && value != "contain"
&& value != "cover" && value != "none" && value != "scale-down") {
decision.classification = "invalid-authoring";
return true;
}
node.style.mutable_textual().object_fit = std::move(value);
return true;
}
if (!value.empty()) {
std::string normalized;
if (!normalize_object_position(value, normalized)) {
decision.classification = "invalid-authoring";
return true;
}
value = std::move(normalized);
}
node.style.mutable_textual().object_position = std::move(value);
return true;
}
} // namespace webscene_native::css
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,14 @@ inline void apply_all_unset(
reset.mutable_textual().overflow_wrap =
previous.textual().overflow_wrap;
}
if (has_inline({"object-fit"})) {
reset.mutable_textual().object_fit =
previous.textual().object_fit;
}
if (has_inline({"object-position"})) {
reset.mutable_textual().object_position =
previous.textual().object_position;
}

// These modeled properties do not yet have dedicated inline-mask
// bits, so preserve their applied values by authored declaration.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ struct specified_property_sample final { std::string_view name; std::string_view

inline constexpr auto specified_property_samples = std::to_array<specified_property_sample>({
{"all","unset"},{"content","\"x\""},{"aspect-ratio","16 / 9"},
{"object-fit","cover"},{"object-position","right 25%"},
{"width","12px"},{"height","20%"},{"min-width","1rem"},{"min-height","2px"},
{"max-width","none"},{"max-height","fit-content"},
{"left","1px"},{"top","2px"},{"right","3px"},{"bottom","4px"},{"inset","1px 2px 3px 4px"},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,10 @@ struct node_style final {
// state to descendants; the root resolves both properties to normal.
std::string word_break;
std::string overflow_wrap;
// Replaced-image paint is cold. Empty values represent the initial
// `fill` and `50% 50%` values without enlarging the hot node record.
std::string object_fit;
std::string object_position;
std::string contain_value;
std::string container_type{"normal"};
std::string container_name{"none"};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,8 @@ native_document::allocation_metrics native_document::read_allocation_metrics() c
+ textual->white_space.capacity() + 1U
+ textual->word_break.capacity() + 1U
+ textual->overflow_wrap.capacity() + 1U
+ textual->object_fit.capacity() + 1U
+ textual->object_position.capacity() + 1U
+ textual->cursor.capacity() + 1U
+ textual->svg_fill.capacity() + 1U
+ textual->svg_stroke.capacity() + 1U
Expand Down
Loading
Loading