Improve Korean UI text fitting
This commit is contained in:
@@ -70,6 +70,16 @@ const DIALOGUE_PANEL_SIZE := Vector2(1200, 356)
|
||||
const DIALOGUE_PORTRAIT_SIZE := Vector2(220, 310)
|
||||
const DIALOGUE_COLUMN_SIZE := Vector2(850, 318)
|
||||
const DIALOGUE_TEXT_SIZE := Vector2(830, 164)
|
||||
const OBJECTIVE_HUD_FONT_SIZE := 13
|
||||
const OBJECTIVE_HUD_MIN_FONT_SIZE := 11
|
||||
const OBJECTIVE_NOTICE_FONT_SIZE := 15
|
||||
const OBJECTIVE_NOTICE_MIN_FONT_SIZE := 12
|
||||
const BRIEFING_OBJECTIVE_FONT_SIZE := 15
|
||||
const BRIEFING_OBJECTIVE_MIN_FONT_SIZE := 12
|
||||
const DIALOGUE_SPEAKER_FONT_SIZE := 18
|
||||
const DIALOGUE_SPEAKER_MIN_FONT_SIZE := 15
|
||||
const DIALOGUE_TEXT_FONT_SIZE := 17
|
||||
const DIALOGUE_TEXT_MIN_FONT_SIZE := 14
|
||||
const BRIEFING_CAMP_THUMBNAIL_SIZE := Vector2(188, 86)
|
||||
const CAMP_TALK_PORTRAIT_SIZE := Vector2(58, 58)
|
||||
const SHOP_MERCHANT_BADGE_SIZE := Vector2(50, 50)
|
||||
@@ -735,6 +745,63 @@ func _apply_label_style(label: Label, font_color: Color, font_size: int = 0) ->
|
||||
_apply_control_font(label, font_size >= 16)
|
||||
|
||||
|
||||
func _fit_label_font_size_to_text(label: Label, text: String, base_size: int, min_size: int, available_size: Vector2 = Vector2.ZERO) -> int:
|
||||
if label == null:
|
||||
return base_size
|
||||
var fit_size := available_size
|
||||
if fit_size.x <= 0.0:
|
||||
fit_size.x = label.custom_minimum_size.x
|
||||
if fit_size.y <= 0.0:
|
||||
fit_size.y = label.custom_minimum_size.y
|
||||
var font_size := _fit_font_size_for_text(text, base_size, min_size, fit_size)
|
||||
label.add_theme_font_size_override("font_size", font_size)
|
||||
return font_size
|
||||
|
||||
|
||||
func _fit_font_size_for_text(text: String, base_size: int, min_size: int, available_size: Vector2) -> int:
|
||||
if text.strip_edges().is_empty():
|
||||
return base_size
|
||||
var safe_base: int = maxi(base_size, min_size)
|
||||
var safe_min: int = maxi(1, mini(base_size, min_size))
|
||||
for font_size in range(safe_base, safe_min - 1, -1):
|
||||
if _estimated_wrapped_text_height(text, available_size.x, font_size) <= available_size.y:
|
||||
return font_size
|
||||
return safe_min
|
||||
|
||||
|
||||
func _estimated_wrapped_text_height(text: String, width: float, font_size: int) -> float:
|
||||
var line_count := _estimated_wrapped_line_count(text, width, font_size)
|
||||
return float(line_count) * _estimated_line_height(font_size)
|
||||
|
||||
|
||||
func _estimated_wrapped_line_count(text: String, width: float, font_size: int) -> int:
|
||||
var capacity := maxf(1.0, floorf(width / maxf(1.0, float(font_size) * 0.92)))
|
||||
var count := 0
|
||||
for paragraph in text.split("\n"):
|
||||
var weighted_length := _weighted_text_length(str(paragraph))
|
||||
count += maxi(1, int(ceil(weighted_length / capacity)))
|
||||
return maxi(1, count)
|
||||
|
||||
|
||||
func _estimated_line_height(font_size: int) -> float:
|
||||
return ceilf(float(font_size) * 1.36)
|
||||
|
||||
|
||||
func _weighted_text_length(text: String) -> float:
|
||||
var total := 0.0
|
||||
for index in range(text.length()):
|
||||
var code := text.unicode_at(index)
|
||||
if code == 10:
|
||||
continue
|
||||
if code == 32:
|
||||
total += 0.35
|
||||
elif code < 128:
|
||||
total += 0.56
|
||||
else:
|
||||
total += 1.0
|
||||
return total
|
||||
|
||||
|
||||
func _make_separator(width: float = 0.0, height: float = 2.0) -> ColorRect:
|
||||
var separator := ColorRect.new()
|
||||
separator.color = UI_OLD_BRONZE
|
||||
@@ -1133,7 +1200,7 @@ func _create_hud() -> void:
|
||||
objective_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
objective_label.custom_minimum_size = Vector2(382, 36)
|
||||
objective_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_apply_label_style(objective_label, UI_AGED_INK, 13)
|
||||
_apply_label_style(objective_label, UI_AGED_INK, OBJECTIVE_HUD_FONT_SIZE)
|
||||
objective_top_row.add_child(objective_label)
|
||||
objective_top_row.add_child(_make_bamboo_gutter(14, 36))
|
||||
|
||||
@@ -1171,7 +1238,7 @@ func _create_hud() -> void:
|
||||
objective_notice_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
objective_notice_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
objective_notice_label.custom_minimum_size = Vector2(548, 80)
|
||||
_apply_label_style(objective_notice_label, UI_AGED_INK, 15)
|
||||
_apply_label_style(objective_notice_label, UI_AGED_INK, OBJECTIVE_NOTICE_FONT_SIZE)
|
||||
objective_notice_column.add_child(objective_notice_label)
|
||||
objective_notice_column.add_child(_make_scroll_rod(548, 3))
|
||||
|
||||
@@ -1593,7 +1660,7 @@ func _create_hud() -> void:
|
||||
briefing_objective_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
briefing_objective_label.custom_minimum_size = Vector2(500, 104)
|
||||
briefing_objective_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_apply_label_style(briefing_objective_label, UI_AGED_INK, 15)
|
||||
_apply_label_style(briefing_objective_label, UI_AGED_INK, BRIEFING_OBJECTIVE_FONT_SIZE)
|
||||
briefing_objective_row.add_child(briefing_objective_label)
|
||||
briefing_objective_row.add_child(_make_edict_marker_stack(["", "", ""], 104))
|
||||
briefing_objective_row.add_child(_make_bamboo_gutter(18, 104))
|
||||
@@ -1930,7 +1997,7 @@ func _create_hud() -> void:
|
||||
dialogue_speaker_label.custom_minimum_size = Vector2(DIALOGUE_TEXT_SIZE.x - 88, 24)
|
||||
dialogue_speaker_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
dialogue_speaker_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
_apply_label_style(dialogue_speaker_label, UI_PARCHMENT_TEXT, 18)
|
||||
_apply_label_style(dialogue_speaker_label, UI_PARCHMENT_TEXT, DIALOGUE_SPEAKER_FONT_SIZE)
|
||||
dialogue_speaker_row.add_child(dialogue_speaker_label)
|
||||
dialogue_speaker_row.add_child(_make_seal_tile("", 24))
|
||||
|
||||
@@ -1951,7 +2018,7 @@ func _create_hud() -> void:
|
||||
dialogue_text_label.custom_minimum_size = Vector2(DIALOGUE_TEXT_SIZE.x - 164, DIALOGUE_TEXT_SIZE.y - 20)
|
||||
dialogue_text_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
dialogue_text_label.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
_apply_label_style(dialogue_text_label, UI_AGED_INK, 17)
|
||||
_apply_label_style(dialogue_text_label, UI_AGED_INK, DIALOGUE_TEXT_FONT_SIZE)
|
||||
dialogue_text_row.add_child(dialogue_text_label)
|
||||
dialogue_text_row.add_child(_make_edict_marker_stack(["", "", ""], DIALOGUE_TEXT_SIZE.y - 20))
|
||||
dialogue_text_row.add_child(_make_bamboo_gutter(18, DIALOGUE_TEXT_SIZE.y - 20))
|
||||
@@ -4419,6 +4486,12 @@ func _refresh_ranges() -> void:
|
||||
func _update_hud() -> void:
|
||||
status_label.text = state.get_status_text()
|
||||
objective_label.text = _format_objective_hud_text()
|
||||
_fit_label_font_size_to_text(
|
||||
objective_label,
|
||||
objective_label.text,
|
||||
OBJECTIVE_HUD_FONT_SIZE,
|
||||
OBJECTIVE_HUD_MIN_FONT_SIZE
|
||||
)
|
||||
campaign_status_label.text = campaign_state.get_progress_text()
|
||||
_update_mission_panel()
|
||||
_update_cell_info()
|
||||
@@ -5166,6 +5239,12 @@ func _on_objective_updated(victory: String, defeat: String) -> void:
|
||||
notice_lines.append("주의: %s" % defeat)
|
||||
if objective_notice_label != null:
|
||||
objective_notice_label.text = "\n".join(notice_lines)
|
||||
_fit_label_font_size_to_text(
|
||||
objective_notice_label,
|
||||
objective_notice_label.text,
|
||||
OBJECTIVE_NOTICE_FONT_SIZE,
|
||||
OBJECTIVE_NOTICE_MIN_FONT_SIZE
|
||||
)
|
||||
if objective_notice_panel != null:
|
||||
objective_notice_panel.visible = true
|
||||
objective_notice_timer = OBJECTIVE_NOTICE_DURATION
|
||||
@@ -5513,7 +5592,19 @@ func _render_dialogue_line() -> void:
|
||||
_apply_dialogue_side(side)
|
||||
dialogue_speaker_label.text = display_speaker if not display_speaker.is_empty() else ""
|
||||
dialogue_speaker_label.visible = not display_speaker.is_empty()
|
||||
_fit_label_font_size_to_text(
|
||||
dialogue_speaker_label,
|
||||
dialogue_speaker_label.text,
|
||||
DIALOGUE_SPEAKER_FONT_SIZE,
|
||||
DIALOGUE_SPEAKER_MIN_FONT_SIZE
|
||||
)
|
||||
dialogue_text_label.text = _format_dialogue_body_text(str(line.get("text", "")))
|
||||
_fit_label_font_size_to_text(
|
||||
dialogue_text_label,
|
||||
dialogue_text_label.text,
|
||||
DIALOGUE_TEXT_FONT_SIZE,
|
||||
DIALOGUE_TEXT_MIN_FONT_SIZE
|
||||
)
|
||||
_update_dialogue_portrait(display_speaker, str(line.get("portrait", "")))
|
||||
_update_dialogue_controls()
|
||||
dialogue_panel.visible = true
|
||||
@@ -6160,6 +6251,12 @@ func _show_briefing() -> void:
|
||||
briefing_location_label.visible = not briefing_location_label.text.is_empty()
|
||||
briefing_objective_collapsed = false
|
||||
briefing_objective_label.text = _format_briefing_objectives()
|
||||
_fit_label_font_size_to_text(
|
||||
briefing_objective_label,
|
||||
briefing_objective_label.text,
|
||||
BRIEFING_OBJECTIVE_FONT_SIZE,
|
||||
BRIEFING_OBJECTIVE_MIN_FONT_SIZE
|
||||
)
|
||||
briefing_objective_label.visible = not briefing_objective_label.text.is_empty()
|
||||
_update_briefing_objective_visibility()
|
||||
briefing_label.text = body
|
||||
|
||||
@@ -166,9 +166,50 @@ func _check_readability_contract(failures: Array[String]) -> void:
|
||||
failures.append("briefing objective section should have a close/open button")
|
||||
elif scene.briefing_objective_toggle_button.text != "닫기":
|
||||
failures.append("briefing objective button should start with a clear close label")
|
||||
_check_text_fit_contract(scene, failures)
|
||||
scene.free()
|
||||
|
||||
|
||||
func _check_text_fit_contract(scene, failures: Array[String]) -> void:
|
||||
for method_name in [
|
||||
"_fit_label_font_size_to_text",
|
||||
"_fit_font_size_for_text",
|
||||
"_estimated_wrapped_text_height",
|
||||
"_estimated_wrapped_line_count",
|
||||
"_weighted_text_length"
|
||||
]:
|
||||
if not scene.has_method(method_name):
|
||||
failures.append("missing text fit helper: %s" % method_name)
|
||||
|
||||
var dialogue_area := Vector2(666.0, 144.0)
|
||||
var short_dialogue_size := int(scene._fit_font_size_for_text("군령을 전하라.", 17, 14, dialogue_area))
|
||||
if short_dialogue_size != 17:
|
||||
failures.append("short dialogue text should keep the base font size, got %d" % short_dialogue_size)
|
||||
var long_dialogue := "「" \
|
||||
+ "서영의 매복은 아직 끝나지 않았다. 숲 뒤 봉쇄병이 동쪽 길목을 닫기 전에 전열을 유지하고, 부상병을 마을로 물려 회복시킨 뒤 다시 밀어붙여라. " \
|
||||
+ "하후돈은 비탈 궁병을 묶고, 하후연은 길목을 살펴라. 조인은 뒤따르는 병력을 모아 조조의 깃발이 끊기지 않게 하라. " \
|
||||
+ "마지막 봉쇄병이 모습을 드러내면 조급히 흩어지지 말고, 마을과 성채의 회복지를 번갈아 쓰며 전열을 앞으로 밀어라. " \
|
||||
+ "적이 다시 숲을 흔들어도 군령은 하나다. 길목을 열고 남은 병력을 정리한 뒤 추격로를 확보하라. " \
|
||||
+ "북쪽 비탈과 남쪽 수풀을 동시에 살피고, 어느 한 장수가 고립되지 않도록 서로의 이동 거리를 맞추어라.」"
|
||||
var fitted_dialogue_size := int(scene._fit_font_size_for_text(long_dialogue, 17, 14, dialogue_area))
|
||||
if fitted_dialogue_size >= 17:
|
||||
failures.append("long dialogue text should reduce font size to avoid clipping")
|
||||
if fitted_dialogue_size < 14:
|
||||
failures.append("long dialogue text should not shrink below its readable minimum")
|
||||
var fitted_dialogue_height := float(scene._estimated_wrapped_text_height(long_dialogue, dialogue_area.x, fitted_dialogue_size))
|
||||
if fitted_dialogue_height > dialogue_area.y:
|
||||
failures.append("fitted long dialogue should stay within the dialogue text area")
|
||||
|
||||
var objective_area := Vector2(500.0, 104.0)
|
||||
var long_objective := "승리: 서영의 후군을 꺾었다. 동쪽 길목을 장악하고 뒤따르는 봉쇄병까지 정리하라.\n패배: 조조가 퇴각하거나 제17턴이 시작되면 패한다.\n보상: 군자금 700냥, 콩, 술, 철검"
|
||||
var fitted_objective_size := int(scene._fit_font_size_for_text(long_objective, 15, 12, objective_area))
|
||||
if fitted_objective_size < 12 or fitted_objective_size > 15:
|
||||
failures.append("briefing objective font fit should stay inside its configured bounds")
|
||||
var fitted_objective_height := float(scene._estimated_wrapped_text_height(long_objective, objective_area.x, fitted_objective_size))
|
||||
if fitted_objective_height > objective_area.y:
|
||||
failures.append("fitted briefing objective text should stay within its panel")
|
||||
|
||||
|
||||
func _check_dialogue_localization_and_side(failures: Array[String]) -> void:
|
||||
var scene = BattleSceneScript.new()
|
||||
var lines := scene._normalized_dialogue_lines([
|
||||
|
||||
Reference in New Issue
Block a user