Embed user agent stylesheets and media control resouces in libservo (#36803)

Embed user agent stylesheets and media control resouces in libservo as
decided in
https://github.com/servo/servo/pull/36788#issuecomment-2845332210

Signed-off-by: webbeef <me@webbeef.org>
This commit is contained in:
webbeef 2025-05-04 11:48:09 -07:00 committed by GitHub
parent 7e2d2ed0ce
commit 3db0194e5a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 26 additions and 73 deletions

View file

@ -16,7 +16,6 @@ use base::Epoch;
use base::id::{PipelineId, WebViewId};
use compositing_traits::CrossProcessCompositorApi;
use constellation_traits::ScrollState;
use embedder_traits::resources::{self, Resource};
use embedder_traits::{UntrustedNodeAddress, ViewportDetails};
use euclid::default::{Point2D as UntypedPoint2D, Rect as UntypedRect, Size2D as UntypedSize2D};
use euclid::{Point2D, Scale, Size2D, Vector2D};
@ -100,6 +99,18 @@ thread_local!(static SEEN_POINTERS: LazyCell<RefCell<HashSet<*const c_void>>> =
LazyCell::new(|| RefCell::new(HashSet::new()))
});
/// A CSS file to style the user agent stylesheet.
static USER_AGENT_CSS: &[u8] = include_bytes!("./stylesheets/user-agent.css");
/// A CSS file to style the Servo browser.
static SERVO_CSS: &[u8] = include_bytes!("./stylesheets/servo.css");
/// A CSS file to style the presentational hints.
static PRESENTATIONAL_HINTS_CSS: &[u8] = include_bytes!("./stylesheets/presentational-hints.css");
/// A CSS file to style the quirks mode.
static QUIRKS_MODE_CSS: &[u8] = include_bytes!("./stylesheets/quirks-mode.css");
/// Information needed by layout.
pub struct LayoutThread {
/// The ID of the pipeline that we belong to.
@ -983,20 +994,12 @@ fn get_ua_stylesheets() -> Result<UserAgentStylesheets, &'static str> {
// FIXME: presentational-hints.css should be at author origin with zero specificity.
// (Does it make a difference?)
let mut user_or_user_agent_stylesheets = vec![
parse_ua_stylesheet(
shared_lock,
"user-agent.css",
&resources::read_bytes(Resource::UserAgentCSS),
)?,
parse_ua_stylesheet(
shared_lock,
"servo.css",
&resources::read_bytes(Resource::ServoCSS),
)?,
parse_ua_stylesheet(shared_lock, "user-agent.css", USER_AGENT_CSS)?,
parse_ua_stylesheet(shared_lock, "servo.css", SERVO_CSS)?,
parse_ua_stylesheet(
shared_lock,
"presentational-hints.css",
&resources::read_bytes(Resource::PresentationalHintsCSS),
PRESENTATIONAL_HINTS_CSS,
)?,
];
@ -1017,11 +1020,8 @@ fn get_ua_stylesheets() -> Result<UserAgentStylesheets, &'static str> {
)));
}
let quirks_mode_stylesheet = parse_ua_stylesheet(
shared_lock,
"quirks-mode.css",
&resources::read_bytes(Resource::QuirksModeCSS),
)?;
let quirks_mode_stylesheet =
parse_ua_stylesheet(shared_lock, "quirks-mode.css", QUIRKS_MODE_CSS)?;
Ok(UserAgentStylesheets {
shared_lock: shared_lock.clone(),

View file

@ -0,0 +1,267 @@
/*
https://html.spec.whatwg.org/multipage/#presentational-hints
*/
@namespace url(http://www.w3.org/1999/xhtml);
pre[wrap] { white-space: pre-wrap; }
div[align=left i] { text-align: -moz-left; }
div[align=right i] { text-align: -moz-right; }
div[align=center i], div[align=middle i] { text-align: -moz-center; }
div[align=justify i] { text-align: justify; }
br[clear=left i] { clear: left; }
br[clear=right i] { clear: right; }
br[clear=all i], br[clear=both i] { clear: both; }
ol[type="1"], li[type="1"] { list-style-type: decimal; }
ol[type=a s], li[type=a s] { list-style-type: lower-alpha; }
ol[type=A s], li[type=A s] { list-style-type: upper-alpha; }
ol[type=i s], li[type=i s] { list-style-type: lower-roman; }
ol[type=I s], li[type=I s] { list-style-type: upper-roman; }
ul[type=none i], li[type=none i] { list-style-type: none; }
ul[type=disc i], li[type=disc i] { list-style-type: disc; }
ul[type=circle i], li[type=circle i] { list-style-type: circle; }
ul[type=square i], li[type=square i] { list-style-type: square; }
table[align=left i] { float: left; }
table[align=right i] { float: right; }
table[align=center i] { margin-left: auto; margin-right: auto; }
:matches(thead, tbody, tfoot, tr, td, th)[align=absmiddle i] {
text-align: center;
}
caption[align=bottom i] { caption-side: bottom; }
p[align=left i], h1[align=left i], h2[align=left i], h3[align=left i], h4[align=left i], h5[align=left i], h6[align=left i] { text-align: left; }
p[align=right i], h1[align=right i], h2[align=right i], h3[align=right i], h4[align=right i], h5[align=right i], h6[align=right i] { text-align: right; }
p[align=center i], h1[align=center i], h2[align=center i], h3[align=center i], h4[align=center i], h5[align=center i], h6[align=center i] { text-align: center; }
p[align=justify i], h1[align=justify i], h2[align=justify i], h3[align=justify i], h4[align=justify i], h5[align=justify i], h6[align=justify i] { text-align: justify; }
thead[valign=top i], tbody[valign=top i], tfoot[valign=top i], tr[valign=top i], td[valign=top i], th[valign=top i] { vertical-align: top; }
thead[valign=middle i], tbody[valign=middle i], tfoot[valign=middle i], tr[valign=middle i], td[valign=middle i], th[valign=middle i] { vertical-align: middle; }
thead[valign=bottom i], tbody[valign=bottom i], tfoot[valign=bottom i], tr[valign=bottom i], td[valign=bottom i], th[valign=bottom i] { vertical-align: bottom; }
thead[valign=baseline i], tbody[valign=baseline i], tfoot[valign=baseline i], tr[valign=baseline i], td[valign=baseline i], th[valign=baseline i] { vertical-align: baseline; }
td[nowrap], th[nowrap] { white-space: nowrap; }
table[rules=none i], table[rules=groups i], table[rules=rows i], table[rules=cols i], table[rules=all i] {
border-style: hidden;
border-collapse: collapse;
}
table:-servo-nonzero-border {
border-style: outset;
}
table[frame=void i] { border-style: hidden; }
table[frame=above i] { border-style: outset hidden hidden hidden; }
table[frame=below i] { border-style: hidden hidden outset hidden; }
table[frame=hsides i] { border-style: outset hidden outset hidden; }
table[frame=lhs i] { border-style: hidden hidden hidden outset; }
table[frame=rhs i] { border-style: hidden outset hidden hidden; }
table[frame=vsides i] { border-style: hidden outset; }
table[frame=box i], table[frame=border i] { border-style: outset; }
table:-servo-nonzero-border > tr > td,
table:-servo-nonzero-border > tr > th,
table:-servo-nonzero-border > thead > tr > td,
table:-servo-nonzero-border > thead > tr > th,
table:-servo-nonzero-border > tbody > tr > td,
table:-servo-nonzero-border > tbody > tr > th,
table:-servo-nonzero-border > tfoot > tr > td,
table:-servo-nonzero-border > tfoot > tr > th {
border-width: 1px;
border-style: inset;
}
table[rules=none i] > tr > td, table[rules=groups i] > tr > td, table[rules=rows i] > tr > td, table[rules=none i] > tr > th, table[rules=groups i] > tr > th, table[rules=rows i] > tr > th,
table[rules=none i] > thead > tr > td, table[rules=groups i] > thead > tr > td, table[rules=rows i] > thead > tr > td, table[rules=none i] > thead > tr > th, table[rules=groups i] > thead > tr > th, table[rules=rows i] > thead > tr > th,
table[rules=none i] > tbody > tr > td, table[rules=groups i] > tbody > tr > td, table[rules=rows i] > tbody > tr > td, table[rules=none i] > tbody > tr > th, table[rules=groups i] > tbody > tr > th, table[rules=rows i] > tbody > tr > th,
table[rules=none i] > tfoot > tr > td, table[rules=groups i] > tfoot > tr > td, table[rules=rows i] > tfoot > tr > td, table[rules=none i] > tfoot > tr > th, table[rules=groups i] > tfoot > tr > th, table[rules=rows i] > tfoot > tr > th {
border-width: 1px;
border-style: none;
}
table[rules=cols i] > tr > td, table[rules=cols i] > tr > th,
table[rules=cols i] > thead > tr > td, table[rules=cols i] > thead > tr > th,
table[rules=cols i] > tbody > tr > td, table[rules=cols i] > tbody > tr > th,
table[rules=cols i] > tfoot > tr > td, table[rules=cols i] > tfoot > tr > th {
border-width: 1px;
border-style: none solid;
}
table[rules=all i] > tr > td, table[rules=all i] > tr > th,
table[rules=all i] > thead > tr > td, table[rules=all i] > thead > tr > th,
table[rules=all i] > tbody > tr > td, table[rules=all i] > tbody > tr > th,
table[rules=all i] > tfoot > tr > td, table[rules=all i] > tfoot > tr > th {
border-width: 1px;
border-style: solid;
}
table[rules=groups i] > colgroup {
border-left-width: 1px;
border-left-style: solid;
border-right-width: 1px;
border-right-style: solid;
}
table[rules=groups i] > tr,
table[rules=groups i] > thead > tr,
table[rules=groups i] > tbody > tr,
table[rules=groups i] > tfoot > tr {
border-top-width: 1px;
border-top-style: solid;
border-bottom-width: 1px;
border-bottom-style: solid;
}
table[rules=rows i] > tr,
table[rules=rows i] > thead > tr,
table[rules=rows i] > tbody > tr,
table[rules=rows i] > tfoot > tr {
border-top-width: 1px;
border-top-style: solid;
border-bottom-width: 1px;
border-bottom-style: solid;
}
hr[align=left] { margin-left: 0; margin-right: auto; }
hr[align=right] { margin-left: auto; margin-right: 0; }
hr[align=center] { margin-left: auto; margin-right: auto; }
hr[color], hr[noshade] { border-style: solid; }
iframe[frameborder="0"], iframe[frameborder=no i] { border: none; }
embed[align=left i], iframe[align=left i], img[type=image i][align=left i], object[align=left i] {
float: left;
}
embed[align=right i], iframe[align=right i], img[type=image i][align=right i], object[align=right i] {
float: right;
}
embed[align=top i], iframe[align=top i], img[type=image i][align=top i], object[align=top i] {
vertical-align: top;
}
embed[align=baseline i], iframe[align=baseline i], img[type=image i][align=baseline i], object[align=baseline i] {
vertical-align: baseline;
}
embed[align=texttop i], iframe[align=texttop i], img[type=image i][align=texttop i], object[align=texttop i] {
vertical-align: text-top;
}
embed[align=absmiddle i], iframe[align=absmiddle i], img[type=image i][align=absmiddle i], object[align=absmiddle i],
embed[align=abscenter i], iframe[align=abscenter i], img[type=image i][align=abscenter i], object[align=abscenter i] {
vertical-align: middle;
}
embed[align=bottom i], iframe[align=bottom i], img[type=image i][align=bottom i], object[align=bottom i] {
vertical-align: bottom;
}
/*
FIXME:
:matches(embed, iframe, img, input[type=image i], object):matches([align=center i], [align=middle i]) {
vertical-align: "aligns the vertical middle of the element with the parent element's baseline."
}
*/
/*
Presentational attributes which can not currently be expressed in CSS.
FIXME: Deal with them with attr(foo dimension) and the like?
body
marginheight
marginwidth
topmargin
rightmargin
bottommargin
leftmargin
background
bgcolor
text
link
vlink
alink
frame, iframe
marginheight
marginwidth
font
face
color
size
table
cellspacing
cellpadding
hspace
vspace
height
width
bordercolor
border
col
width
tr
height
td, th
width
height
caption, thead, tbody, tfoot, tr, td, and th
align
table, thead, tbody, tfoot, tr, td, or th
background
bgcolor
(quirks mode) th, td
nowrap
hr
color
noshade
size
width
legend
align
embed, iframe, img, input[type=image i], object
hspace
vspace
img, input[type=image i], object
border
embed, iframe, img, input[type=image i], object, video
width
height
*/
/*
Extra
ol > li
https://html.spec.whatwg.org/multipage/#ordinal-value
col
span
colgroup (if not col child)
span
td, th
colspan
rowspan
:computed-value(text-align is initial) > th {
text-align: center;
}
https://html.spec.whatwg.org/multipage/#rendered-legend
*/

View file

@ -0,0 +1,44 @@
/*
https://html.spec.whatwg.org/multipage/#flow-content-3
> In quirks mode, the following rules are also expected to apply:
*/
@namespace url(http://www.w3.org/1999/xhtml);
form { margin-bottom: 1em; }
table {
font-weight: initial;
font-style: initial;
font-variant: initial;
font-size: initial;
line-height: initial;
white-space: initial;
/* text-align: initial; -- see FIXME below */
}
/*
* FIXME(pcwalton): Actually saying `text-align: initial` above breaks `<table>` inside `<center>`
* in quirks mode. This is because we (following Gecko, WebKit, and Blink) implement the HTML5
* align-descendants rules with a special `text-align: -moz-center`. `text-align: initial`, if
* placed on the `<table>` element per the spec, would break this behavior. So we place it on
* `<tbody>` instead.
*/
tbody {
text-align: initial;
}
/* FIXME: https://html.spec.whatwg.org/multipage/#margin-collapsing-quirks */
input:not([type=image]), textarea { box-sizing: border-box; }
img[align=left i] { margin-right: 3px; }
img[align=right i] { margin-left: 3px; }

View file

@ -0,0 +1,259 @@
button {
cursor: default;
}
button,
input {
background: white;
border: solid lightgrey 1px;
color: black;
font-family: sans-serif;
font-size: 0.8333em;
}
textarea {
background: white;
border: solid lightgrey 1px;
color: black;
font-family: sans-serif;
font-size: 0.8333em;
}
input::selection,
textarea::selection {
background: rgba(176, 214, 255, 1.0);
color: black;
}
button,
input[type="button"],
input[type="submit"],
input[type="reset"] {
background: lightgrey;
border-top: solid 1px #EEEEEE;
border-left: solid 1px #CCCCCC;
border-right: solid 1px #999999;
border-bottom: solid 1px #999999;
color: black;
}
input[type="hidden"] { display: none !important }
input[type="checkbox"],
input[type="radio"] {
font-family: monospace !important;
border: none !important;
background: transparent;
}
input[type="checkbox"]::before {
display: inline-block;
border: solid currentcolor 1px;
content: "";
padding: 0;
width: 1em;
height: 1em;
text-align: center;
}
input[type="checkbox"]:checked::before { content: "✓"; }
input[type="checkbox"]:indeterminate::before { content: "-"; }
input[type="radio"]::before {
display: inline-block;
border: solid currentcolor 1px;
content: "";
padding: 0;
width: 1em;
height: 1em;
border-radius: 50%;
text-align: center;
}
input[type="radio"]:checked::before { content: "●"; line-height: 1em; }
input[type="file"]::before {
content: "Choose File";
background: lightgrey;
border-top: solid 1px #EEEEEE;
border-left: solid 1px #CCCCCC;
border-right: solid 1px #999999;
border-bottom: solid 1px #999999;
}
input[type="file"] {
text-align: center;
color: black;
border-style: none;
}
td[align="left"] { text-align: left; }
td[align="center"] { text-align: center; }
td[align="right"] { text-align: right; }
center { text-align: -moz-center; }
label { cursor: default; }
img {
overflow: clip !important;
overflow-clip-margin: 0 !important;
}
input:not([type=radio i]):not([type=checkbox i]):not([type=reset i]):not([type=button i]):not([type=submit i]) {
cursor: text;
overflow: hidden !important;
white-space: pre;
}
textarea {
cursor: text;
overflow: auto;
}
/* https://html.spec.whatwg.org/multipage/rendering.html#the-details-and-summary-elements */
details {
display: block;
}
details::-servo-details-summary {
margin-left: 40px;
display: list-item;
list-style: disclosure-closed;
}
details[open]::-servo-details-summary {
list-style: disclosure-open;
}
*|*::-servo-details-content {
margin-left: 40px;
overflow: hidden;
display: block;
}
/*
* Until servo supports svg properly, make sure to at least prevent svg
* children from being layed out and rendered like usual html.
* https://github.com/servo/servo/issues/10646
*/
svg > * {
display: none;
}
*|*::-servo-anonymous-box {
unicode-bidi: inherit;
direction: inherit;
writing-mode: inherit;
}
*|*::-servo-anonymous-table {
display: table;
}
*|*::-servo-anonymous-table-row {
display: table-row;
}
*|*::-servo-anonymous-table-cell {
display: table-cell;
}
*|*::-servo-table-grid {
all: inherit;
margin: unset;
float: unset;
clear: unset;
position: unset;
z-index: unset;
page-break-before: unset;
page-break-after: unset;
page-break-inside: unset;
vertical-align: unset;
line-height: unset;
transform: unset;
transform-origin: unset;
backface-visibility: unset;
clip: unset;
transform-style: unset;
rotate: unset;
scale: unset;
translate: unset;
align-self: unset;
justify-self: unset;
grid-column-start: unset;
grid-column-end: unset;
grid-row-start: unset;
grid-row-end: unset;
order: unset;
outline: unset;
outline-offset: unset;
column-span: unset;
contain: unset;
container: unset;
scroll-margin: unset;
/* The grid needs to be block-level, so avoid inheriting `display: inline-table`. */
display: table;
}
meter {
display: inline-block;
width: 100px;
height: 12px;
border-radius: 6px;
background: linear-gradient(#e6e6e6, #e6e6e6, #eeeeee 20%, #cccccc 45%, #cccccc 55%);
overflow: clip;
}
/* FIXME: These should use the ::-moz-meter-bar pseudo element */
meter div {
height: 100%;
}
meter:-moz-meter-optimum div {
background: linear-gradient(#ad7, #ad7, #cea 20%, #7a3 45%, #7a3 55%);
}
meter:-moz-meter-sub-optimum div {
background: linear-gradient(#fe7, #fe7, #ffc 20%, #db3 45%, #db3 55%);
}
meter:-moz-meter-sub-sub-optimum div {
background: linear-gradient(#f77, #f77, #fcc 20%, #d44 45%, #d44 55%);
}
/* https://html.spec.whatwg.org/#the-details-and-summary-elements */
details, summary {
display: block;
}
details > summary:first-of-type {
display: list-item;
counter-increment: list-item 0;
list-style: disclosure-closed inside;
}
details[open] > summary:first-of-type {
list-style-type: disclosure-open;
}
/* Styles for the <progress> element */
progress {
display: inline-block;
width: 200px;
height: 6px;
border-radius: 3px;
border: 1px solid rgba(0, 0, 0, 0.5);
}
/* FIXME: This should use ::-moz-progress-bar */
progress #-servo-progress-bar {
display: block;
height: 100%;
background-color: #7a3;
}
select {
background-color: lightgrey;
border-radius: 5px;
border: 1px solid gray;
padding: 0 0.25em;
/* Don't show a text cursor when hovering selected option */
cursor: default;
}

View file

@ -0,0 +1,433 @@
/*
https://html.spec.whatwg.org/multipage/#form-controls
*/
@namespace url(http://www.w3.org/1999/xhtml);
[hidden], area, base, basefont, datalist, head, link, menu[type=popup i], meta,
noembed, noframes, param, rp, script, source, style, template, track, title {
display: none;
}
embed[hidden] { display: inline; height: 0; width: 0; }
/* FIXME: only if scripting is enabled */
noscript { display: none !important; }
input[type=hidden i] { display: none !important; }
html, body { display: block; }
body { margin: 8px; }
address, blockquote, center, div, figure, figcaption, footer, form, header, hr,
legend, listing, main, p, plaintext, pre, summary, xmp {
display: block;
}
blockquote, figure, listing, p, plaintext, pre, xmp {
margin-top: 1em; margin-bottom: 1em;
}
blockquote, figure { margin-left: 40px; margin-right: 40px; }
address { font-style: italic; }
listing, plaintext, pre, xmp {
font-family: monospace; white-space: pre;
}
dialog:not([open]) { display: none; }
dialog {
position: absolute;
left: 0; right: 0;
/* FIXME: support fit-content */
width: fit-content;
height: fit-content;
margin: auto;
border: solid;
padding: 1em;
background: white;
color: black;
}
/* FIXME: support ::backdrop */
dialog::backdrop {
position: fixed;
top: 0; right: 0; bottom: 0; left: 0;
background: rgba(0,0,0,0.1);
}
/* for small devices, modal dialogs go full-screen */
@media screen and (max-width: 540px) {
/* FIXME: support :modal */
dialog:modal {
top: 0;
width: auto;
margin: 1em;
}
}
cite, dfn, em, i, var { font-style: italic; }
b, strong { font-weight: bolder; }
code, kbd, samp, tt { font-family: monospace; }
big { font-size: larger; }
small { font-size: smaller; }
sub { vertical-align: sub; }
sup { vertical-align: super; }
sub, sup { line-height: normal; font-size: smaller; }
ruby { display: ruby; }
rt { display: ruby-text; }
/*
* All tag names that can be links are listed here, because applying pseudo-class selectors
* disables style sharing, so we want to apply pseudo-class selectors to as few elements as
* possible.
*/
a:link, area:link, link:link { color: #0000EE; }
a:visited, area:visited, link:visited { color: #551A8B; }
a:link, a:visited,
area:link, area:visited,
link:link, link:visited { text-decoration: underline; cursor: pointer; }
a:link[rel~=help], a:visited[rel~=help],
area:link[rel~=help], area:visited[rel~=help],
link:link[rel~=help], link:visited[rel~=help] { cursor: help; }
/*
* FIXME: use `outline: auto;`
*/
a:focus, area:focus {
outline: thin dotted;
}
input:focus, textarea:focus, button:focus {
outline: thin solid black;
}
mark { background: yellow; color: black; }
abbr[title], acronym[title] { text-decoration: dotted underline; }
ins, u { text-decoration: underline; }
del, s, strike { text-decoration: line-through; }
blink { text-decoration: blink; }
q::before { content: open-quote; }
q::after { content: close-quote; }
/*br { display-outside: newline; } /* this also has bidi implications */
br::before { content: "\A"; white-space: pre }
nobr { white-space: nowrap; }
wbr { display-outside: break-opportunity; } /* this also has bidi implications */
nobr wbr { white-space: normal; }
/* Eventually we will want the following, but currently Servo does not
properly parse the :dir pseudo-selector.
[dir=ltr i], bdi:dir(ltr), input[type=tel]:dir(ltr) { direction: ltr; }
*/
[dir=ltr i] { direction: ltr; }
[dir=rtl i] { direction: rtl; }
[dir=ltr i], [dir=rtl i], [dir=auto i] { unicode-bidi: isolate; }
/* To ensure http://www.w3.org/TR/REC-html40/struct/dirlang.html#style-bidi:
*
* "When a block element that does not have a dir attribute is transformed to
* the style of an inline element by a style sheet, the resulting presentation
* should be equivalent, in terms of bidirectional formatting, to the
* formatting obtained by explicitly adding a dir attribute (assigned the
* inherited value) to the transformed element."
*
* and the rules in http://dev.w3.org/html5/spec/rendering.html#rendering
*/
address,
article,
aside,
blockquote,
body,
caption,
center,
col,
colgroup,
dd,
dir,
div,
dl,
dt,
fieldset,
figcaption,
figure,
footer,
form,
h1,
h2,
h3,
h4,
h5,
h6,
header,
hgroup,
hr,
html,
legend,
li,
listing,
main,
marquee,
menu,
nav,
noframes,
ol,
p,
plaintext,
pre,
search,
section,
summary,
table,
tbody,
td,
tfoot,
th,
thead,
tr,
ul,
xmp
{
unicode-bidi: isolate;
}
bdi, output {
unicode-bidi: isolate;
}
bdo, bdo[dir] { unicode-bidi: isolate-override; }
textarea[dir=auto i], pre[dir=auto i] { unicode-bidi: plaintext; }
article, aside, h1, h2, h3, h4, h5, h6, hgroup, nav, section {
display: block;
}
h1 { margin-top: 0.67em; margin-bottom: 0.67em; font-size: 2.00em; font-weight: bold; }
h2 { margin-top: 0.83em; margin-bottom: 0.83em; font-size: 1.50em; font-weight: bold; }
h3 { margin-top: 1.00em; margin-bottom: 1.00em; font-size: 1.17em; font-weight: bold; }
h4 { margin-top: 1.33em; margin-bottom: 1.33em; font-size: 1.00em; font-weight: bold; }
h5 { margin-top: 1.67em; margin-bottom: 1.67em; font-size: 0.83em; font-weight: bold; }
h6 { margin-top: 2.33em; margin-bottom: 2.33em; font-size: 0.67em; font-weight: bold; }
:matches(article, aside, nav, section) h1 { margin-top: 0.83em; margin-bottom: 0.83em; font-size: 1.50em; }
:matches(article, aside, nav, section) :matches(article, aside, nav, section) h1 { margin-top: 1.00em; margin-bottom: 1.00em; font-size: 1.17em; }
:matches(article, aside, nav, section) :matches(article, aside, nav, section) :matches(article, aside, nav, section) h1 { margin-top: 1.33em; margin-bottom: 1.33em; font-size: 1.00em; }
:matches(article, aside, nav, section) :matches(article, aside, nav, section) :matches(article, aside, nav, section) :matches(article, aside, nav, section) h1 { margin-top: 1.67em; margin-bottom: 1.67em; font-size: 0.83em; }
:matches(article, aside, nav, section) :matches(article, aside, nav, section) :matches(article, aside, nav, section) :matches(article, aside, nav, section) :matches(article, aside, nav, section) h1 { margin-top: 2.33em; margin-bottom: 2.33em; font-size: 0.67em; }
:matches(article, aside, nav, section) hgroup > h1 ~ h2 { margin-top: 1.00em; margin-bottom: 1.00em; font-size: 1.17em; }
:matches(article, aside, nav, section) :matches(article, aside, nav, section) hgroup > h1 ~ h2 { margin-top: 1.33em; margin-bottom: 1.33em; font-size: 1.00em; }
:matches(article, aside, nav, section) :matches(article, aside, nav, section) :matches(article, aside, nav, section) hgroup > h1 ~ h2 { margin-top: 1.67em; margin-bottom: 1.67em; font-size: 0.83em; }
:matches(article, aside, nav, section) :matches(article, aside, nav, section) :matches(article, aside, nav, section) :matches(article, aside, nav, section) hgroup > h1 ~ h2 { margin-top: 2.33em; margin-bottom: 2.33em; font-size: 0.67em; }
:matches(article, aside, nav, section) hgroup > h1 ~ h3 { margin-top: 1.33em; margin-bottom: 1.33em; font-size: 1.00em; }
:matches(article, aside, nav, section) :matches(article, aside, nav, section) hgroup > h1 ~ h3 { margin-top: 1.67em; margin-bottom: 1.67em; font-size: 0.83em; }
:matches(article, aside, nav, section) :matches(article, aside, nav, section) :matches(article, aside, nav, section) hgroup > h1 ~ h3 { margin-top: 2.33em; margin-bottom: 2.33em; font-size: 0.67em; }
:matches(article, aside, nav, section) hgroup > h1 ~ h4 { margin-top: 1.67em; margin-bottom: 1.67em; font-size: 0.83em; }
:matches(article, aside, nav, section) :matches(article, aside, nav, section) hgroup > h1 ~ h4 { margin-top: 2.33em; margin-bottom: 2.33em; font-size: 0.67em; }
:matches(article, aside, nav, section) hgroup > h1 ~ h5 { margin-top: 2.33em; margin-bottom: 2.33em; font-size: 0.67em; }
dir, dd, dl, dt, menu, ol, ul { display: block; }
li { display: list-item; }
dir, dl, menu, ol, ul { margin-top: 1em; margin-bottom: 1em; }
:matches(dir, dl, menu, ol, ul) :matches(dir, dl, menu, ol, ul) {
margin-top: 0; margin-bottom: 0;
}
dd { margin-left: 40px; } /* FIXME: use margin-inline-start when supported */
dir, menu, ol, ul { padding-left: 40px; } /* FIXME: use padding-inline-start when supported */
ol { list-style-type: decimal; }
dir, menu, ul { list-style-type: disc; }
:matches(dir, menu, ol, ul) :matches(dir, menu, ul) {
list-style-type: circle;
}
:matches(dir, menu, ol, ul) :matches(dir, menu, ol, ul) :matches(dir, menu, ul) {
list-style-type: square;
}
table { display: table; }
caption {
display: table-caption;
text-align: center;
}
colgroup, colgroup[hidden] { display: table-column-group; }
col, col[hidden] { display: table-column; }
thead, thead[hidden] { display: table-header-group; }
tbody, tbody[hidden] { display: table-row-group; }
tfoot, tfoot[hidden] { display: table-footer-group; }
tr, tr[hidden] { display: table-row; }
td, th, td[hidden], th[hidden] { display: table-cell; }
colgroup[hidden], col[hidden], thead[hidden], tbody[hidden],
tfoot[hidden], tr[hidden], td[hidden], th[hidden] {
visibility: collapse;
}
table {
box-sizing: border-box;
border-spacing: 2px;
border-collapse: separate;
text-indent: initial;
}
td, th { padding: 1px; }
th {
font-weight: bold;
text-align: -moz-center-or-inherit;
}
thead, tbody, tfoot, table > tr { vertical-align: middle; }
tr, td, th { vertical-align: inherit; }
table, td, th { border-color: gray; }
thead, tbody, tfoot, tr { border-color: inherit; }
table:matches(
[rules=none i], [rules=groups i], [rules=rows i],
[rules=cols i], [rules=all i],
[frame=void i], [frame=above i], [frame=below i],
[frame=hsides i], [frame=lhs i], [frame=rhs i],
[frame=vsides i], [frame=box i], [frame=border i]
),
table:matches(
[rules=none i], [rules=groups i], [rules=rows i],
[rules=cols i], [rules=all i]
) > tr > :matches(td, th),
table:matches(
[rules=none i], [rules=groups i], [rules=rows i],
[rules=cols i], [rules=all i]
) > :matches(thead, tbody, tfoot) > tr > :matches(td, th) {
border-color: black;
}
:matches(table, thead, tbody, tfoot, tr) > form {
display: none !important;
}
input, select, button, textarea {
letter-spacing: initial;
word-spacing: initial;
line-height: initial;
text-transform: initial;
text-indent: initial;
text-shadow: initial;
appearance: auto;
}
input:not([type=image i], [type=range i], [type=checkbox i], [type=radio i]) {
overflow: clip !important;
overflow-clip-margin: 0 !important;
}
input, select, textarea {
text-align: initial;
}
:autofill {
field-sizing: fixed !important;
}
input:is([type=reset i], [type=button i], [type=submit i]), button {
text-align: center;
}
input, textarea, select, button { display: inline-block; }
input[type=hidden i], input[type=file i], input[type=image i] {
appearance: none;
}
input[type=radio i], input[type=checkbox i], input[type=reset i], input[type=button i], input[type=submit i],
input[type=color i], input[type=search i], select, button {
box-sizing: border-box;
}
textarea { white-space: pre-wrap; }
hr {
color: gray;
border-style: inset;
border-width: 1px;
margin-block-start: 0.5em;
margin-inline-end: auto;
margin-block-end: 0.5em;
margin-inline-start: auto;
overflow: hidden;
}
fieldset {
display: block; /* https://www.w3.org/Bugs/Public/show_bug.cgi?id=27018 */
margin-left: 2px; margin-right: 2px;
border: groove 2px;
border-color: ThreeDFace; /* FIXME: system color */
padding: 0.35em 0.625em 0.75em;
min-width: min-content;
}
legend {
padding-left: 2px; padding-right: 2px;
}
iframe:not([seamless]) { border: 2px inset; }
iframe[seamless] { display: block; }
video { object-fit: contain; }
textarea { white-space: pre-wrap; }
*|*:not(:root):fullscreen {
position:fixed !important;
top:0 !important; right:0 !important; bottom:0 !important; left:0 !important;
margin:0 !important;
box-sizing:border-box !important;
min-width:0 !important;
max-width:none !important;
min-height:0 !important;
max-height:none !important;
width:100% !important;
height:100% !important;
transform:none !important;
/* intentionally not !important */
object-fit:contain;
/* The internal-only -servo-top-layer property is used
to implement https://fullscreen.spec.whatwg.org/#top-layer */
-servo-top-layer: top;
}
iframe:fullscreen {
border:none !important;
padding:0 !important;
}
/* https://drafts.csswg.org/css-lists-3/#ua-stylesheet */
*::marker {
text-align: end;
text-transform: none;
unicode-bidi: isolate;
font-variant-numeric: tabular-nums;
white-space: pre;
}

View file

@ -225,7 +225,7 @@ fn test_fetch_blob() {
#[test]
fn test_file() {
let path = Path::new("../../resources/servo.css")
let path = Path::new("../../resources/ahem.css")
.canonicalize()
.unwrap();
let url = ServoUrl::from_file_path(path.clone()).unwrap();

View file

@ -12,7 +12,6 @@ use std::{f64, mem};
use compositing_traits::{CrossProcessCompositorApi, ImageUpdate, SerializableImageData};
use content_security_policy as csp;
use dom_struct::dom_struct;
use embedder_traits::resources::{self, Resource as EmbedderResource};
use embedder_traits::{MediaPositionState, MediaSessionEvent, MediaSessionPlaybackState};
use euclid::default::Size2D;
use headers::{ContentLength, ContentRange, HeaderMapExt};
@ -110,6 +109,12 @@ use crate::realms::{InRealm, enter_realm};
use crate::script_runtime::CanGc;
use crate::script_thread::ScriptThread;
/// A CSS file to style the media controls.
static MEDIA_CONTROL_CSS: &str = include_str!("../resources/media-controls.css");
/// A JS file to control the media controls.
static MEDIA_CONTROL_JS: &str = include_str!("../resources/media-controls.js");
#[derive(PartialEq)]
enum FrameStatus {
Locked,
@ -1949,14 +1954,13 @@ impl HTMLMediaElement {
ElementCreator::ScriptCreated,
can_gc,
);
let mut media_controls_script = resources::read_string(EmbedderResource::MediaControlsJS);
// This is our hacky way to temporarily workaround the lack of a privileged
// JS context.
// The media controls UI accesses the document.servoGetMediaControls(id) API
// to get an instance to the media controls ShadowRoot.
// `id` needs to match the internally generated UUID assigned to a media element.
let id = document.register_media_controls(&shadow_root);
let media_controls_script = media_controls_script.as_mut_str().replace("@@@id@@@", &id);
let media_controls_script = MEDIA_CONTROL_JS.replace("@@@id@@@", &id);
*self.media_controls_id.borrow_mut() = Some(id);
script
.upcast::<Node>()
@ -1969,7 +1973,6 @@ impl HTMLMediaElement {
return;
}
let media_controls_style = resources::read_string(EmbedderResource::MediaControlsCSS);
let style = HTMLStyleElement::new(
local_name!("script"),
None,
@ -1980,7 +1983,7 @@ impl HTMLMediaElement {
);
style
.upcast::<Node>()
.SetTextContent(Some(DOMString::from(media_controls_style)), can_gc);
.SetTextContent(Some(DOMString::from(MEDIA_CONTROL_CSS)), can_gc);
if let Err(e) = shadow_root
.upcast::<Node>()

View file

@ -0,0 +1,61 @@
button {
display: inline-block;
width: 24px;
height: 24px;
min-width: var(--button-size);
min-height: var(--button-size);
padding: 6px;
border: 0;
margin: 0;
background-color: transparent;
background-repeat: no-repeat;
background-position: center;
}
.root {
display: block;
position: relative;
min-height: 40px;
min-width: 230px;
}
.controls {
position: absolute;
bottom: 0;
width: 100%;
height: 40px;
background-color: rgba(26,26,26,.8);
color: #ffffff;
}
.hidden {
display: none;
}
.playing {
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAALElEQVR4AWMYQeD///8KQOwAxQq4xQkb1PAfARpwiQ9Kg0YNGjWI8iwycgAA8BVdyMCA4SwAAAAASUVORK5CYII=") no-repeat;
}
.paused {
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAATklEQVR4AWMYgeD///8C1DLoARA7UG4QAqynyHX/UcF7II6n3CAE2A/ECpQbhHBdPTUM+gDEDZQadIB8ryFckUBpYG8ARf/AJ0i4K0YBAOu3zFnxHnpDAAAAAElFTkSuQmCC") no-repeat;
}
.ended {
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAQAAAD8x0bcAAAArElEQVR4AbWRIQ7CQBBFR7QBhcYRFKISVwQcgfsgmiDq8S22BgkGAxwBJBp0BSEkhOaxYs2wW6jhjZn8eeqPfEJbfkHItoECDRRgQq9eSrBQsWLgl1rsAJgyo+TG6IsmBrocudKp1ew25MVcNG5PHDjr05i+oy946qCkcKSCUgdrLgQqCUyy0VIMJE5rsWjIqUgJbfcpFUvfUzLgzsnMA8gIxQcROXszOZH8hTfry4rnukG7bgAAAABJRU5ErkJggg==") no-repeat;
}
.volumeup {
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAQAAAD8x0bcAAAAi0lEQVR4AWOgNfh/4n/Yfybc0g1gcs////8P/RfCoeT/fyjL9v+H/1f+c2NTAgRwnun/v/+nYCoBAzB7yX9GILni/8//IjBpFAAVyQKSLkA6BJ+i3UBSDEiX4lO0C6qoDJ+iTCDpCqRDCTl8JdDhooSDYDL+wLT7/xEYmDz4omU3ZrRgKj0NjmCqAwANy7W0uf6FTQAAAABJRU5ErkJggg==") no-repeat;
}
.muted {
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAQAAAD8x0bcAAAAy0lEQVR4AayRtUICUBRA7a7Jye5RF7u+wM4vsF2s0e7O37BzkZqZ+AF2WGk4XLqZeO/dPq/TUtuoZSki7mIgFloFlv3+icgOoKE8GlsH1sVOgOgy9jGjpzIuRjqv/rgNI3/hQCuD0t+8WK7Eywx6NfSGIAX+5t90CwOF5ODgNj4EO2TzxbDkdXwlgmCdLOolr+UlEbTn37QAK0fRBz8BAImvEYwMnmiJfdRzHxTAYoAQJjqPWWxJMf+3vHEAzCf+8A4avXYR9xSpm5QAaOvUDhZuhCsAAAAASUVORK5CYII=") no-repeat;
}
.fullscreen {
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABGdBTUEAALGPC/xhBQAAAa1JREFUSA3Vlj1Ow0AQhW1OAFUaKmoqOEAklCIlJQ3nyBFyBahc0IAEEnUUunQWVLiloqEKNzDfmJ2V7Z1JlpKRRrt+82bezq7/yrZtH4qiOMUt2wJelWX5aQXJnYDf4UdWHOyjgFTjlm0Bz53ECAsHF65lzUFkDiffXM5Y+esQTq8CZ0ZEchLzBF5gviVsBwgiz2aYvrwtuiFWmkkjEN7S2h+wpn8Gso/ViLhXBP64+COYnkkUiAdqJLgiHhdcD74TWAnQ79pL/AsniGxki+ReTswQmSuJ2BTvm9klhGPNMUcIur+LMYHYIiiYxcd895oiUy9IbI5n3WlejX+OS/uyDV4bxNzt83IiHorfMortOuRlTLImJCe3EpisXIsz7Syulis53L6ZIhAm8hxs8PigMbeKDwpkcuRpXolAg3evipxE3YVdXGL6qqhVAKwTeZJJzwYr1+I6wrO6rcD1ZTcQ6NXtpjuL7xHRWq5ApQVyRqpJJ+PuRaT2vmiXBOPBZ4icwbmweJ7AIeR1jkjgrOFLTmLSmvy2nCSRX0B+W6755n5ZcXLlGbrHvd+W9x/rQiZUwmH0RgAAAABJRU5ErkJggg==') no-repeat;
}
.fullscreen.fullscreen-active {
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABGdBTUEAALGPC/xhBQAAAbtJREFUSA3tlj0vBFEUhnd8hFhrE4mPbSREovIfdFQ6hcJ/EJVColP6GRoqhZbEFqKmET2xDSpWYjxncu915u6ZzWS20DjJ6577no/3zL1mqNWwNE1HwKb4g5rrdcjayHo54oxVbG8QAepl0FNphLVBQ6bflZ2ySiLU6+a+3bY/Hq/qA0EEog4m4ieDmwOzwrNazUOPwgQKpfkVWDAEVuHvQAsUDhjqSLKmeIQXKxKQ2Jv8UPY7eejuHJIsEantJ6B6l/glIXsKvOgq/DICT+Rld6IHH9IbEursz8GM5kv68+RdxiKJLibYYr+sOeffJknyqXlyZbhJzTn/g9yuwf9Tf3QC8SUPM8eoMUuXi/vWPJcsuSuac36H3I7BZ98UeQduQGwbcQEJ03ES+3vQ8y7kakkQkQegrYzAKwXyLuQs96K5yDrrYi6r3KZJ2jEicnS2EdwCX8Dbu3P6PcGzT3brCWuvCGTc/BpOPsVyJ0UC2ZkT3wfa8iJErObZZ4BYEyzFzww3DsKF4tsiBNaAPhaZ3PrGxBo9e0PkQP6ajYELIFa5uVejh3+SNn74z0JEjkClyX1zv9Jnxzf/AaCJ561FockLAAAAAElFTkSuQmCC') no-repeat;
}

View file

@ -0,0 +1,416 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
(() => {
"use strict";
// States.
const BUFFERING = "buffering";
const ENDED = "ended";
const ERRORED = "errored";
const PAUSED = "paused";
const PLAYING = "playing";
// State transitions.
const TRANSITIONS = {
buffer: {
paused: BUFFERING
},
end: {
playing: ENDED,
paused: ENDED
},
error: {
buffering: ERRORED,
playing: ERRORED,
paused: ERRORED
},
pause: {
buffering: PAUSED,
playing: PAUSED
},
play: {
buffering: PLAYING,
ended: PLAYING,
paused: PLAYING
}
};
function generateMarkup(isAudioOnly) {
return `
<div class="controls">
<button id="play-pause-button"></button>
<input id="progress" type="range" value="0" min="0" max="100" step="1"></input>
<span id="position-duration-box" class="hidden">
<span id="position-text">#1</span>
<span id="duration"> / #2</span>
</span>
<button id="volume-switch"></button>
<input id="volume-level" type="range" value="100" min="0" max="100" step="1"></input>
${isAudioOnly ? "" : '<button id="fullscreen-switch" class="fullscreen"></button>'}
</div>
`;
}
function camelCase(str) {
const rdashes = /-(.)/g;
return str.replace(rdashes, (str, p1) => {
return p1.toUpperCase();
});
}
function formatTime(time, showHours = false) {
// Format the duration as "h:mm:ss" or "m:ss"
time = Math.round(time / 1000);
const hours = Math.floor(time / 3600);
const mins = Math.floor((time % 3600) / 60);
const secs = Math.floor(time % 60);
const formattedHours =
hours || showHours ? `${hours.toString().padStart(2, "0")}:` : "";
return `${formattedHours}${mins
.toString()
.padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
}
class MediaControls {
constructor() {
this.nonce = Date.now();
// Get the instance of the shadow root where these controls live.
this.controls = document.servoGetMediaControls("@@@id@@@");
// Get the instance of the host of these controls.
this.media = this.controls.host;
this.mutationObserver = new MutationObserver(() => {
// We can only get here if the `controls` attribute is removed.
this.cleanup();
});
this.mutationObserver.observe(this.media, {
attributeFilter: ["controls"]
});
this.isAudioOnly = this.media.localName == "audio";
// Create root element and load markup.
this.root = document.createElement("div");
this.root.classList.add("root");
this.root.innerHTML = generateMarkup(this.isAudioOnly);
this.controls.appendChild(this.root);
const elementNames = [
"duration",
"play-pause-button",
"position-duration-box",
"position-text",
"progress",
"volume-switch",
"volume-level"
];
if (!this.isAudioOnly) {
elementNames.push("fullscreen-switch");
}
// Import elements.
this.elements = {};
elementNames.forEach(id => {
this.elements[camelCase(id)] = this.controls.getElementById(id);
});
// Init position duration box.
const positionTextNode = this.elements.positionText;
const durationSpan = this.elements.duration;
const durationFormat = durationSpan.textContent;
const positionFormat = positionTextNode.textContent;
durationSpan.classList.add("duration");
durationSpan.setAttribute("role", "none");
Object.defineProperties(this.elements.positionDurationBox, {
durationSpan: {
value: durationSpan
},
position: {
get: () => {
return positionTextNode.textContent;
},
set: v => {
positionTextNode.textContent = positionFormat.replace("#1", v);
}
},
duration: {
get: () => {
return durationSpan.textContent;
},
set: v => {
durationSpan.textContent = v ? durationFormat.replace("#2", v) : "";
}
},
show: {
value: (currentTime, duration) => {
const self = this.elements.positionDurationBox;
if (self.position != currentTime) {
self.position = currentTime;
}
if (self.duration != duration) {
self.duration = duration;
}
self.classList.remove("hidden");
}
}
});
// Add event listeners.
this.mediaEvents = [
"play",
"pause",
"ended",
"volumechange",
"loadeddata",
"loadstart",
"timeupdate",
"progress",
"playing",
"waiting",
"canplay",
"canplaythrough",
"seeking",
"seeked",
"emptied",
"loadedmetadata",
"error",
"suspend"
];
this.mediaEvents.forEach(event => {
this.media.addEventListener(event, this);
});
this.controlEvents = [
{ el: this.elements.playPauseButton, type: "click" },
{ el: this.elements.volumeSwitch, type: "click" },
{ el: this.elements.volumeLevel, type: "input" }
];
if (!this.isAudioOnly) {
this.controlEvents.push({ el: this.elements.fullscreenSwitch, type: "click" });
}
this.controlEvents.forEach(({ el, type }) => {
el.addEventListener(type, this);
});
// Create state transitions.
//
// It exposes one method per transition. i.e. this.pause(), this.play(), etc.
// For each transition, we check that the transition is possible and call
// the `onStateChange` handler.
for (let name in TRANSITIONS) {
if (!TRANSITIONS.hasOwnProperty(name)) {
continue;
}
this[name] = () => {
const from = this.state;
// Checks if the transition is valid in the current state.
if (!TRANSITIONS[name][from]) {
const error = `Transition "${name}" invalid for the current state "${from}"`;
console.error(error);
throw new Error(error);
}
const to = TRANSITIONS[name][from];
if (from == to) {
return;
}
// Transition to the next state.
this.state = to;
this.onStateChange(from);
};
}
// Set initial state.
this.state = this.media.paused ? PAUSED : PLAYING;
this.onStateChange(null);
}
cleanup() {
this.mutationObserver.disconnect();
this.mediaEvents.forEach(event => {
this.media.removeEventListener(event, this);
});
this.controlEvents.forEach(({ el, type }) => {
el.removeEventListener(type, this);
});
}
// State change handler
onStateChange(from) {
this.render(from);
}
render(from = this.state) {
if (!this.isAudioOnly) {
// XXX This should ideally use clientHeight/clientWidth,
// but for some reason I couldn't figure out yet,
// using it breaks layout.
this.root.style.height = this.media.videoHeight;
this.root.style.width = this.media.videoWidth;
}
// Error
if (this.state == ERRORED) {
//XXX render errored state
return;
}
if (this.state != from) {
// Play/Pause button.
const playPauseButton = this.elements.playPauseButton;
playPauseButton.classList.remove(from);
playPauseButton.classList.add(this.state);
}
// Progress.
const positionPercent =
(this.media.currentTime / this.media.duration) * 100;
if (Number.isFinite(positionPercent)) {
this.elements.progress.value = positionPercent;
} else {
this.elements.progress.value = 0;
}
// Current time and duration.
let currentTime = formatTime(0);
let duration = formatTime(0);
if (!isNaN(this.media.currentTime) && !isNaN(this.media.duration)) {
currentTime = formatTime(Math.round(this.media.currentTime * 1000));
duration = formatTime(Math.round(this.media.duration * 1000));
}
this.elements.positionDurationBox.show(currentTime, duration);
// Volume.
this.elements.volumeSwitch.className =
this.media.muted || !this.media.volume ? "muted" : "volumeup";
const volumeLevelValue = this.media.muted
? 0
: Math.round(this.media.volume * 100);
if (this.elements.volumeLevel.value != volumeLevelValue) {
this.elements.volumeLevel.value = volumeLevelValue;
}
}
handleEvent(event) {
if (!event.isTrusted) {
console.warn(`Drop untrusted event ${event.type}`);
return;
}
if (this.mediaEvents.includes(event.type)) {
this.onMediaEvent(event);
} else {
this.onControlEvent(event);
}
}
onControlEvent(event) {
switch (event.type) {
case "click":
switch (event.currentTarget) {
case this.elements.playPauseButton:
this.playOrPause();
break;
case this.elements.volumeSwitch:
this.toggleMuted();
break;
case this.elements.fullscreenSwitch:
this.toggleFullscreen();
break;
}
break;
case "input":
switch (event.currentTarget) {
case this.elements.volumeLevel:
this.changeVolume();
break;
}
break;
default:
throw new Error(`Unknown event ${event.type}`);
}
}
// HTMLMediaElement event handler
onMediaEvent(event) {
switch (event.type) {
case "ended":
this.end();
break;
case "play":
case "pause":
// Transition to PLAYING or PAUSED state.
this[event.type]();
break;
case "volumechange":
case "timeupdate":
case "resize":
this.render();
break;
case "loadedmetadata":
break;
}
}
/* Media actions */
playOrPause() {
switch (this.state) {
case PLAYING:
this.media.pause();
break;
case BUFFERING:
case ENDED:
case PAUSED:
this.media.play();
break;
default:
throw new Error(`Invalid state ${this.state}`);
}
}
toggleMuted() {
this.media.muted = !this.media.muted;
}
toggleFullscreen() {
const { fullscreenEnabled, fullscreenElement } = document;
const isElementFullscreen = fullscreenElement && fullscreenElement === this.media;
if (fullscreenEnabled && isElementFullscreen) {
document.exitFullscreen().then(() => {
this.elements.fullscreenSwitch.classList.remove("fullscreen-active");
});
} else {
this.media.requestFullscreen().then(() => {
this.elements.fullscreenSwitch.classList.add("fullscreen-active");
});
}
}
changeVolume() {
const volume = parseInt(this.elements.volumeLevel.value);
if (!isNaN(volume)) {
this.media.volume = volume / 100;
}
}
}
new MediaControls();
})();

View file

@ -91,18 +91,6 @@ pub enum Resource {
/// The message can contain a placeholder `${reason}` for the error code.
/// It can be empty but then nothing will be displayed when an internal error occurs.
NetErrorHTML,
/// A CSS file to style the user agent stylesheet.
/// It can be empty but then there's simply no user agent stylesheet.
UserAgentCSS,
/// A CSS file to style the Servo browser.
/// It can be empty but several features might not work as expected.
ServoCSS,
/// A CSS file to style the presentational hints.
/// It can be empty but then presentational hints will not be styled.
PresentationalHintsCSS,
/// A CSS file to style the quirks mode.
/// It can be empty but then quirks mode will not be styled.
QuirksModeCSS,
/// A placeholder image to display if we couldn't get the requested image.
///
/// ## Panic
@ -110,12 +98,6 @@ pub enum Resource {
/// If the resource is not provided, servo will fallback to a baked in default (See resources/rippy.png).
/// However, if the image is provided but invalid, Servo will crash.
RippyPNG,
/// A CSS file to style the media controls.
/// It can be empty but then media controls will not be styled.
MediaControlsCSS,
/// A JS file to control the media controls.
/// It can be empty but then media controls will not work.
MediaControlsJS,
/// A placeholder HTML page to display when the code responsible for rendering a page panics and the original
/// page can no longer be displayed.
/// The message can contain a placeholder `${details}` for the error details.
@ -137,13 +119,7 @@ impl Resource {
Resource::HstsPreloadList => "hsts_preload.json",
Resource::BadCertHTML => "badcert.html",
Resource::NetErrorHTML => "neterror.html",
Resource::UserAgentCSS => "user-agent.css",
Resource::ServoCSS => "servo.css",
Resource::PresentationalHintsCSS => "presentational-hints.css",
Resource::QuirksModeCSS => "quirks-mode.css",
Resource::RippyPNG => "rippy.png",
Resource::MediaControlsCSS => "media-controls.css",
Resource::MediaControlsJS => "media-controls.js",
Resource::CrashHTML => "crash.html",
Resource::DirectoryListingHTML => "directory-listing.html",
Resource::AboutMemoryHTML => "about-memory.html",
@ -183,21 +159,7 @@ fn resources_for_tests() -> Box<dyn ResourceReaderMethods + Sync + Send> {
},
Resource::BadCertHTML => &include_bytes!("../../../resources/badcert.html")[..],
Resource::NetErrorHTML => &include_bytes!("../../../resources/neterror.html")[..],
Resource::UserAgentCSS => &include_bytes!("../../../resources/user-agent.css")[..],
Resource::ServoCSS => &include_bytes!("../../../resources/servo.css")[..],
Resource::PresentationalHintsCSS => {
&include_bytes!("../../../resources/presentational-hints.css")[..]
},
Resource::QuirksModeCSS => {
&include_bytes!("../../../resources/quirks-mode.css")[..]
},
Resource::RippyPNG => &include_bytes!("../../../resources/rippy.png")[..],
Resource::MediaControlsCSS => {
&include_bytes!("../../../resources/media-controls.css")[..]
},
Resource::MediaControlsJS => {
&include_bytes!("../../../resources/media-controls.js")[..]
},
Resource::CrashHTML => &include_bytes!("../../../resources/crash.html")[..],
Resource::DirectoryListingHTML => {
&include_bytes!("../../../resources/directory-listing.html")[..]