Mayo commited on
fix: global font family & disable image transition
Browse files
koharu-renderer/src/facade.rs
CHANGED
|
@@ -146,14 +146,8 @@ impl Renderer {
|
|
| 146 |
text_align: None,
|
| 147 |
});
|
| 148 |
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
style.font_families.insert(0, ff.to_string());
|
| 152 |
-
} else {
|
| 153 |
-
style.font_families.retain(|x| x != ff);
|
| 154 |
-
style.font_families.insert(0, ff.to_string());
|
| 155 |
-
}
|
| 156 |
-
}
|
| 157 |
let font = self.select_font(&style)?;
|
| 158 |
let block_effect = style.effect.unwrap_or(effect);
|
| 159 |
let color = text_block
|
|
@@ -294,6 +288,20 @@ fn default_stroke_width(font_size: f32) -> f32 {
|
|
| 294 |
(font_size * 0.10).clamp(1.2, 8.0)
|
| 295 |
}
|
| 296 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
fn resolve_stroke_style(
|
| 298 |
block: &TextBlock,
|
| 299 |
block_stroke: Option<&TextStrokeStyle>,
|
|
@@ -427,7 +435,10 @@ fn load_symbol_fallbacks(fontbook: &mut FontBook) -> Vec<Font> {
|
|
| 427 |
|
| 428 |
#[cfg(test)]
|
| 429 |
mod tests {
|
| 430 |
-
use super::{
|
|
|
|
|
|
|
|
|
|
| 431 |
use crate::layout::{LayoutLine, LayoutRun, WritingMode};
|
| 432 |
use koharu_types::TextAlign;
|
| 433 |
|
|
@@ -534,4 +545,26 @@ mod tests {
|
|
| 534 |
assert_eq!(layout.lines[0].baseline.1, 32.0);
|
| 535 |
assert_eq!(layout.height, 60.0);
|
| 536 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 537 |
}
|
|
|
|
| 146 |
text_align: None,
|
| 147 |
});
|
| 148 |
|
| 149 |
+
apply_global_font_family(&mut style.font_families, font_family);
|
| 150 |
+
apply_default_font_families(&mut style.font_families, &normalized_translation);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
let font = self.select_font(&style)?;
|
| 152 |
let block_effect = style.effect.unwrap_or(effect);
|
| 153 |
let color = text_block
|
|
|
|
| 288 |
(font_size * 0.10).clamp(1.2, 8.0)
|
| 289 |
}
|
| 290 |
|
| 291 |
+
fn apply_global_font_family(font_families: &mut Vec<String>, font_family: Option<&str>) {
|
| 292 |
+
if font_families.is_empty()
|
| 293 |
+
&& let Some(font_family) = font_family
|
| 294 |
+
{
|
| 295 |
+
font_families.push(font_family.to_string());
|
| 296 |
+
}
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
fn apply_default_font_families(font_families: &mut Vec<String>, text: &str) {
|
| 300 |
+
if font_families.is_empty() {
|
| 301 |
+
*font_families = font_families_for_text(text);
|
| 302 |
+
}
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
fn resolve_stroke_style(
|
| 306 |
block: &TextBlock,
|
| 307 |
block_stroke: Option<&TextStrokeStyle>,
|
|
|
|
| 435 |
|
| 436 |
#[cfg(test)]
|
| 437 |
mod tests {
|
| 438 |
+
use super::{
|
| 439 |
+
align_layout_horizontally, apply_default_font_families, apply_global_font_family,
|
| 440 |
+
center_layout_vertically,
|
| 441 |
+
};
|
| 442 |
use crate::layout::{LayoutLine, LayoutRun, WritingMode};
|
| 443 |
use koharu_types::TextAlign;
|
| 444 |
|
|
|
|
| 545 |
assert_eq!(layout.lines[0].baseline.1, 32.0);
|
| 546 |
assert_eq!(layout.height, 60.0);
|
| 547 |
}
|
| 548 |
+
|
| 549 |
+
#[test]
|
| 550 |
+
fn explicit_block_font_should_not_be_overridden_by_global_font() {
|
| 551 |
+
let mut font_families = vec!["Block Font".to_string()];
|
| 552 |
+
apply_global_font_family(&mut font_families, Some("Global Font"));
|
| 553 |
+
|
| 554 |
+
assert_eq!(font_families, vec!["Block Font".to_string()]);
|
| 555 |
+
}
|
| 556 |
+
|
| 557 |
+
#[test]
|
| 558 |
+
fn global_font_should_fill_empty_block_font_list() {
|
| 559 |
+
let mut font_families = Vec::new();
|
| 560 |
+
apply_global_font_family(&mut font_families, Some("Global Font"));
|
| 561 |
+
assert_eq!(font_families, vec!["Global Font".to_string()]);
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
#[test]
|
| 565 |
+
fn default_font_families_should_fill_empty_list() {
|
| 566 |
+
let mut font_families = Vec::new();
|
| 567 |
+
apply_default_font_families(&mut font_families, "hello");
|
| 568 |
+
assert!(!font_families.is_empty());
|
| 569 |
+
}
|
| 570 |
}
|
ui/components/canvas/Workspace.tsx
CHANGED
|
@@ -299,6 +299,7 @@ export function Workspace() {
|
|
| 299 |
data-testid='workspace-inpainted-image'
|
| 300 |
data={currentDocument.inpainted}
|
| 301 |
visible={showInpaintedImage}
|
|
|
|
| 302 |
/>
|
| 303 |
)}
|
| 304 |
<canvas
|
|
@@ -345,10 +346,11 @@ export function Workspace() {
|
|
| 345 |
style={{ zIndex: 30 }}
|
| 346 |
/>
|
| 347 |
)}
|
| 348 |
-
{currentDocument
|
| 349 |
<Image
|
| 350 |
data-testid='workspace-rendered-image'
|
| 351 |
-
data={currentDocument
|
|
|
|
| 352 |
style={{ zIndex: 40 }}
|
| 353 |
/>
|
| 354 |
)}
|
|
|
|
| 299 |
data-testid='workspace-inpainted-image'
|
| 300 |
data={currentDocument.inpainted}
|
| 301 |
visible={showInpaintedImage}
|
| 302 |
+
transition={false}
|
| 303 |
/>
|
| 304 |
)}
|
| 305 |
<canvas
|
|
|
|
| 346 |
style={{ zIndex: 30 }}
|
| 347 |
/>
|
| 348 |
)}
|
| 349 |
+
{currentDocument.rendered && showRenderedImage && (
|
| 350 |
<Image
|
| 351 |
data-testid='workspace-rendered-image'
|
| 352 |
+
data={currentDocument.rendered}
|
| 353 |
+
transition={false}
|
| 354 |
style={{ zIndex: 40 }}
|
| 355 |
/>
|
| 356 |
)}
|
ui/components/panels/RenderControlsPanel.tsx
CHANGED
|
@@ -110,6 +110,27 @@ const normalizeStroke = (stroke?: Partial<RenderStroke>): RenderStroke => ({
|
|
| 110 |
widthPx: stroke?.widthPx,
|
| 111 |
})
|
| 112 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
const resolveEffectiveTextAlign = (
|
| 114 |
block:
|
| 115 |
| {
|
|
@@ -159,8 +180,8 @@ export function RenderControlsPanel() {
|
|
| 159 |
]
|
| 160 |
const fontOptions = uniqueStrings(fontCandidates)
|
| 161 |
const currentFont =
|
| 162 |
-
fontFamily ??
|
| 163 |
selectedBlock?.style?.fontFamilies?.[0] ??
|
|
|
|
| 164 |
firstBlock?.style?.fontFamilies?.[0] ??
|
| 165 |
(hasBlocks ? fallbackFontFamilies[0] : '')
|
| 166 |
const currentEffect = normalizeEffect(
|
|
@@ -189,15 +210,35 @@ export function RenderControlsPanel() {
|
|
| 189 |
const currentTextAlign = resolveEffectiveTextAlign(
|
| 190 |
selectedBlock ?? firstBlock,
|
| 191 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
|
| 193 |
const buildStyle = (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
style: TextStyle | undefined,
|
| 195 |
updates: Partial<TextStyle>,
|
| 196 |
): TextStyle => ({
|
| 197 |
-
fontFamilies:
|
| 198 |
-
updates.fontFamilies ?? style?.fontFamilies ?? fallbackFontFamilies,
|
| 199 |
fontSize: updates.fontSize ?? style?.fontSize,
|
| 200 |
-
color: updates.color ?? style
|
| 201 |
effect: updates.effect ?? style?.effect,
|
| 202 |
stroke: updates.stroke ?? style?.stroke,
|
| 203 |
textAlign: updates.textAlign ?? style?.textAlign,
|
|
@@ -205,7 +246,7 @@ export function RenderControlsPanel() {
|
|
| 205 |
|
| 206 |
const applyStyleToSelected = (updates: Partial<TextStyle>) => {
|
| 207 |
if (selectedBlockIndex === undefined) return false
|
| 208 |
-
const nextStyle = buildStyle(selectedBlock?.style, updates)
|
| 209 |
void replaceBlock(selectedBlockIndex, { style: nextStyle })
|
| 210 |
return true
|
| 211 |
}
|
|
@@ -214,7 +255,7 @@ export function RenderControlsPanel() {
|
|
| 214 |
if (!hasBlocks) return
|
| 215 |
const nextBlocks = textBlocks.map((block) => ({
|
| 216 |
...block,
|
| 217 |
-
style: buildStyle(block.style, updates),
|
| 218 |
}))
|
| 219 |
void updateTextBlocks(nextBlocks)
|
| 220 |
}
|
|
@@ -273,6 +314,18 @@ export function RenderControlsPanel() {
|
|
| 273 |
|
| 274 |
return (
|
| 275 |
<div className='flex w-full min-w-0 flex-col gap-1.5'>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 276 |
<div className='grid w-full min-w-0 grid-cols-[3.5rem_minmax(0,1fr)] items-center gap-1.5'>
|
| 277 |
<span className='text-muted-foreground text-[10px] font-medium tracking-wide uppercase'>
|
| 278 |
{fontLabel}
|
|
@@ -283,16 +336,16 @@ export function RenderControlsPanel() {
|
|
| 283 |
<Select
|
| 284 |
value={currentFont}
|
| 285 |
onValueChange={(value) => {
|
| 286 |
-
setFontFamily(value)
|
| 287 |
const nextFamilies = mergeFontFamilies(
|
| 288 |
value,
|
| 289 |
selectedBlock?.style?.fontFamilies,
|
| 290 |
)
|
| 291 |
if (applyStyleToSelected({ fontFamilies: nextFamilies })) return
|
|
|
|
| 292 |
if (!hasBlocks) return
|
| 293 |
const nextBlocks = textBlocks.map((block) => ({
|
| 294 |
...block,
|
| 295 |
-
style: buildStyle(block.style, {
|
| 296 |
fontFamilies: mergeFontFamilies(
|
| 297 |
value,
|
| 298 |
block.style?.fontFamilies,
|
|
|
|
| 110 |
widthPx: stroke?.widthPx,
|
| 111 |
})
|
| 112 |
|
| 113 |
+
const resolveStyleColor = (
|
| 114 |
+
style: TextStyle | undefined,
|
| 115 |
+
block:
|
| 116 |
+
| {
|
| 117 |
+
fontPrediction?: {
|
| 118 |
+
text_color: [number, number, number]
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
+
| undefined,
|
| 122 |
+
fallbackColor: RgbaColor,
|
| 123 |
+
): RgbaColor =>
|
| 124 |
+
style?.color ??
|
| 125 |
+
(block?.fontPrediction?.text_color
|
| 126 |
+
? [
|
| 127 |
+
block.fontPrediction.text_color[0],
|
| 128 |
+
block.fontPrediction.text_color[1],
|
| 129 |
+
block.fontPrediction.text_color[2],
|
| 130 |
+
255,
|
| 131 |
+
]
|
| 132 |
+
: fallbackColor)
|
| 133 |
+
|
| 134 |
const resolveEffectiveTextAlign = (
|
| 135 |
block:
|
| 136 |
| {
|
|
|
|
| 180 |
]
|
| 181 |
const fontOptions = uniqueStrings(fontCandidates)
|
| 182 |
const currentFont =
|
|
|
|
| 183 |
selectedBlock?.style?.fontFamilies?.[0] ??
|
| 184 |
+
fontFamily ??
|
| 185 |
firstBlock?.style?.fontFamilies?.[0] ??
|
| 186 |
(hasBlocks ? fallbackFontFamilies[0] : '')
|
| 187 |
const currentEffect = normalizeEffect(
|
|
|
|
| 210 |
const currentTextAlign = resolveEffectiveTextAlign(
|
| 211 |
selectedBlock ?? firstBlock,
|
| 212 |
)
|
| 213 |
+
const scopeLabel =
|
| 214 |
+
selectedBlockIndex !== undefined
|
| 215 |
+
? t('render.fontScopeBlockIndex', {
|
| 216 |
+
index: selectedBlockIndex + 1,
|
| 217 |
+
defaultValue: `Block ${selectedBlockIndex + 1}`,
|
| 218 |
+
})
|
| 219 |
+
: t('render.fontScopeGlobal', {
|
| 220 |
+
defaultValue: 'Global',
|
| 221 |
+
})
|
| 222 |
+
const scopeToneClass =
|
| 223 |
+
selectedBlockIndex !== undefined
|
| 224 |
+
? 'border-primary/20 bg-primary/10 text-primary'
|
| 225 |
+
: 'border-border/60 bg-muted text-muted-foreground'
|
| 226 |
|
| 227 |
const buildStyle = (
|
| 228 |
+
block:
|
| 229 |
+
| {
|
| 230 |
+
style?: TextStyle
|
| 231 |
+
fontPrediction?: {
|
| 232 |
+
text_color: [number, number, number]
|
| 233 |
+
}
|
| 234 |
+
}
|
| 235 |
+
| undefined,
|
| 236 |
style: TextStyle | undefined,
|
| 237 |
updates: Partial<TextStyle>,
|
| 238 |
): TextStyle => ({
|
| 239 |
+
fontFamilies: updates.fontFamilies ?? style?.fontFamilies ?? [],
|
|
|
|
| 240 |
fontSize: updates.fontSize ?? style?.fontSize,
|
| 241 |
+
color: updates.color ?? resolveStyleColor(style, block, fallbackColor),
|
| 242 |
effect: updates.effect ?? style?.effect,
|
| 243 |
stroke: updates.stroke ?? style?.stroke,
|
| 244 |
textAlign: updates.textAlign ?? style?.textAlign,
|
|
|
|
| 246 |
|
| 247 |
const applyStyleToSelected = (updates: Partial<TextStyle>) => {
|
| 248 |
if (selectedBlockIndex === undefined) return false
|
| 249 |
+
const nextStyle = buildStyle(selectedBlock, selectedBlock?.style, updates)
|
| 250 |
void replaceBlock(selectedBlockIndex, { style: nextStyle })
|
| 251 |
return true
|
| 252 |
}
|
|
|
|
| 255 |
if (!hasBlocks) return
|
| 256 |
const nextBlocks = textBlocks.map((block) => ({
|
| 257 |
...block,
|
| 258 |
+
style: buildStyle(block, block.style, updates),
|
| 259 |
}))
|
| 260 |
void updateTextBlocks(nextBlocks)
|
| 261 |
}
|
|
|
|
| 314 |
|
| 315 |
return (
|
| 316 |
<div className='flex w-full min-w-0 flex-col gap-1.5'>
|
| 317 |
+
<div className='flex items-center justify-end'>
|
| 318 |
+
<span
|
| 319 |
+
data-testid='render-scope-indicator'
|
| 320 |
+
className={cn(
|
| 321 |
+
'rounded-full border px-2 py-0.5 text-[10px] font-medium tracking-wide uppercase',
|
| 322 |
+
scopeToneClass,
|
| 323 |
+
)}
|
| 324 |
+
>
|
| 325 |
+
{scopeLabel}
|
| 326 |
+
</span>
|
| 327 |
+
</div>
|
| 328 |
+
|
| 329 |
<div className='grid w-full min-w-0 grid-cols-[3.5rem_minmax(0,1fr)] items-center gap-1.5'>
|
| 330 |
<span className='text-muted-foreground text-[10px] font-medium tracking-wide uppercase'>
|
| 331 |
{fontLabel}
|
|
|
|
| 336 |
<Select
|
| 337 |
value={currentFont}
|
| 338 |
onValueChange={(value) => {
|
|
|
|
| 339 |
const nextFamilies = mergeFontFamilies(
|
| 340 |
value,
|
| 341 |
selectedBlock?.style?.fontFamilies,
|
| 342 |
)
|
| 343 |
if (applyStyleToSelected({ fontFamilies: nextFamilies })) return
|
| 344 |
+
setFontFamily(value)
|
| 345 |
if (!hasBlocks) return
|
| 346 |
const nextBlocks = textBlocks.map((block) => ({
|
| 347 |
...block,
|
| 348 |
+
style: buildStyle(block, block.style, {
|
| 349 |
fontFamilies: mergeFontFamilies(
|
| 350 |
value,
|
| 351 |
block.style?.fontFamilies,
|