自定义网页鼠标指针——一段曲折的旅程
看到别人主题有自定义鼠标指针的功能,我也想给我主题加一个玩玩。
探索
从 User Agent Stylesheet 学习
找到了相关文档:
看起来似乎只需要下面 CSS 代码就完事了。
:root {
cursor: url("xxx.cur"), auto;
}
随便下载了一个图标包,傻眼了:一个包里面有若干个 CUR 文件。
好吧,CUR 格式是一种图形文件格式,而不是打包了一组图标。
显然下面的写法不是很符合正确 CSS 写法。
:root {
cursor: url("auto.cur"), auto;
cursor: url("context-menu.cur"), context-menu; /* [!code ++] */
/* 省略更多 */ /* [!code ++] */
}
那有没有什么标准,说明什么网页元素用什么光标?
我想到了找 User Agent Stylesheet(用户代理样式表)
- Chromium 的:Source/core/css/html.css - chromium/blink - Git at Google
- Firefox 的只需要用浏览器打开
resource://gre-resources/html.css即可看到。 - 未能找到 Safari 的 User Agent Stylesheet。
用户代理样式表中有这些 cursor 相关的定义:
点我展开相关定义
Chromium
label {
cursor: default; /* [!code highlight] */
}
input {
-webkit-appearance: textfield;
padding: 1px;
background-color: white;
border: 2px inset;
-webkit-rtl-ordering: logical;
-webkit-user-select: text;
cursor: auto; /* [!code highlight] */
}
input::-webkit-inner-spin-button {
-webkit-appearance: inner-spin-button;
display: inline-block;
cursor: default; /* [!code highlight] */
flex: none;
align-self: stretch;
-webkit-user-select: none;
-webkit-user-modify: read-only !important;
opacity: 0;
pointer-events: none;
}
textarea {
-webkit-appearance: textarea;
background-color: white;
border: 1px solid;
-webkit-rtl-ordering: logical;
-webkit-user-select: text;
flex-direction: column;
resize: auto;
cursor: auto; /* [!code highlight] */
padding: 2px;
white-space: pre-wrap;
word-wrap: break-word;
}
input[type="button" i],
input[type="submit" i],
input[type="reset" i],
input[type="file" i]::-webkit-file-upload-button,
button {
align-items: flex-start;
text-align: center;
cursor: default; /* [!code highlight] */
color: ButtonText;
padding: 2px 6px 3px 6px;
border: 2px outset ButtonFace;
background-color: ButtonFace;
box-sizing: border-box
}
area {
display: inline;
cursor: pointer; /* [!code highlight] */
}
select {
-webkit-appearance: menulist;
box-sizing: border-box;
align-items: center;
border: 1px solid;
white-space: pre;
-webkit-rtl-ordering: logical;
color: black;
background-color: white;
cursor: default; /* [!code highlight] */
}
a:-webkit-any-link {
color: -webkit-link;
text-decoration: underline;
cursor: auto; /* [!code highlight] */
}
Firefox
.mozGrabber:-moz-native-anonymous {
outline: ridge 2px silver;
padding: 2px;
position: absolute;
width: 12px;
height: 12px;
background-image: url("resource://gre/res/grabber.gif");
background-repeat: no-repeat;
background-position: center center;
user-select: none;
cursor: move; /* [!code highlight] */
}
提取聚合相关声明
label {
cursor: default;
}
input {
cursor: auto;
}
input::-webkit-inner-spin-button {
cursor: default;
}
textarea {
cursor: auto;
}
input[type="button" i],
input[type="submit" i],
input[type="reset" i],
input[type="file" i]::-webkit-file-upload-button,
button {
cursor: default;
}
area {
cursor: pointer;
}
select {
cursor: default;
}
a:-webkit-any-link {
cursor: auto;
}
.mozGrabber:-moz-native-anonymous {
cursor: move;
}
我们可以将这些拿出来作为声明。
从 cursor 实现学习
忽然想到,我找下 cursor: auto 是怎么实现的,对照设置下就好了,于是找到以下源码:
- event_handler.cc - Chromium Code Search
- chromium/third_party/blink/renderer/core/input/event_handler.cc at main · chromium/chromium
bool EventHandler::ShouldShowIBeamForNode(const Node* node,
const HitTestResult& result) {
if (!node)
return false;
if (node->IsTextNode() && (node->CanStartSelection() || result.IsOverLink()))
return true;
return IsEditable(*node);
}
std::optional<ui::Cursor> EventHandler::SelectCursor(
const ui::Cursor& i_beam = style.IsHorizontalWritingMode() ? IBeamCursor() : VerticalTextCursor();
switch (style.Cursor()) {
case ECursor::kAuto:
return SelectAutoCursor(result, node, i_beam);
// 省略
case ECursor::kText:
return i_beam;
// 省略
}
return PointerCursor();
}
std::optional<ui::Cursor> EventHandler::SelectAutoCursor(
const HitTestResult& result,
Node* node,
const ui::Cursor& i_beam) {
if (ShouldShowIBeamForNode(node, result))
return i_beam;
return PointerCursor();
}
cursor: auto 的实现逻辑非常简单:
-
调用
ShouldShowIBeamForNode()判断当前节点是否,满足:可选择的文本/链接文本/可编辑区域(如<input>,<textarea>或带contenteditable属性的元素)- 如果是,返回
i_beam(即cursor: text)
- 如果是,返回
-
默认情况返回普通箭头光标(即
cursor: default)
结论:把可编辑的文本设置为 cursor: text。
发挥主观能动性
发挥主观能动性,观察标准 HTML 元素/ARIA 属性,按语义进行标注,得到了以下 CSS 声明:
一些 CSS 声明
:root {
cursor: default;
}
/* 禁止操作 */
:disabled,
[aria-disabled="true"] {
cursor: not-allowed;
}
/* 帮助提示 */
[title],
abbr,
acronym,
[role="tooltip"] {
cursor: help;
}
/* 调整大小 - 东西 */
input[type="range"],
[role="slider"] {
cursor: ew-resize;
}
/* 抓取 */
input[type="range"]::-webkit-slider-thumb {
cursor: grab;
}
/* 抓取中 */
[draggable="true"]:active,
input[type="range"]::-webkit-slider-thumb:active {
cursor: grabbing;
}
/* 单元格 */
td,
th,
[role="grid"],
[role="gridcell"] {
cursor: cell;
}
/* 等待 */
[aria-busy="true"] {
cursor: wait;
}
/* 文本光标 */
input:not([type]),
input[type="text"],
input[type="password"],
input[type="email"],
input[type="search"],
input[type="tel"],
input[type="url"],
input[type="number"],
[contenteditable="true"],
textarea,
code,
kbd,
samp,
var,
pre,
[role="textbox"],
[role="searchbox"] {
cursor: text;
}
/* 指针光标 */
a,
button,
select,
label,
input[type="button"],
input[type="submit"],
input[type="reset"],
input[type="file"],
input[type="checkbox"],
input[type="radio"],
input[type="color"],
input[type="date"],
input[type="datetime-local"],
input[type="time"],
input[type="month"],
input[type="week"],
video,
audio,
video::-webkit-media-controls,
audio::-webkit-media-controls,
img[onclick],
img[role="button"],
::-webkit-scrollbar-thumb,
::-webkit-file-upload-button,
::-ms-browse,
::-webkit-search-cancel-button,
::-ms-clear,
::-webkit-clear-button,
::-webkit-calendar-picker-indicator,
option,
summary,
li[onclick],
li[role="button"],
tr[onclick],
svg [onclick],
[role="button"],
[role="link"],
[role="menuitem"],
[role="menuitemcheckbox"],
[role="menuitemradio"],
[role="tab"],
[role="treeitem"],
[role="option"],
[role="switch"],
[role="checkbox"],
[role="radio"],
[role="combobox"] {
cursor: pointer;
}
/* 准星 */
canvas {
cursor: crosshair;
}
/* 抓取(可拖拽元素) */
[draggable="true"] {
cursor: grab;
}
成果
聚合上面的阶段性成果,并按情况解释 cursor: auto 实际值,得到了以下的最终版本:
最终版本 CSS 声明
:root,
label,
input::-webkit-inner-spin-button,
input[type="button" i],
input[type="submit" i],
input[type="reset" i],
input[type="file" i]::-webkit-file-upload-button,
button,
select {
cursor: default;
}
.mozGrabber:-moz-native-anonymous {
cursor: move;
}
:disabled,
[aria-disabled="true"] {
cursor: not-allowed;
}
[title],
abbr,
acronym,
[role="tooltip"] {
cursor: help;
}
input[type="range"],
[role="slider"] {
cursor: ew-resize;
}
[draggable="true"],
input[type="range"]::-webkit-slider-thumb {
cursor: grab;
}
[draggable="true"]:active,
input[type="range"]::-webkit-slider-thumb:active {
cursor: grabbing;
}
td,
th,
[role="grid"],
[role="gridcell"] {
cursor: cell;
}
[aria-busy="true"] {
cursor: wait;
}
input:not([type]),
input[type="text"],
input[type="password"],
input[type="email"],
input[type="search"],
input[type="tel"],
input[type="url"],
input[type="number"],
[contenteditable="true"],
textarea,
code,
kbd,
samp,
var,
pre,
[role="textbox"],
[role="searchbox"] {
cursor: text;
}
area,
a,
a:-webkit-any-link,
button,
select,
label,
input[type="button"],
input[type="submit"],
input[type="reset"],
input[type="file"],
input[type="checkbox"],
input[type="radio"],
input[type="color"],
input[type="date"],
input[type="datetime-local"],
input[type="time"],
input[type="month"],
input[type="week"],
input[type="range"],
input[type="image"],
video,
audio,
video::-webkit-media-controls,
audio::-webkit-media-controls,
img[onclick],
img[role="button"],
::-webkit-scrollbar-thumb,
::-webkit-file-upload-button,
::-ms-browse,
::-webkit-search-cancel-button,
::-ms-clear,
::-webkit-clear-button,
::-webkit-calendar-picker-indicator,
option,
summary,
li[onclick],
li[role="button"],
tr[onclick],
svg [onclick],
[role="button"],
[role="link"],
[role="menuitem"],
[role="menuitemcheckbox"],
[role="menuitemradio"],
[role="tab"],
[role="treeitem"],
[role="option"],
[role="switch"],
[role="checkbox"],
[role="radio"],
[role="combobox"] {
cursor: pointer;
}
canvas {
cursor: crosshair;
}
使用方法:
:root {
cursor: default; /* [!code --] */
ursor: url("实际的图标地址.cur"), default; /* [!code ++] */
}
/* 此外再补充其他需要的声明 */ /* [!code ++] */
结语
感谢您看到这里,欢迎在评论区留言。
0