Merge pull request #543 from fanlusky/fix/willow-green-horizontal-layout

fix(server): 杨柳青横排翻译贴底、行尾留白且末项高亮不铺满
This commit is contained in:
Lu Fan(SonnyCalcr) authored and GitHub committed 2026-09-30 01:40:42 +08:00
commit f79c77364a
4 files changed
+89 -6

No files matched your search

@@ -109,6 +109,8 @@ struct CandSkinTokens
float itemExtraHeight = 2.0f;
float itemPadLeft = 0.0f;
float itemPadRight = 0.0f;
// 横排时把卡片多出的宽度均分给同一行各项,铺满高亮的皮肤(杨柳青)末项才能贴到卡片右边。
bool justifyRows = false;
msimeui::Thickness preeditMargin{};
D2D1_COLOR_F rowTextSelected = D2D1::ColorF(0, 0.0f);
D2D1_COLOR_F rowLabelSelected = D2D1::ColorF(0, 0.0f);
@@ -424,6 +426,7 @@ void CandidatePresenter::ApplySkin()
tokens.itemRadius = 0.0f;
tokens.outerItemRadius = 9.0f;
tokens.itemGap = 0.0f;
tokens.justifyRows = true;
// 行内边距对齐 CSS 的 .row.cand padding(加上 .text 的左内边距),预编辑行对齐 .row.pinyin。
if (GetConfiguredCandidateWindowLayout() == "horizontal")
{
@@ -620,6 +623,9 @@ void CandidatePresenter::ApplySkin()
appearance.annotationFontSize = fontSize;
appearance.contentPadLeft = tokens.itemPadLeft;
appearance.contentPadRight = tokens.itemPadRight;
// itemExtraHeight 上下各半:横排翻译换到第二行时从下半段内边距上方开始,内边距留在翻译下面,
// 对应 CSS 里 .row.cand 的 padding 包住整个 .text(杨柳青横排上下各 6px)。
appearance.contentPadBottom = tokens.itemExtraHeight * 0.5f;
appearance.textPadLeft = 5.0f;
appearance.labelGap = 1.5f;
appearance.selectedBarWidth = 3.0f;
@@ -631,6 +637,7 @@ void CandidatePresenter::ApplySkin()
const bool preeditHidden = GetConfiguredCandidateWindowPreeditStyle() == "empty";
appearance.outerCornerRadius = tokens.outerItemRadius;
appearance.outerTopCornersEnabled = preeditHidden;
appearance.justifyHorizontalRows = tokens.justifyRows;
// 隐藏的预编辑行高度为 0,但 body 仍在它和列表之间留 2px 行距;贴角皮肤把它抵掉,
// 让首项贴着卡片的内容顶边(秋桂四边内边距一致,杨柳青高亮直抵卡片顶边)。
if (preeditHidden)
+8
View File
@@ -530,8 +530,16 @@ class CandidateList : public Visual
// row) sits between the list and the frame's top edge.
float outerCornerRadius = 0.0f;
bool outerTopCornersEnabled = true;
// Horizontal only: when the list is arranged wider than a line of
// items, spread the spare width evenly over that line's items so the
// last one reaches the list's right edge (full-bleed highlights).
bool justifyHorizontalRows = false;
float contentPadLeft = 5.0f;
float contentPadRight = 5.0f;
// Part of itemHeight that is the row's bottom padding. Lines stacked
// under the first one (wrapped annotation, horizontal translation)
// start above it, and the padding is restored below the last line.
float contentPadBottom = 0.0f;
float textPadLeft = 0.0f;
float labelGap = 1.5f;
float selectedBarWidth = 3.0f;
+34 -6
View File
@@ -325,6 +325,10 @@ CandidateList::ItemGeometry CandidateList::MeasureItem(size_t index, float width
geometry.text = {textX, 0.0f, textW, textH};
float height = textH;
float lineEnd = textW;
// height 末尾属于行底内边距的部分:只有单行高度(itemHeight)里才含这段内边距,
// 换到下方的辅助码和翻译从它上面开始排,排完再把它补回最后一行下面。
const float padBottom = std::clamp(appearance_.contentPadBottom, 0.0f, appearance_.itemHeight * 0.5f);
float padBelow = textH > appearance_.itemHeight ? 0.0f : padBottom;
// 短字段保持原有并排方式;空间不足时让辅助码和翻译完整换行。
if (!item.annotation.empty())
@@ -334,9 +338,13 @@ CandidateList::ItemGeometry CandidateList::MeasureItem(size_t index, float width
const bool inlineAnnotation = lineEnd + 4.0f + annotationW <= contentWidth;
const float annotationH = std::max(
appearance_.itemHeight, MeasureTextHeight(item.annotation, appearance_.annotationFontSize, annotationW));
geometry.annotation = {textX + (inlineAnnotation ? lineEnd + 4.0f : 0.0f), inlineAnnotation ? 0.0f : height,
annotationW, annotationH};
height = std::max(height, geometry.annotation.y + annotationH);
geometry.annotation = {textX + (inlineAnnotation ? lineEnd + 4.0f : 0.0f),
inlineAnnotation ? 0.0f : height - padBelow, annotationW, annotationH};
if (geometry.annotation.y + annotationH > height)
{
height = geometry.annotation.y + annotationH;
padBelow = annotationH > appearance_.itemHeight ? 0.0f : padBottom;
}
lineEnd = geometry.annotation.x - textX + annotationW;
}
if (!item.translation.empty())
@@ -353,9 +361,9 @@ CandidateList::ItemGeometry CandidateList::MeasureItem(size_t index, float width
: (naturalTranslationWidth <= contentWidth
? fontSize * 1.25f
: std::max(fontSize * 1.25f, MeasureTextHeight(item.translation, fontSize, translationW)));
geometry.translation = {textX + (inlineTranslation ? lineEnd + gap : 0.0f), inlineTranslation ? 0.0f : height,
translationW, translationH};
height = std::max(height, geometry.translation.y + translationH);
geometry.translation = {textX + (inlineTranslation ? lineEnd + gap : 0.0f),
inlineTranslation ? 0.0f : height - padBelow, translationW, translationH};
height = std::max(height, geometry.translation.y + translationH + (inlineTranslation ? 0.0f : padBelow));
}
geometry.bounds = {0.0f, 0.0f, width, height};
return geometry;
@@ -377,6 +385,26 @@ RectF CandidateList::ItemRect(size_t index) const
rect.x = bounds_.x;
rect.width = bounds_.width;
}
else if (appearance_.justifyHorizontalRows)
{
// 横排铺满:列表比这一行的自然宽度宽时(卡片保持了同一输入下更宽的尺寸),把多出的宽度
// 均分给这一行的每一项,末项右边贴着列表右边,高亮不会在行尾留下一截空白。
const float lineY = itemGeometry_[index].bounds.y;
size_t first = index;
while (first > 0 && itemGeometry_[first - 1].bounds.y == lineY)
--first;
size_t last = index;
while (last + 1 < itemGeometry_.size() && itemGeometry_[last + 1].bounds.y == lineY)
++last;
const RectF &tail = itemGeometry_[last].bounds;
const float extra = bounds_.width - (tail.x + tail.width);
if (extra > 0.0f)
{
const float share = extra / static_cast<float>(last - first + 1);
rect.x += share * static_cast<float>(index - first);
rect.width += share;
}
}
return rect;
}
+40
View File
@@ -244,6 +244,46 @@ TEST_CASE(horizontal_candidate_translation_uses_a_second_line)
REQUIRE_NEAR(list.MeasureInLayout({1000.0f, 1000.0f}).height, plain.height);
}
TEST_CASE(horizontal_candidate_translation_keeps_the_row_bottom_padding_below_it)
{
// 行高里的底部内边距挪到翻译下面,而不是夹在候选与翻译之间;挪动不应让整行变高。
CandidateList list(38.0f);
CandidateList::Appearance appearance;
appearance.itemHeight = 38.0f;
appearance.contentPadBottom = 5.0f;
list.SetAppearance(appearance);
list.SetOrientation(CandidateList::Orientation::Horizontal);
list.SetItems({{L"1", L"candidate", L"", L"word"}});
const float translationH = appearance.fontSize * 0.78f * 1.25f;
REQUIRE_NEAR(list.MeasureInLayout({1000.0f, 1000.0f}).height, 38.0f + translationH);
list.ArrangeInLayout({0.0f, 0.0f, 1000.0f, 38.0f + translationH});
const RectF item = list.GetItemBounds(0);
REQUIRE_NEAR(item.height, 38.0f + translationH);
}
TEST_CASE(justified_horizontal_candidates_spread_spare_width_to_the_right_edge)
{
CandidateList list(28.0f);
CandidateList::Appearance appearance;
appearance.itemHeight = 28.0f;
appearance.itemGap = 0.0f;
appearance.justifyHorizontalRows = true;
list.SetAppearance(appearance);
list.SetOrientation(CandidateList::Orientation::Horizontal);
list.SetItems({{L"1", L"候选", L"", L""}, {L"2", L"候选", L"", L""}});
const SizeF natural = list.MeasureInLayout({1000.0f, 1000.0f});
const float naturalFirstWidth = list.GetItemBounds(0).width;
// 卡片保持了更宽的尺寸:多出的 40 均分给两项,末项右边贴着列表右边。
list.ArrangeInLayout({10.0f, 0.0f, natural.width + 40.0f, natural.height});
const RectF first = list.GetItemBounds(0);
const RectF second = list.GetItemBounds(1);
REQUIRE_NEAR(first.x, 10.0f);
REQUIRE_NEAR(first.width, naturalFirstWidth + 20.0f);
REQUIRE_NEAR(second.x, first.x + first.width);
REQUIRE_NEAR(second.x + second.width, 10.0f + natural.width + 40.0f);
}
TEST_CASE(vertical_candidate_rows_fill_a_wider_arranged_width)
{
// 竖排候选项的自然宽度较窄,但被分配到更宽的行宽时(如卡片最小宽度撑大),