Skip to content
Open
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
67 changes: 45 additions & 22 deletions src/css/properties/transition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -365,9 +365,10 @@ mod transition_handler_body {
let mut _timing_functions: Option<(SmallList<EasingFunction, 1>, VendorPrefix)> =
self.timing_functions.take();

let mut sources: Option<Vec<u32>> = None;
let mut rtl_properties: Option<SmallList<PropertyId, 1>> =
if let Some(p) = &mut _properties {
expand_properties(&mut p.0, arena, context)
expand_properties(&mut p.0, &mut sources, arena, context)
Comment thread
robobun marked this conversation as resolved.
Resolved
} else {
None
};
Expand All @@ -389,13 +390,20 @@ mod transition_handler_body {
let intersection =
*property_prefixes & *duration_prefixes & *delay_prefixes & *timing_prefixes;
if !intersection.is_empty() {
let transitions =
get_transitions(arena, properties, durations, delays, timing_functions);
let transitions = get_transitions(
Comment thread
robobun marked this conversation as resolved.
Resolved
arena,
properties,
sources.as_deref(),
durations,
delays,
timing_functions,
);

if let Some(rtl_properties2) = &mut rtl_properties {
let rtl_transitions = get_transitions(
arena,
rtl_properties2,
sources.as_deref(),
durations,
delays,
timing_functions,
Expand Down Expand Up @@ -468,40 +476,35 @@ mod transition_handler_body {
fn get_transitions(
arena: &bun_alloc::Arena,
properties: &mut SmallList<PropertyId, 1>,
sources: Option<&[u32]>,
durations: &mut SmallList<Time, 1>,
delays: &mut SmallList<Time, 1>,
timing_functions: &mut SmallList<EasingFunction, 1>,
) -> SmallList<Transition, 1> {
#[inline]
fn cycle_bump(idx: &mut u32, len: u32) {
*idx = (*idx + 1) % len;
}

// transition-property determines the number of transitions. The values of other
// properties are repeated to match this length.
let mut transitions = SmallList::<Transition, 1>::init_capacity(1);
let mut durations_idx: u32 = 0;
let mut delays_idx: u32 = 0;
let mut timing_idx: u32 = 0;
for property_id in properties.slice() {
let duration = if durations.len() > durations_idx {
durations.at(durations_idx).deep_clone(arena)
debug_assert!(sources.is_none_or(|sources| sources.len() == properties.slice().len()));
for (index, property_id) in (0u32..).zip(properties.slice()) {
// Each name takes the values of its entry as written. lightningcss matches by position, which shifts them.
let source = sources.map_or(index, |sources| sources[index as usize]);
let duration = if !durations.is_empty() {
durations.at(source % durations.len()).deep_clone(arena)
Comment thread
robobun marked this conversation as resolved.
Resolved
} else {
Time::Seconds(0.0)
};
let delay = if delays.len() > delays_idx {
delays.at(delays_idx).deep_clone(arena)
let delay = if !delays.is_empty() {
delays.at(source % delays.len()).deep_clone(arena)
} else {
Time::Seconds(0.0)
};
let timing_function = if timing_functions.len() > timing_idx {
timing_functions.at(timing_idx).deep_clone(arena)
let timing_function = if !timing_functions.is_empty() {
timing_functions
.at(source % timing_functions.len())
.deep_clone(arena)
} else {
EasingFunction::Ease
};
cycle_bump(&mut durations_idx, durations.len());
cycle_bump(&mut delays_idx, delays.len());
cycle_bump(&mut timing_idx, timing_functions.len());
let transition = Transition {
property: property_id.deep_clone(arena),
duration,
Expand All @@ -525,6 +528,7 @@ mod transition_handler_body {

fn expand_properties(
properties: &mut SmallList<PropertyId, 1>,
sources: &mut Option<Vec<u32>>,
arena: &bun_alloc::Arena,
context: &mut PropertyHandlerContext,
) -> Option<SmallList<PropertyId, 1>> {
Expand All @@ -541,11 +545,24 @@ mod transition_handler_body {
}
}

// Once an entry becomes several names, each name keeps the index of its entry as written.
#[cold]
fn record_names(sources: &mut Option<Vec<u32>>, source: u32, names: u32, capacity: u32) {
let sources = sources.get_or_insert_with(|| {
let mut sources = Vec::with_capacity(capacity as usize);
sources.extend(0..source);
sources
});
sources.resize(sources.len() + names as usize, source);
}

let mut rtl_properties: Option<SmallList<PropertyId, 1>> = None;
let mut i: u32 = 0;
let mut source: u32 = 0;

// Expand logical properties in place.
while i < properties.len() {
let start = i;
let result = get_logical_properties(properties.at(i));
match result {
LogicalPropertyId::Block(feature, block)
Expand All @@ -555,7 +572,7 @@ mod transition_handler_body {
if let Some(rtl) = &mut rtl_properties {
replace(arena, rtl, block, i);
}
i += 1;
i += u32::try_from(block.len()).expect("int cast");
}
LogicalPropertyId::Inline(feature, ltr, rtl)
if context.should_compile_logical(feature) =>
Expand Down Expand Up @@ -611,6 +628,12 @@ mod transition_handler_body {
i += 1;
}
}

let names = i - start;
if names > 1 || sources.is_some() {
record_names(sources, source, names, properties.len());
}
source += 1;
}

rtl_properties
Expand Down
25 changes: 25 additions & 0 deletions test/bundler/css/mask-geometry-box.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,29 @@ describe("css", () => {
expect(output).toContain("padding-box");
},
});

// The default browser targets add `-webkit-mask-box-image` in front of `mask-border`. Both keep the 2s.
for (const backend of ["cli", "api"] as const) {
itBundled(`css/transition-mask-border-keeps-its-duration-${backend}`, {
backend,
files: {
"index.css": /* css */ `
.a {
transition: mask-border 2s, opacity 3s;
}
`,
},
outdir: "/out",
entryPoints: ["/index.css"],
onAfterBundle(api) {
api.expectFile("/out/index.css").toMatchInlineSnapshot(`
"/* index.css */
.a {
transition: -webkit-mask-box-image 2s, mask-border 2s, opacity 3s;
}
"
`);
},
});
}
});
147 changes: 147 additions & 0 deletions test/js/bun/css/css.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7159,6 +7159,153 @@ describe("css tests", () => {
chrome: Some(95 << 16),
},
);

// One entry of a `transition` can become several names: a logical shorthand becomes its physical longhands,
// and `mask-border` gets `-webkit-mask-box-image`. Each name keeps the values of the entry it came from.
describe("an entry that becomes several names keeps its duration, delay and timing function", () => {
const safari = (version: number) => ({ safari: Some(version << 16) });
const rtlLangs = "ae ar arc bcc bqi ckb dv fa glk he ku mzn nqo pnb ps sd ug ur yi"
.split(" ")
.map(lang => `:lang(${lang})`)
.join(", ");
// The rules that one rule with an inline logical property becomes for Safari 8.
const directionRules = (ltr: string, rtl: string) => `
.foo:not(:-webkit-any(${rtlLangs})) { ${ltr} }
.foo:not(:is(${rtlLangs})) { ${ltr} }
.foo:-webkit-any(${rtlLangs}) { ${rtl} }
.foo:is(${rtlLangs}) { ${rtl} }
`;

prefix_test(
`.foo { transition: mask-border 1s, opacity 2s }`,
`.foo { transition: -webkit-mask-box-image 1s, mask-border 1s, opacity 2s; }`,
safari(15),
);
prefix_test(
`.foo { transition: inset 1s, opacity 2s }`,
`.foo { transition: top 1s, bottom 1s, left 1s, right 1s, opacity 2s; }`,
safari(8),
);
prefix_test(
`.foo { transition: margin-block 1s, opacity 2s, color 3s }`,
`.foo { transition: margin-top 1s, margin-bottom 1s, opacity 2s, color 3s; }`,
safari(8),
);
prefix_test(
`.foo { transition: border-block-color 1s, border-inline-width 2s, opacity 3s }`,
`.foo { transition: border-top-color 1s, border-bottom-color 1s, border-left-width 2s, border-right-width 2s, opacity 3s; }`,
safari(8),
);
prefix_test(
`.foo { transition: mask-composite 1s, mask-mode 2s, opacity 3s }`,
`.foo { transition: -webkit-mask-composite 1s, mask-composite 1s, -webkit-mask-source-type 2s, mask-mode 2s, opacity 3s; }`,
safari(15),
);
prefix_test(
`.foo { transition: inset 1s linear 3s, opacity 2s ease-in 4s }`,
`.foo { transition: top 1s linear 3s, bottom 1s linear 3s, left 1s linear 3s, right 1s linear 3s, opacity 2s ease-in 4s; }`,
safari(8),
);
// A shorter list repeats over the entries as written, and a value past the last entry is not used.
prefix_test(
`.foo { transition: inset 1s, opacity 2s, color 3s; transition-duration: 4s, 5s }`,
`.foo { transition: top 4s, bottom 4s, left 4s, right 4s, opacity 5s, color 4s; }`,
safari(8),
);
Comment thread
robobun marked this conversation as resolved.
Resolved
prefix_test(
`.foo { transition: inset 1s; transition-duration: 4s, 5s }`,
`.foo { transition: top 4s, bottom 4s, left 4s, right 4s; }`,
safari(8),
);
// daisyUI ships this shape in its megamenu, with `linear()` as the first easing.
prefix_test(
`.foo { transition: inset .3s cubic-bezier(.2, .9, .3, 1.1), background-color .2s ease-out }`,
`.foo {
transition: top .3s cubic-bezier(.2, .9, .3, 1.1), bottom .3s cubic-bezier(.2, .9, .3, 1.1), left .3s cubic-bezier(.2, .9, .3, 1.1), right .3s cubic-bezier(.2, .9, .3, 1.1), background-color .2s ease-out;
}`,
safari(14),
);
// The order as written stays, so the last entry for a property still decides.
prefix_test(
`.foo { transition: top 5s, inset 1s, left 7s }`,
`.foo { transition: top 5s, top 1s, bottom 1s, left 1s, right 1s, left 7s; }`,
safari(8),
);
prefix_test(
`.foo { transition: all 1s, inset 2s }`,
`.foo { transition: all 1s, top 2s, bottom 2s, left 2s, right 2s; }`,
safari(8),
);
prefix_test(
`.foo { transition: inset 1s, opacity 2s; transition-duration: 3s, 4s }`,
`.foo { transition: top 3s, bottom 3s, left 3s, right 3s, opacity 4s; }`,
safari(8),
);
prefix_test(
`.foo { transition-duration: 3s, 4s; transition: inset 1s, opacity 2s }`,
`.foo { transition: top 1s, bottom 1s, left 1s, right 1s, opacity 2s; }`,
safari(8),
);
// The handler writes what it holds when a prefixed value differs or a longhand is not parsed.
prefix_test(
`.foo { -webkit-transition: inset 1s, opacity 2s; transition: inset 3s, opacity 4s }`,
`.foo {
-webkit-transition: top 1s, bottom 1s, left 1s, right 1s, opacity 2s;
transition: top 3s, bottom 3s, left 3s, right 3s, opacity 4s;
}`,
safari(8),
);
prefix_test(
`.foo { transition: inset 1s, opacity 2s; transition-timing-function: var(--ease) }`,
`.foo {
transition: top 1s, bottom 1s, left 1s, right 1s, opacity 2s;
transition-timing-function: var(--ease);
}`,
safari(8),
);
prefix_test(
`.foo { transition: inset 1s, opacity 2s, transform 3s }`,
`.foo {
-webkit-transition: top 1s, bottom 1s, left 1s, right 1s, opacity 2s, -webkit-transform 3s, transform 3s;
transition: top 1s, bottom 1s, left 1s, right 1s, opacity 2s, -webkit-transform 3s, transform 3s;
}`,
safari(6),
);
// Both direction rules, with the inline logical property in front of the entry and behind it.
prefix_test(
`.foo { transition: margin-inline-start 1s, inset 2s, opacity 3s }`,
directionRules(
`transition: margin-left 1s, top 2s, bottom 2s, left 2s, right 2s, opacity 3s;`,
`transition: margin-right 1s, top 2s, bottom 2s, left 2s, right 2s, opacity 3s;`,
),
safari(8),
);
prefix_test(
`.foo { transition: inset 1s, margin-inline-start 2s, opacity 3s }`,
directionRules(
`transition: top 1s, bottom 1s, left 1s, right 1s, margin-left 2s, opacity 3s;`,
`transition: top 1s, bottom 1s, left 1s, right 1s, margin-right 2s, opacity 3s;`,
),
safari(8),
);
// Prints `color 1s, opacity 1s`: the later property list meets one duration per name of the first list.
cssTestTodo(
`.foo { transition: inset 1s, opacity 2s } .foo { transition-property: color, opacity }`,
`.foo { transition: color 1s, opacity 2s; }`,
safari(8),
);

// Nothing becomes several names without targets, or for targets that need nothing.
minify_test(
".foo { transition: inset 1s, mask-border 2s, transform 3s }",
".foo{transition:inset 1s,mask-border 2s,transform 3s}",
);
prefix_test(
`.foo { transition: inset 1s, mask-border 2s, transform 3s, opacity 4s }`,
`.foo { transition: inset 1s, mask-border 2s, transform 3s, opacity 4s; }`,
{ chrome: Some(130 << 16) },
);
});
});

describe("animation", () => {
Expand Down
Loading