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
25 changes: 25 additions & 0 deletions docs/validation/code-oss-scrollbar-parts-785.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Code OSS retained scrollbar part state (#785)

## Current-main boundary

WebScene already projects authored base `::-webkit-scrollbar` width/height and visibility, thumb/track color and radius, content-box transparent borders, proportional overlay geometry, scrolling, and primary-button thumb dragging. This slice keeps that path and fills the remaining declarations reached by unchanged Code OSS 1.137.

The pinned checkout contains 39 `::-webkit-scrollbar-thumb`, 9 track, and 9 corner occurrences across source and generated CSS. Automation dialogs, Issue Reporter, Image Carousel, and Prompt Timeline use thumb `:hover`/`:active`, `min-height`, transparent borders, themed colors, and hidden corners.

## Retained implementation

`::-webkit-scrollbar-thumb:hover` and `:active` receive distinct parse kinds. Matching admits each only while the native thumb owns that state, then folds it into pseudo kind 4 before the existing cascade. Base and state declarations therefore share source order, specificity, layers, rollback, variables, and `!important` handling.

Two interaction bits occupy the existing `dom_node` flag byte. A single runtime pointer identifies the hovered thumb; the existing drag pointer remains the active owner. Pointer transitions recascade only the previous/new scrollers. No pseudo DOM node, document scan, timer, animation frame, or platform widget is introduced.

The lazy textual scrollbar record adds one authored vertical minimum thumb extent, matching the `min-height` declaration used by Code OSS. Scene paint and thumb hit testing consume the same value. Existing transparent-border/content-box inset and absent-corner behavior remain unchanged; the authored Code OSS `display:none` corner rules therefore continue to produce no corner command.

## Authored gates

- Portable compiler protection/restoration retains thumb `:hover` and `:active` selectors.
- Native selector payload contracts retain kinds 11/12 and the common host origin.
- The focused retained-scene contract checks normal, hover, active, 28 px minimum extent, transparent border inset, hidden-corner absence, drag geometry, and scroll event count.
- The performance contract requires pointer-state changes to preserve layout-pass count, publish bounded scene work, avoid sustained frame demand, and keep unrelated scroller cascade work at zero.
- Final product acceptance compares Prompt Timeline, Automation dialog, Issue Reporter, and Image Carousel regions in light/dark Chromium and AppScene captures, then exercises 100 create/scroll/style/remove cycles and a 4,096-scroller mutation fixture with heap/RSS and retained-count return.

These gates are authored but unexecuted under the active implementation-throughput direction. No validation result is claimed by this document.
Original file line number Diff line number Diff line change
Expand Up @@ -1511,6 +1511,7 @@ mod selector_syntax {
| "-webkit-scrollbar"
| "-webkit-scrollbar-thumb"
| "-webkit-scrollbar-track"
| "-webkit-scrollbar-corner"
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ inline int split_pseudo_element_selector(const std::string& selector, std::strin
if (const auto kind = split_suffix("::details-content", 9); kind != 0) return kind;
if (const auto kind = split_suffix("::-webkit-details-marker", 10); kind != 0) return kind;
if (const auto kind = split_suffix("::backdrop", 7); kind != 0) return kind;
// Scrollbar-part interaction belongs to the retained part rather than
// the originating DOM element. Keep distinct parse kinds so matching
// can admit them only while that exact native thumb state is active,
// then fold them back into the thumb's single cascade.
if (const auto kind = split_suffix("::-webkit-scrollbar-thumb:hover", 11); kind != 0) return kind;
if (const auto kind = split_suffix("::-webkit-scrollbar-thumb:active", 12); kind != 0) return kind;
if (const auto kind = split_suffix("::-webkit-scrollbar-thumb", 4); kind != 0) return kind;
if (const auto kind = split_suffix("::-webkit-scrollbar-track", 5); kind != 0) return kind;
if (const auto kind = split_suffix("::-webkit-scrollbar-corner", 6); kind != 0) return kind;
Expand Down Expand Up @@ -324,6 +330,11 @@ inline void apply_scrollbar_declaration(
if (declaration.name == "background" || declaration.name == "background-color") {
scrollbar.thumb_rgba = lower == "initial" ? 0U
: native_document::parse_color(value);
} else if (declaration.name == "min-height") {
const auto length = native_document::parse_length(value);
if (length.unit == length_unit::pixels) {
scrollbar.thumb_min_height = std::max(0.0F, length.value);
}
} else if (declaration.name == "border-radius") {
scrollbar.thumb_radius = std::max(
0.0F, native_document::parse_length(value).value);
Expand All @@ -348,6 +359,7 @@ inline void apply_scrollbar_declaration(
scrollbar.track_radius = std::max(
0.0F, native_document::parse_length(value).value);
}
return;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,15 @@ rule_matches match_candidates(native_document& document,const dom_node& node,
if (pseudo_kind != 0) {
const auto& origin = rule.payload->compiled_pseudo_origin;
if (!origin.compounds.empty() && match_selector(node,rule,origin)) {
result.pseudo.emplace_back(pseudo_kind, &rule);
if (pseudo_kind == 11 && !node.scrollbar_thumb_hovered) continue;
if (pseudo_kind == 12 && !node.scrollbar_thumb_active) continue;
// All active thumb-state selectors target the same pseudo
// box. Folding them into kind 4 before sorting preserves
// specificity, source order, layers and !important across
// base, :hover and :active rules.
result.pseudo.emplace_back(
pseudo_kind == 11 || pseudo_kind == 12 ? 4 : pseudo_kind,
&rule);
}
continue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ void for_each_cascaded_pseudo_declaration(
// kind prevents a high-priority ::before declaration from affecting
// ::after while reusing the same layer/importance/rollback machinery
// as the originating element.
std::array<std::vector<const css_rule*>, 11> rules_by_kind;
std::array<std::vector<const css_rule*>, 13> rules_by_kind;
for (const auto& [kind, rule] : matched_rules) {
if (kind > 0 && static_cast<size_t>(kind) < rules_by_kind.size())
rules_by_kind[static_cast<size_t>(kind)].push_back(rule);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,7 @@ struct node_style final {
float height{6};
float overlay_inset{2};
float thumb_border_width{0};
float thumb_min_height{18};
float thumb_radius{3};
float track_radius{3};
uint32_t thumb_rgba{0xA0A0A0D0U};
Expand Down Expand Up @@ -1798,6 +1799,11 @@ struct dom_node final {
// display state reaches or leaves `none`.
bool css_cascade_initialized : 1 {false};
bool css_was_rendered : 1 {false};
// Retained scrollbar pseudo state is independent of the host element's
// :hover/:active state. These use the remaining bits in the existing node
// flag byte and therefore do not increase the hot dom_node footprint.
bool scrollbar_thumb_hovered : 1 {false};
bool scrollbar_thumb_active : 1 {false};
std::string tag;
std::string id_attribute;
std::string class_name;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3646,7 +3646,6 @@ void native_document::append_scene(
const auto maximum_y = std::max(
0.0F, snapped_content_height - snapped_viewport_height);
const auto& scrollbar = node.style.scrollbar();
constexpr auto minimum_thumb = 18.0F;
const auto append_overlay_rect = [&](float x, float y, float width, float height,
uint32_t rgba, float radius) {
if (width <= 0 || height <= 0) return;
Expand All @@ -3672,17 +3671,24 @@ void native_document::append_scene(
const auto vertical_thickness = scrollbar.width;
const auto horizontal_thickness = scrollbar.height;
const auto inset = scrollbar.overlay_inset;
if ((node.style.scroll_y_enabled || root_viewport_scroll_y) && maximum_y > 0.01F
const auto paints_vertical =
(node.style.scroll_y_enabled || root_viewport_scroll_y) && maximum_y > 0.01F
&& vertical_thickness > 0
&& viewport_width >= vertical_thickness + inset * 2
&& viewport_height > 0) {
&& viewport_height > 0;
const auto paints_horizontal =
(node.style.scroll_x_enabled || root_viewport_scroll_x) && maximum_x > 0.01F
&& horizontal_thickness > 0
&& viewport_height >= horizontal_thickness + inset * 2
&& viewport_width > 0;
if (paints_vertical) {
const auto rail_x = viewport_x + viewport_width - vertical_thickness - inset;
const auto rail_y = viewport_y + inset;
const auto rail_height = std::max(0.0F, viewport_height - inset * 2);
const auto thumb_height = std::min(
rail_height,
std::max(
minimum_thumb,
scrollbar.thumb_min_height,
rail_height * snapped_viewport_height
/ std::max(snapped_viewport_height, snapped_content_height)));
const auto thumb_travel = std::max(0.0F, rail_height - thumb_height);
Expand All @@ -3696,17 +3702,14 @@ void native_document::append_scene(
vertical_thickness - border * 2, thumb_height - border * 2,
scrollbar.thumb_rgba, std::max(0.0F, scrollbar.thumb_radius - border));
}
if ((node.style.scroll_x_enabled || root_viewport_scroll_x) && maximum_x > 0.01F
&& horizontal_thickness > 0
&& viewport_height >= horizontal_thickness + inset * 2
&& viewport_width > 0) {
if (paints_horizontal) {
const auto rail_x = viewport_x + inset;
const auto rail_y = viewport_y + viewport_height - horizontal_thickness - inset;
const auto rail_width = std::max(0.0F, viewport_width - inset * 2);
const auto thumb_width = std::min(
rail_width,
std::max(
minimum_thumb,
18.0F,
rail_width * snapped_viewport_width
/ std::max(snapped_viewport_width, snapped_content_width)));
const auto thumb_travel = std::max(0.0F, rail_width - thumb_width);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -697,7 +697,11 @@
const auto pseudo_kind = split_pseudo_element_selector(rule.selector(), pseudo_origin);
if (pseudo_kind != 0) {
if (!pseudo_origin.empty() && css_selector_matches(node, pseudo_origin)) {
matched_pseudo_rules.emplace_back(pseudo_kind, &rule);
if (pseudo_kind == 11 && !node.scrollbar_thumb_hovered) continue;
if (pseudo_kind == 12 && !node.scrollbar_thumb_active) continue;
matched_pseudo_rules.emplace_back(
pseudo_kind == 11 || pseudo_kind == 12 ? 4 : pseudo_kind,
&rule);
}
continue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2101,6 +2101,7 @@
}
if (nodes.contains(active_pointer_target)) active_pointer_target = nullptr;
if (nodes.contains(scrollbar_drag_target)) scrollbar_drag_target = nullptr;
if (nodes.contains(scrollbar_hover_target)) scrollbar_hover_target = nullptr;
if (nodes.contains(hover_target)) hover_target = nullptr;
if (nodes.contains(current_related_target)) current_related_target = nullptr;
released_detached_dom_nodes += document.erase_detached_subtrees(roots);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@
middle_paste_target_id = 0U;
active_pointer_target = nullptr;
scrollbar_drag_target = nullptr;
scrollbar_hover_target = nullptr;
scrollbar_drag_vertical = false;
scrollbar_drag_pointer_origin = 0;
scrollbar_drag_scroll_origin = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,7 @@
bool outbound_drag_threshold_crossed{false};
dom_node* active_pointer_target{nullptr};
dom_node* scrollbar_drag_target{nullptr};
dom_node* scrollbar_hover_target{nullptr};
bool scrollbar_drag_vertical{false};
double scrollbar_drag_pointer_origin{0};
float scrollbar_drag_scroll_origin{0};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3654,7 +3654,6 @@
float x,
float y)
{
constexpr auto minimum_thumb = 18.0F;
for (auto* node = &hit_target; node != nullptr; node = node->parent) {
if (node->style.scrollbar_hidden
|| node->layout.width <= 0 || node->layout.height <= 0) continue;
Expand Down Expand Up @@ -3698,7 +3697,7 @@
const auto rail_y = viewport_y + inset;
const auto rail_height = std::max(0.0F, viewport_height - inset * 2);
const auto thumb_height = std::min(rail_height, std::max(
minimum_thumb,
scrollbar.thumb_min_height,
rail_height * node->scroll_viewport_height
/ std::max(node->scroll_viewport_height, node->scroll_content_height)));
const auto travel = std::max(0.0F, rail_height - thumb_height);
Expand All @@ -3716,7 +3715,7 @@
const auto rail_y = viewport_y + viewport_height - scrollbar.height - inset;
const auto rail_width = std::max(0.0F, viewport_width - inset * 2);
const auto thumb_width = std::min(rail_width, std::max(
minimum_thumb,
18.0F,
rail_width * node->scroll_viewport_width
/ std::max(node->scroll_viewport_width, node->scroll_content_width)));
const auto travel = std::max(0.0F, rail_width - thumb_width);
Expand All @@ -3732,6 +3731,33 @@
return std::nullopt;
}

void update_scrollbar_thumb_interaction(
dom_node* next_hover,
dom_node* next_active)
{
auto* previous_hover = scrollbar_hover_target;
auto* previous_active = scrollbar_drag_target;
scrollbar_hover_target = next_hover;
std::array<dom_node*, 4> affected{
previous_hover, previous_active, next_hover, next_active};
for (size_t index = 0; index < affected.size(); ++index) {
auto* node = affected[index];
if (node == nullptr || !is_connected(*node)) continue;
if (std::find(affected.begin(), affected.begin() + index, node)
!= affected.begin() + index) continue;
const auto hovered = node == next_hover;
const auto active = node == next_active;
if (node->scrollbar_thumb_hovered == hovered
&& node->scrollbar_thumb_active == active) continue;
node->scrollbar_thumb_hovered = hovered;
node->scrollbar_thumb_active = active;
// State selectors are folded into the existing thumb cascade, so
// one affected-node recascade preserves specificity, layers,
// source order and !important without scanning the document.
apply_css_rules(*node);
}
}

bool dispatch_input(const webscene_input_event& input, bool defer_cursor_update = false)
{
struct user_input_dispatch_scope {
Expand Down Expand Up @@ -3812,6 +3838,7 @@
if (pointer_capture_target != nullptr || scrollbar_drag_target != nullptr) {
return true;
}
update_scrollbar_thumb_interaction(nullptr, nullptr);
auto* previous = hover_target;
hover_target = nullptr;
current_related_target = nullptr;
Expand Down Expand Up @@ -3859,6 +3886,23 @@
const auto viewport = viewport_provider();
const auto pointer_outside = input.x < 0 || input.y < 0
|| input.x >= viewport.width || input.y >= viewport.height;
const auto scrollbar_hit_at_pointer = [&]() -> std::optional<scrollbar_thumb_hit> {
if (pointer_outside
|| (input.kind != WEBSCENE_INPUT_POINTER_MOVE
&& input.kind != WEBSCENE_INPUT_POINTER_DOWN
&& input.kind != WEBSCENE_INPUT_POINTER_UP)) return std::nullopt;
auto* start = initial_hit_target;
if (start == nullptr) start = &document.body();
return hit_test_scrollbar_thumb(
*start, static_cast<float>(input.x), static_cast<float>(input.y));
};
auto current_scrollbar_hit = scrollbar_hit_at_pointer();
if (scrollbar_drag_target == nullptr
&& input.kind == WEBSCENE_INPUT_POINTER_MOVE) {
update_scrollbar_thumb_interaction(
current_scrollbar_hit.has_value() ? current_scrollbar_hit->target : nullptr,
nullptr);
}
if (scrollbar_drag_target != nullptr
&& (input.kind == WEBSCENE_INPUT_POINTER_MOVE
|| input.kind == WEBSCENE_INPUT_POINTER_UP)) {
Expand Down Expand Up @@ -3887,6 +3931,10 @@
}
}
if (input.kind == WEBSCENE_INPUT_POINTER_UP) {
update_scrollbar_thumb_interaction(
current_scrollbar_hit.has_value()
? current_scrollbar_hit->target : nullptr,
nullptr);
scrollbar_drag_target = nullptr;
if (pointer_outside) return leave_host();
}
Expand All @@ -3910,9 +3958,16 @@
}
if (input.kind == WEBSCENE_INPUT_POINTER_DOWN
&& ((input.flags >> 8U) & 0xffU) <= 1U) {
if (const auto hit = hit_test_scrollbar_thumb(
if (current_scrollbar_hit.has_value()) {
update_scrollbar_thumb_interaction(
current_scrollbar_hit->target,
current_scrollbar_hit->target);
// Hover declarations can alter the authored minimum extent.
// Re-read the same retained geometry before beginning drag.
current_scrollbar_hit = hit_test_scrollbar_thumb(
*hit_target, static_cast<float>(input.x), static_cast<float>(input.y));
hit.has_value()) {
}
if (const auto& hit = current_scrollbar_hit; hit.has_value()) {
scrollbar_drag_target = hit->target;
scrollbar_drag_vertical = hit->vertical;
scrollbar_drag_pointer_origin = hit->pointer_origin;
Expand All @@ -3922,6 +3977,11 @@
return true;
}
}
if (input.kind == WEBSCENE_INPUT_POINTER_UP) {
update_scrollbar_thumb_interaction(
current_scrollbar_hit.has_value() ? current_scrollbar_hit->target : nullptr,
nullptr);
}
auto* target = pointer_capture_target != nullptr
&& input.kind != WEBSCENE_INPUT_POINTER_DOWN
? pointer_capture_target
Expand Down
Loading
Loading