feat: 悬浮状态条大小不再跟候选字号,另设大小滑块

候选字号调大后状态条跟着放大,比候选窗口还显眼(用户群反馈)。
状态条改按主题原样画,大小由 [status_bar] size(60%–150%,缺省 100%)单独定,设置的中英文页加滑块。
百分比滑块挪到共用控件,键位提示与状态条共用。
This commit is contained in:
yenharvey
2026-10-20 17:34:14 +08:00
parent bb697083b8
commit d2e70ea75c
21 changed files with 104 additions and 55 deletions
+4 -29
View File
@@ -2,9 +2,7 @@
use bamboo_slip::taffy::LengthPercentage;
use bamboo_slip::taffy::{Dimension, FlexDirection, FlexWrap, NodeId, Rect, Size, Style};
use bamboo_slip::{
Checkbox, RenderError, SettingRow, Slider, Switch, Ui, Visual, group, section_title,
};
use bamboo_slip::{Checkbox, RenderError, SettingRow, Switch, Ui, Visual, group, section_title};
use qingjian_platform::{KeyHintMode, Scheme, code_schemes};
use super::shortcuts;
@@ -13,7 +11,7 @@ use crate::message::{Message, Popup};
use crate::shortcut::Shortcut;
use crate::support::Feature;
use crate::view::Page;
use crate::view::parts::controls::{select, toggle_info, toggle_with};
use crate::view::parts::controls::{percent_slider, select, toggle_info, toggle_with};
use crate::view::parts::lists::{entry_row, import_row};
use crate::view::parts::menus::{CODE_OFF, scheme_options};
@@ -150,7 +148,7 @@ fn key_hint(ui: &mut Ui<Message>, page: &Page) -> Result<[NodeId; 3], RenderErro
let size = percent_slider(
ui,
page,
("键位提示大小", "key_hint_size"),
("键位提示大小", ("general", "key_hint_size")),
general.key_hint_size(),
(60.0, 150.0),
off,
@@ -158,7 +156,7 @@ fn key_hint(ui: &mut Ui<Message>, page: &Page) -> Result<[NodeId; 3], RenderErro
let opacity = percent_slider(
ui,
page,
("键位提示透明度", "key_hint_opacity"),
("键位提示透明度", ("general", "key_hint_opacity")),
general.key_hint_opacity(),
(30.0, 100.0),
off,
@@ -166,29 +164,6 @@ fn key_hint(ui: &mut Ui<Message>, page: &Page) -> Result<[NodeId; 3], RenderErro
Ok([mode, size, opacity])
}
/// 一行百分比滑块,步长 5%:`ratio` 是当前倍数(1 为 100%),拖到的值按整数百分比写进 `[general]`。
fn percent_slider(
ui: &mut Ui<Message>,
page: &Page,
(title, key): (&str, &'static str),
ratio: f32,
(min, max): (f32, f32),
disabled: bool,
) -> Result<NodeId, RenderError> {
let percent = (ratio * 100.0).round();
let text = format!("{percent:.0}%");
let slider = Slider::new(percent, min, max)
.step(5.0)
.label(&text)
.disabled(disabled)
.build(ui, &page.theme, key, move |value| {
Message::set("general", key, value.round() as i64)
})?;
SettingRow::new(title)
.control(slider)
.build(ui, &page.theme)
}
/// 辅码:开关、显示、触发键,再一组码表(随包的、导入的、导入按钮)。
fn aux_code(ui: &mut Ui<Message>, page: &Page) -> Result<Vec<NodeId>, RenderError> {
let theme = &page.theme;
+10 -2
View File
@@ -8,7 +8,7 @@ use crate::message::Message;
use crate::support::Feature;
use crate::view::Page;
use crate::view::parts::actions::with_notice;
use crate::view::parts::controls::{choice, toggle, toggle_info};
use crate::view::parts::controls::{choice, percent_slider, toggle, toggle_info};
/// 全角 / 半角两档,配置里是布尔。
const WIDTHS: [bool; 2] = [true, false];
@@ -162,8 +162,16 @@ pub fn language(ui: &mut Ui<Message>, page: &Page) -> Result<Vec<NodeId>, Render
("status_bar", "enabled"),
config.status_bar.enabled,
)?;
let size = percent_slider(
ui,
page,
("状态条大小", ("status_bar", "size")),
config.status_bar.size(),
(60.0, 150.0),
!config.status_bar.enabled,
)?;
out.push(section_title(ui, theme, "状态条")?);
out.push(group(ui, theme, &[status])?);
out.push(group(ui, theme, &[status, size])?);
}
Ok(out)
}
+25 -2
View File
@@ -1,7 +1,7 @@
//! 各页共用的几种设置行:开关、分段选择、下拉框。
//! 各页共用的几种设置行:开关、分段选择、下拉框、百分比滑块。
use bamboo_slip::taffy::NodeId;
use bamboo_slip::{RenderError, Segmented, Select, SettingRow, Switch, Ui};
use bamboo_slip::{RenderError, Segmented, Select, SettingRow, Slider, Switch, Ui};
use super::menus::anchor;
use crate::message::{Message, Popup};
@@ -98,3 +98,26 @@ pub fn select(
.disabled(disabled)
.build(ui, &page.theme, anchor(popup), Message::OpenMenu(popup))
}
/// 一行百分比滑块,步长 5%:`ratio` 是当前倍数(1 为 100%),拖到的值按整数百分比写进 `[section] key`。
pub fn percent_slider(
ui: &mut Ui<Message>,
page: &Page,
(title, (section, key)): (&str, (&'static str, &'static str)),
ratio: f32,
(min, max): (f32, f32),
disabled: bool,
) -> Result<NodeId, RenderError> {
let percent = (ratio * 100.0).round();
let text = format!("{percent:.0}%");
let slider = Slider::new(percent, min, max)
.step(5.0)
.label(&text)
.disabled(disabled)
.build(ui, &page.theme, key, move |value| {
Message::set(section, key, value.round() as i64)
})?;
SettingRow::new(title)
.control(slider)
.build(ui, &page.theme)
}
@@ -118,6 +118,9 @@ pub struct RouterConfig {
/// 状态条记住的位置(`[status_bar] x` / `y`,内容左上角物理像素)。
pub status_pos: Option<(i32, i32)>,
/// 状态条大小(`[status_bar] size`,百分比,已收到 60–150)。
pub status_size: u8,
/// 拼音侧方案(`[general] scheme`)。
pub scheme: Scheme,
@@ -208,6 +211,7 @@ impl From<&Config> for RouterConfig {
predict: config.shortcut.predict,
status_enabled: config.status_bar.enabled,
status_pos: config.status_bar.x.zip(config.status_bar.y),
status_size: config.status_bar.size.clamp(60, 150),
scheme: config.general.scheme(),
traditional: config.general.traditional() != TraditionalVariant::Off,
aux_code_key: config.general.aux_code_key(),
@@ -130,6 +130,7 @@ impl Router {
full_width: self.full_width_for(english),
appearance: self.config.appearance,
anchor: self.config.status_pos,
size: self.config.status_size,
mode: self.input_mode(),
});
}
@@ -22,6 +22,9 @@ pub struct StatusView {
/// 配置里记住的内容左上角物理像素;`None` 首次按屏幕右下角摆。
pub anchor: Option<(i32, i32)>,
/// 大小(百分比,100 为原大),与候选字号无关。
pub size: u8,
/// 输入状态,给节点树画法的主题显示(与候选窗口同一份)。
pub mode: Mode,
}
+4 -2
View File
@@ -139,16 +139,18 @@ impl Painter {
self.renderer.forget();
}
/// 画状态条。
/// 画状态条;`size` 是设置里的大小(百分比)。
pub(super) fn render_status(
&mut self,
cells: &[StatusCell],
mode: &Mode,
dark: bool,
dpi: u32,
size: u8,
) -> Option<RenderedStatus> {
let scale = scale(dpi) * f32::from(size) / 100.0;
self.renderer
.render_status(cells, mode, &self.theme.with_dark(dark), scale(dpi))
.render_status(cells, mode, &self.theme.with_dark(dark), scale)
.inspect_err(|error| tracing::warn!(%error, "状态条渲染失败"))
.ok()
}
+1
View File
@@ -243,6 +243,7 @@ impl StatusBar {
&view.mode,
self.dark.get(),
self.dpi.get(),
view.size,
),
_ => None,
}
@@ -421,6 +421,8 @@ enabled = false
# 记住的屏幕位置(物理像素,拖动后自动写入);留空则首次出现在屏幕右下角
# x = 0
# y = 0
# 大小(百分比,60–150);不跟候选字号走
size = 100
[update]
# 检查更新:每天向官网(qingjian.app)读一次版本索引,有新版在菜单与设置的「关于」页提示;请求不带任何标识,不自动下载安装
@@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize};
/// `[status_bar]` 分节:桌面上常驻、可拖动的悬浮状态条(显示当前中 / 英,可选双拼方案)。
/// 与任务栏的中 / 英指示器(语言栏按钮)并存,各是一条。位置记在这里,重启后回到原处。
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct StatusBarConfig {
/// 是否显示悬浮状态条。缺省关。
@@ -13,4 +13,25 @@ pub struct StatusBarConfig {
/// 记住的屏幕纵坐标(内容左上角物理像素)。
pub y: Option<i32>,
/// 大小(百分比,60–150),见 [`Self::size`]。与候选字号无关。
pub size: u8,
}
impl Default for StatusBarConfig {
fn default() -> Self {
Self {
enabled: false,
x: None,
y: None,
size: 100,
}
}
}
impl StatusBarConfig {
/// 缩放倍数,写出范围的收到 60%–150%。
pub fn size(&self) -> f32 {
f32::from(self.size.clamp(60, 150)) / 100.0
}
}
@@ -66,7 +66,7 @@ impl Metrics<'_> {
impl Renderer {
/// 画状态条,返回位图与各格右边界(供点击命中)。`mode` 给节点树画法的主题显示中 / 英等状态。
/// 设置里改了候选字号时整条按同一比例放大缩小。
/// 不跟设置里的候选字号走(字号调大了状态条会比候选窗还显眼),大小由调用方乘进 `scale`。
/// 固定排法:每格宽 = 内容宽 + 两侧内边距,高 = 行高 + 内边距。
pub fn render_status(
&mut self,
@@ -75,8 +75,6 @@ impl Renderer {
theme: &Theme,
scale: f32,
) -> Result<RenderedStatus, RenderError> {
// 整条按候选字的比例缩放:去掉设置里的字号再整体乘倍数,图标、边距、点击边界跟文字一起变,与用哪个文字样式无关
let scale = scale * theme.candidate_scale();
let theme = &theme.with_text_sizes(TextSizes::default());
self.stage.text().use_families(theme.families());
let spec = &theme.file().status;
+1 -1
View File
@@ -410,7 +410,7 @@ impl Theme {
self.animations
}
/// 设置里的候选字号相对主题的比例:表格行高与状态条整体跟着它缩放。
/// 设置里的候选字号相对主题的比例:表格行高与键位提示键盘跟着它缩放。
pub(crate) fn candidate_scale(&self) -> f32 {
self.sizes.ratio("candidate", |name| self.theme_size(name))
}
@@ -171,15 +171,6 @@ fn render_all(mut renderer: Renderer) -> Vec<Shot> {
);
shots.push(shot(format!("{name}-nihao-vertical-light"), rendered, None));
}
// 状态条整条跟候选字号缩放:字、齿轮、边距一起变
let status = renderer
.render_status(&scenes::status_cells(), &Mode::default(), &large, 2.0)
.unwrap();
shots.push(shot(
"large-text-status-light".to_owned(),
status.rendered,
Some(&status.cell_edges),
));
shots
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

@@ -41,4 +41,3 @@ large-text-nihao-vertical-light bitmap 806x987 content 60,60 686x867
large-text-cloud-horizontal-light bitmap 845x314 content 60,60 725x194
large-candidate-nihao-vertical-light bitmap 762x1234 content 60,60 642x1114
large-annotation-nihao-vertical-light bitmap 934x830 content 60,60 814x710
large-text-status-light bitmap 648x261 content 90,90 468x81 edges [230.4375, 374.4375, 468.0]
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

@@ -41,4 +41,3 @@ large-text-nihao-vertical-light bitmap 795x987 content 60,60 675x867
large-text-cloud-horizontal-light bitmap 864x314 content 60,60 744x194
large-candidate-nihao-vertical-light bitmap 737x1234 content 60,60 617x1114
large-annotation-nihao-vertical-light bitmap 910x830 content 60,60 790x710
large-text-status-light bitmap 646x261 content 90,90 466x81 edges [228.70313, 372.70313, 466.0]
+22 -1
View File
@@ -2,7 +2,7 @@
use std::path::PathBuf;
use qingjian_render::{FontLibrary, Mode, Renderer, StatusCell, Theme};
use qingjian_render::{FontLibrary, Mode, Renderer, StatusCell, TextSizes, Theme};
/// 左边按中英各画一个同尺寸不同色的方块(不用字:Linux CI 没有中文字体,两个字都画成同一个缺字框),
/// 格子之间各夹一个 10 点宽的装饰,格子 4 点内边距。
@@ -89,3 +89,24 @@ fn status_tree_cells_and_mode() {
chinese.rendered.pixmap.data()
);
}
/// 设置里调大候选字号,状态条不跟着变:和不调字号画出来一模一样。
#[test]
fn status_ignores_candidate_font_size() {
let Ok(library) = FontLibrary::system("zh-CN") else {
eprintln!("没有系统字体,跳过");
return;
};
let mut renderer = Renderer::new(library);
let cells = [StatusCell::text("中", true), StatusCell::Gear];
let plain = Theme::light();
let large = plain.with_text_sizes(TextSizes::new(32.0, 22.0));
let a = renderer
.render_status(&cells, &Mode::default(), &plain, 2.0)
.unwrap();
let b = renderer
.render_status(&cells, &Mode::default(), &large, 2.0)
.unwrap();
assert_eq!(a.cell_edges, b.cell_edges);
assert_eq!(a.rendered.pixmap.data(), b.rendered.pixmap.data());
}
+1 -1
View File
@@ -67,7 +67,7 @@
- **设置里的字号**:`[general] candidate_font_size` / `annotation_font_size`(0 用主题的,不限范围)由壳经 `Theme::with_text_sizes` 盖到主题上:
`candidate` 与 `annotation` 换成设置的字号,行高同比例;`index`、`preedit` 与表格 `row_height` 按候选字的比例缩放(`theme/text_sizes.rs`)。
表格行高是下限(`minmax(row_height, auto)`),内容更高时撑开。
状态条不逐个样式改字号:`render_status` 去掉设置的字号,把整条的倍数乘上候选字的比例,文字、图标、边距、点击边界一起缩放。
状态条不跟设置的字号:`render_status` 去掉设置的字号按主题原样画,大小由壳按 `[status_bar] size` 乘进整条的倍数(文字、图标、边距、点击边界一起缩放)。
- **状态条**:`status.root` 是一棵与候选窗口同写法的节点树,`repeat` 绑定 `cells` 展开格子(Windows 现在是模式、标点、齿轮三格)。
格子里的条件 `emphasized`(当前模式、生效的全角标点)、`gear`、`first`、`last`,文字绑定 `text`;整条能用 `mode.*`(`Renderer::render_status` 收一份 `Mode`,Windows 的 `StatusView` 带着,与候选窗口同一个 `Router::input_mode`)。
点击按格:每格第一个产出的节点的右边界(根坐标),没画出来的格同前一格,最后一格延到内容右边;格子之间的装饰算前一格(`renderer/status/tree.rs`)。
+1 -1
View File
@@ -62,7 +62,7 @@ description: 进入英文模式的方式、英文补全与拼写纠正、Space
| `符` | | 打开[符号面板](shortcuts.md#符号面板) |
| ⚙ | | 打开设置 |
按住拖动可移到屏幕任意位置,位置会被记住;外观随系统的深浅色与缩放。
按住拖动可移到屏幕任意位置,位置会被记住;外观随系统的深浅色与缩放。大小在「状态条大小」里调(60%–150%),不跟候选字号走。
它只在当前输入法为青简时显示:切换到微软拼音等其他输入法时收起,切回后再次出现;关闭某个应用不影响它。它与任务栏图标并存。
前台是全屏应用(游戏、全屏看视频、演示)时自动收起,退出全屏后回来。也可以在任务栏「中 / 英」图标的右键菜单里随时显示或隐藏它。
+2 -1
View File
@@ -67,6 +67,7 @@ description: 设置窗口各页的内容、打开方式、搜索、输入法菜
| 标点 | 标点先上屏候选 | `nihao,` 一气打完得到「你好,」;关闭时标点进英文直输段 |
| 标点 | 中文与英文、数字之间自动加空格(macOS、Windows) | 「用Go写了30行」上屏成「用 Go 写了 30 行」,见 [中西文自动空格](../input/english-mode.md#中西文自动空格) |
| 状态条(Windows) | 悬浮状态条 | 桌面上的「中 / 英」状态条,见 [悬浮状态条](../input/english-mode.md#悬浮状态条(Windows)) |
| 状态条(Windows) | 状态条大小 | 60%–150%,缺省 100%;不随候选字号变 |
## 外观
@@ -80,7 +81,7 @@ description: 设置窗口各页的内容、打开方式、搜索、输入法菜
| 排布 | 拼音显示位置 | 行内与窗口、只在行内、只在窗口 |
| 文字 | 渲染引擎 | 青简渲染器或系统绘制 |
| 文字 | 字体 | 系统字族;只对青简渲染器生效 |
| 文字 | 候选字号、译文字号 | 单位为点,缺省为主题的字号;「恢复主题字号」回到主题的字号,换主题后跟随新主题;Windows 的悬浮状态条按候选字号整体缩放 |
| 文字 | 候选字号、译文字号 | 单位为点,缺省为主题的字号;「恢复主题字号」回到主题的字号,换主题后跟随新主题 |
| 动效 | 过渡动画 | 关闭后高亮直接跳到位,主题里的循环动画也停止 |
### 横排与拼音位置