forked from 1b-agent/opencadstudio-upstream
Text layout: right-to-left runs and leading spaces like the reference
MTEXT paragraphs keep left-to-right order and their attachment's alignment even when they start with Hebrew or Arabic: a run of right-to-left words (with the spaces between and after them) reads backwards in place, and inside a word each right-to-left letter run is reversed on its own while digits, punctuation and Latin keep their order. Shaped text gets a left-to-right paragraph direction, so TEXT follows the bidirectional rules with a left-to-right base, and a TEXT line that starts with Hebrew or Arabic is no longer right-aligned. A paragraph's leading spaces keep their width and trailing spaces are left out of the width used for centring and right alignment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3830a69ca7
commit
acafb3be29
3 files changed
+122
-81
No files matched your search
+5
-30
@@ -281,38 +281,13 @@ pub fn text_run_placement_at_scale(
|
|||||||
// box one way while the strokes run the other (the bounds advance is
|
// box one way while the strokes run the other (the bounds advance is
|
||||||
// always positive — it uses |width_factor|). Left keeps its 0 reference.
|
// always positive — it uses |width_factor|). Left keeps its 0 reference.
|
||||||
let sign = width_factor.signum();
|
let sign = width_factor.signum();
|
||||||
let is_rtl = {
|
// Left / right alignment do not depend on the text's script: the
|
||||||
let mut strong_rtl = false;
|
// reference lays a line that starts with Hebrew or Arabic out from its
|
||||||
for ch in value_for_bounds.chars() {
|
// left end like any other.
|
||||||
match unicode_bidi::bidi_class(ch) {
|
|
||||||
unicode_bidi::BidiClass::R | unicode_bidi::BidiClass::AL => {
|
|
||||||
strong_rtl = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
unicode_bidi::BidiClass::L => {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
strong_rtl
|
|
||||||
};
|
|
||||||
let ax = match t.horizontal_alignment {
|
let ax = match t.horizontal_alignment {
|
||||||
HA::Left => {
|
HA::Left => 0.0,
|
||||||
if is_rtl {
|
|
||||||
b.advance * sign
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
HA::Center | HA::Middle => b.advance * 0.5 * sign,
|
HA::Center | HA::Middle => b.advance * 0.5 * sign,
|
||||||
HA::Right => {
|
HA::Right => b.advance * sign,
|
||||||
if is_rtl {
|
|
||||||
0.0
|
|
||||||
} else {
|
|
||||||
b.advance * sign
|
|
||||||
}
|
|
||||||
}
|
|
||||||
HA::Aligned | HA::Fit => 0.0,
|
HA::Aligned | HA::Fit => 0.0,
|
||||||
};
|
};
|
||||||
// Vertical anchor uses the inked extent (cap / baseline geometry).
|
// Vertical anchor uses the inked extent (cap / baseline geometry).
|
||||||
|
|||||||
@@ -867,6 +867,73 @@ pub fn is_rtl_char(c: char) -> bool {
|
|||||||
/// Reorder a line's atoms into visual order (left to right) using the Unicode
|
/// Reorder a line's atoms into visual order (left to right) using the Unicode
|
||||||
/// Bidirectional Algorithm (UBA) Rule L2. If the line contains no RTL characters
|
/// Bidirectional Algorithm (UBA) Rule L2. If the line contains no RTL characters
|
||||||
/// and is not an RTL paragraph, returns the atoms unchanged.
|
/// and is not an RTL paragraph, returns the atoms unchanged.
|
||||||
|
/// Left-to-right mark: keeps the shaper's paragraph direction left to right
|
||||||
|
/// and closes a right-to-left run inside a word.
|
||||||
|
pub(crate) const LRM: char = '';
|
||||||
|
|
||||||
|
/// Whether a word is written right to left as a whole: right-to-left
|
||||||
|
/// letters (and their marks) only.
|
||||||
|
fn is_rtl_word(w: &str) -> bool {
|
||||||
|
w.chars().any(is_rtl_char)
|
||||||
|
&& w.chars().all(|c| {
|
||||||
|
matches!(
|
||||||
|
unicode_bidi::bidi_class(c),
|
||||||
|
unicode_bidi::BidiClass::R | unicode_bidi::BidiClass::AL | unicode_bidi::BidiClass::NSM
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A word as the reference draws it inside MTEXT: each run of right-to-left
|
||||||
|
/// letters reads backwards in place, everything else (digits, punctuation,
|
||||||
|
/// Latin) keeps its order — `אב_123_גד` shows `בא_123_דג`. A mark after
|
||||||
|
/// each right-to-left run keeps the shaper from carrying neutrals and
|
||||||
|
/// numbers into it.
|
||||||
|
pub(crate) fn rtl_display_word(word: String) -> String {
|
||||||
|
if !word.chars().any(is_rtl_char) || is_rtl_word(&word) {
|
||||||
|
return word;
|
||||||
|
}
|
||||||
|
let mut out = String::with_capacity(word.len() + 6);
|
||||||
|
let mut in_rtl = false;
|
||||||
|
for c in word.chars() {
|
||||||
|
let rtl = matches!(
|
||||||
|
unicode_bidi::bidi_class(c),
|
||||||
|
unicode_bidi::BidiClass::R | unicode_bidi::BidiClass::AL
|
||||||
|
) || (in_rtl && unicode_bidi::bidi_class(c) == unicode_bidi::BidiClass::NSM);
|
||||||
|
if in_rtl && !rtl {
|
||||||
|
out.push(LRM);
|
||||||
|
}
|
||||||
|
out.push(c);
|
||||||
|
in_rtl = rtl;
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The reference's MTEXT line order: words keep their order left to right,
|
||||||
|
/// except that a run of right-to-left words — with the spaces between and
|
||||||
|
/// after them — reads backwards as a whole (`אבג דה abc` shows
|
||||||
|
/// ` הד גבאabc`). Each right-to-left word is itself shaped backwards.
|
||||||
|
pub fn reorder_rtl_runs(atoms: Vec<LayoutAtom>) -> Vec<LayoutAtom> {
|
||||||
|
let rtl = |a: &LayoutAtom| matches!(&a.kind, AtomKind::Word(w) if is_rtl_word(w));
|
||||||
|
if !atoms.iter().any(rtl) {
|
||||||
|
return atoms;
|
||||||
|
}
|
||||||
|
let mut atoms = atoms;
|
||||||
|
let mut i = 0;
|
||||||
|
while i < atoms.len() {
|
||||||
|
if !rtl(&atoms[i]) {
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut end = i + 1;
|
||||||
|
while end < atoms.len() && (rtl(&atoms[end]) || matches!(atoms[end].kind, AtomKind::Space)) {
|
||||||
|
end += 1;
|
||||||
|
}
|
||||||
|
atoms[i..end].reverse();
|
||||||
|
i = end;
|
||||||
|
}
|
||||||
|
atoms
|
||||||
|
}
|
||||||
|
|
||||||
pub fn reorder_line_atoms(atoms: Vec<LayoutAtom>, is_rtl: bool) -> Vec<LayoutAtom> {
|
pub fn reorder_line_atoms(atoms: Vec<LayoutAtom>, is_rtl: bool) -> Vec<LayoutAtom> {
|
||||||
if atoms.len() <= 1 && !is_rtl {
|
if atoms.len() <= 1 && !is_rtl {
|
||||||
return atoms;
|
return atoms;
|
||||||
@@ -1057,7 +1124,9 @@ pub fn word_cells(
|
|||||||
Some(
|
Some(
|
||||||
crate::scene::text::ttf_glyph::char_cells(&run, text)
|
crate::scene::text::ttf_glyph::char_cells(&run, text)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(x0, x1)| (x0 * scale, x1 * scale))
|
.zip(text.chars())
|
||||||
|
.filter(|(_, c)| *c != LRM)
|
||||||
|
.map(|((x0, x1), _)| (x0 * scale, x1 * scale))
|
||||||
.collect(),
|
.collect(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1141,7 +1210,8 @@ pub fn wrap_paragraph(
|
|||||||
AtomKind::Word(_) | AtomKind::Stack { .. } => {
|
AtomKind::Word(_) | AtomKind::Stack { .. } => {
|
||||||
let w = atom_width(&atom, entity_h, base_wf, base_font);
|
let w = atom_width(&atom, entity_h, base_wf, base_font);
|
||||||
let max_w = line_max_w(subline_idx);
|
let max_w = line_max_w(subline_idx);
|
||||||
if !cur.is_empty() && cur_w + w > max_w && !after_align_tab {
|
let has_content = cur.iter().any(|a| !matches!(a.kind, AtomKind::Space));
|
||||||
|
if has_content && cur_w + w > max_w && !after_align_tab {
|
||||||
while matches!(cur.last().map(|a| &a.kind), Some(AtomKind::Space)) {
|
while matches!(cur.last().map(|a| &a.kind), Some(AtomKind::Space)) {
|
||||||
cur.pop();
|
cur.pop();
|
||||||
}
|
}
|
||||||
@@ -1155,7 +1225,9 @@ pub fn wrap_paragraph(
|
|||||||
}
|
}
|
||||||
AtomKind::Space => {
|
AtomKind::Space => {
|
||||||
after_align_tab = false;
|
after_align_tab = false;
|
||||||
if cur.is_empty() {
|
// A wrapped line does not start with the spaces it broke at;
|
||||||
|
// the paragraph's own leading spaces keep their width.
|
||||||
|
if cur.is_empty() && subline_idx > 0 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
cur_w += atom_width(&atom, entity_h, base_wf, base_font);
|
cur_w += atom_width(&atom, entity_h, base_wf, base_font);
|
||||||
@@ -1526,7 +1598,7 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
|
|||||||
} else if ch == ' ' || ch == '\t' {
|
} else if ch == ' ' || ch == '\t' {
|
||||||
if !word.is_empty() {
|
if !word.is_empty() {
|
||||||
atoms.push(LayoutAtom {
|
atoms.push(LayoutAtom {
|
||||||
kind: AtomKind::Word(std::mem::take(&mut word)),
|
kind: AtomKind::Word(rtl_display_word(std::mem::take(&mut word))),
|
||||||
state: run.state.clone(),
|
state: run.state.clone(),
|
||||||
char_offset: word_start,
|
char_offset: word_start,
|
||||||
});
|
});
|
||||||
@@ -1554,7 +1626,7 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
|
|||||||
if let Some(prev) = word.chars().last() {
|
if let Some(prev) = word.chars().last() {
|
||||||
if cjk_break_between(prev, ch) {
|
if cjk_break_between(prev, ch) {
|
||||||
atoms.push(LayoutAtom {
|
atoms.push(LayoutAtom {
|
||||||
kind: AtomKind::Word(std::mem::take(&mut word)),
|
kind: AtomKind::Word(rtl_display_word(std::mem::take(&mut word))),
|
||||||
state: run.state.clone(),
|
state: run.state.clone(),
|
||||||
char_offset: word_start,
|
char_offset: word_start,
|
||||||
});
|
});
|
||||||
@@ -1567,7 +1639,7 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
|
|||||||
}
|
}
|
||||||
if !word.is_empty() {
|
if !word.is_empty() {
|
||||||
atoms.push(LayoutAtom {
|
atoms.push(LayoutAtom {
|
||||||
kind: AtomKind::Word(word),
|
kind: AtomKind::Word(rtl_display_word(word)),
|
||||||
state: run.state.clone(),
|
state: run.state.clone(),
|
||||||
char_offset: word_start,
|
char_offset: word_start,
|
||||||
});
|
});
|
||||||
@@ -1603,52 +1675,24 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trim leading + trailing Space atoms so line_w / cursor_start agree
|
// Trailing Space atoms draw nothing; the line width that centring /
|
||||||
// on the paragraph's visible content. Without this a stray trailing
|
// right-alignment use leaves them out (see `line_w` below), while
|
||||||
// space measures wider than it draws and centring / right-alignment
|
// leading spaces keep their width, as in the reference application.
|
||||||
// is off by half a space-width.
|
|
||||||
//
|
//
|
||||||
// Skipped when emitting glyph boxes (the MText editor) so a space the
|
// Kept when emitting glyph boxes (the MText editor) so a space the
|
||||||
// user just typed at the end keeps a selectable box and the caret can
|
// user just typed at the end keeps a selectable box and the caret can
|
||||||
// sit after it.
|
// sit after it.
|
||||||
if !opts.want_glyph_boxes {
|
if !opts.want_glyph_boxes {
|
||||||
let first_word = atoms
|
|
||||||
.iter()
|
|
||||||
.position(|a| !matches!(a.kind, AtomKind::Space))
|
|
||||||
.unwrap_or(atoms.len());
|
|
||||||
atoms.drain(..first_word);
|
|
||||||
while matches!(atoms.last().map(|a| &a.kind), Some(AtomKind::Space)) {
|
while matches!(atoms.last().map(|a| &a.kind), Some(AtomKind::Space)) {
|
||||||
atoms.pop();
|
atoms.pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let is_rtl_para = {
|
// The reference lays every MTEXT paragraph out left to right, also
|
||||||
let mut strong_rtl = None;
|
// one that starts with Hebrew or Arabic: alignment follows the
|
||||||
for atom in &atoms {
|
// attachment, and only right-to-left runs read backwards in place
|
||||||
match &atom.kind {
|
// (`reorder_rtl_runs`).
|
||||||
AtomKind::Word(w) => {
|
let is_rtl_para = false;
|
||||||
for ch in w.chars() {
|
|
||||||
match unicode_bidi::bidi_class(ch) {
|
|
||||||
unicode_bidi::BidiClass::R | unicode_bidi::BidiClass::AL => {
|
|
||||||
strong_rtl = Some(true);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
unicode_bidi::BidiClass::L => {
|
|
||||||
strong_rtl = Some(false);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if strong_rtl.is_some() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
strong_rtl.unwrap_or(false)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Wrap to the column the text actually flows down, not to the block:
|
// Wrap to the column the text actually flows down, not to the block:
|
||||||
// measuring against the full width would let a line run across the
|
// measuring against the full width would let a line run across the
|
||||||
@@ -1678,7 +1722,7 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
|
|||||||
let atoms = if opts.vertical_text {
|
let atoms = if opts.vertical_text {
|
||||||
atoms
|
atoms
|
||||||
} else {
|
} else {
|
||||||
reorder_line_atoms(atoms, is_rtl_para)
|
reorder_rtl_runs(atoms)
|
||||||
};
|
};
|
||||||
sub_lines.push(SubLine {
|
sub_lines.push(SubLine {
|
||||||
atoms,
|
atoms,
|
||||||
@@ -2123,8 +2167,13 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let visible_atoms = sub
|
||||||
|
.atoms
|
||||||
|
.iter()
|
||||||
|
.rposition(|a| !matches!(a.kind, AtomKind::Space))
|
||||||
|
.map_or(0, |i| i + 1);
|
||||||
let line_w = line_total_width(
|
let line_w = line_total_width(
|
||||||
&sub.atoms,
|
&sub.atoms[..visible_atoms],
|
||||||
entity_h,
|
entity_h,
|
||||||
base_wf,
|
base_wf,
|
||||||
&base_font_name,
|
&base_font_name,
|
||||||
@@ -2383,7 +2432,7 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
|
|||||||
}
|
}
|
||||||
if opts.want_glyph_boxes {
|
if opts.want_glyph_boxes {
|
||||||
let run_h = atom.state.height_mul * entity_h;
|
let run_h = atom.state.height_mul * entity_h;
|
||||||
let count = text.chars().count();
|
let count = text.chars().filter(|c| *c != LRM).count();
|
||||||
let is_rtl = text.chars().any(is_rtl_char);
|
let is_rtl = text.chars().any(is_rtl_char);
|
||||||
if is_rtl && count > 0 {
|
if is_rtl && count > 0 {
|
||||||
let word_w = if tracking != atom.state.tracking {
|
let word_w = if tracking != atom.state.tracking {
|
||||||
@@ -2429,7 +2478,7 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
|
|||||||
let face = Face::resolve(&font_name);
|
let face = Face::resolve(&font_name);
|
||||||
let shaped = word_cells(text, &atom.state, entity_h, base_wf, &base_font_name);
|
let shaped = word_cells(text, &atom.state, entity_h, base_wf, &base_font_name);
|
||||||
let mut cx = cursor_x;
|
let mut cx = cursor_x;
|
||||||
for (ci, ch) in text.chars().enumerate() {
|
for (ci, ch) in text.chars().filter(|c| *c != LRM).enumerate() {
|
||||||
let adv = match face.glyph(ch) {
|
let adv = match face.glyph(ch) {
|
||||||
Some(g) => {
|
Some(g) => {
|
||||||
(g.advance + face.spacing_after(ch) * tracking) * scale
|
(g.advance + face.spacing_after(ch) * tracking) * scale
|
||||||
|
|||||||
@@ -603,6 +603,9 @@ pub fn char_cells(run: &ShapedRun, text: &str) -> Vec<(f32, f32)> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Left-to-right mark shaped ahead of every run.
|
||||||
|
const BASE_MARK: char = '';
|
||||||
|
|
||||||
type ShapeCache = HashMap<(String, String), Option<Arc<ShapedRun>>>;
|
type ShapeCache = HashMap<(String, String), Option<Arc<ShapedRun>>>;
|
||||||
|
|
||||||
fn shape_cache() -> &'static Mutex<ShapeCache> {
|
fn shape_cache() -> &'static Mutex<ShapeCache> {
|
||||||
@@ -901,7 +904,11 @@ fn build_shaped(_family: &str, text: &str) -> Option<ShapedRun> {
|
|||||||
let attrs = Attrs::new().family(Family::SansSerif);
|
let attrs = Attrs::new().family(Family::SansSerif);
|
||||||
let mut buffer = Buffer::new(&mut font_system, Metrics::new(SHAPE_FS, SHAPE_FS));
|
let mut buffer = Buffer::new(&mut font_system, Metrics::new(SHAPE_FS, SHAPE_FS));
|
||||||
buffer.set_size(&mut font_system, None, None);
|
buffer.set_size(&mut font_system, None, None);
|
||||||
buffer.set_text(&mut font_system, text, &attrs, Shaping::Advanced, None);
|
// A leading left-to-right mark keeps the paragraph direction left to
|
||||||
|
// right, as the reference lays text out (right-to-left runs still read
|
||||||
|
// backwards); `cells` stay relative to `text`.
|
||||||
|
let marked = format!("{BASE_MARK}{text}");
|
||||||
|
buffer.set_text(&mut font_system, &marked, &attrs, Shaping::Advanced, None);
|
||||||
buffer.shape_until_scroll(&mut font_system, false);
|
buffer.shape_until_scroll(&mut font_system, false);
|
||||||
|
|
||||||
let mut glyphs = Vec::new();
|
let mut glyphs = Vec::new();
|
||||||
@@ -910,7 +917,10 @@ fn build_shaped(_family: &str, text: &str) -> Option<ShapedRun> {
|
|||||||
for run in buffer.layout_runs() {
|
for run in buffer.layout_runs() {
|
||||||
advance = advance.max(run.line_w * px_to_9);
|
advance = advance.max(run.line_w * px_to_9);
|
||||||
for glyph in run.glyphs.iter() {
|
for glyph in run.glyphs.iter() {
|
||||||
cells.push((glyph.start, glyph.end, glyph.x * px_to_9, (glyph.x + glyph.w) * px_to_9));
|
if glyph.start >= BASE_MARK.len_utf8() {
|
||||||
|
let m = BASE_MARK.len_utf8();
|
||||||
|
cells.push((glyph.start - m, glyph.end - m, glyph.x * px_to_9, (glyph.x + glyph.w) * px_to_9));
|
||||||
|
}
|
||||||
let face_index = font_system
|
let face_index = font_system
|
||||||
.db_mut()
|
.db_mut()
|
||||||
.face(glyph.font_id)
|
.face(glyph.font_id)
|
||||||
@@ -978,7 +988,11 @@ fn build_shaped(family: &str, text: &str) -> Option<ShapedRun> {
|
|||||||
let mut buf = Buffer::new(&mut fs, Metrics::new(SHAPE_FS, SHAPE_FS));
|
let mut buf = Buffer::new(&mut fs, Metrics::new(SHAPE_FS, SHAPE_FS));
|
||||||
// No wrapping: a run is a single line.
|
// No wrapping: a run is a single line.
|
||||||
buf.set_size(&mut fs, None, None);
|
buf.set_size(&mut fs, None, None);
|
||||||
buf.set_text(&mut fs, text, &attrs, Shaping::Advanced, None);
|
// A leading left-to-right mark keeps the paragraph direction left to
|
||||||
|
// right, as the reference lays text out (right-to-left runs still read
|
||||||
|
// backwards); `cells` stay relative to `text`.
|
||||||
|
let marked = format!("{BASE_MARK}{text}");
|
||||||
|
buf.set_text(&mut fs, &marked, &attrs, Shaping::Advanced, None);
|
||||||
buf.shape_until_scroll(&mut fs, false);
|
buf.shape_until_scroll(&mut fs, false);
|
||||||
|
|
||||||
if primary_metrics.is_none() {
|
if primary_metrics.is_none() {
|
||||||
@@ -1017,7 +1031,10 @@ fn build_shaped(family: &str, text: &str) -> Option<ShapedRun> {
|
|||||||
for run in buf.layout_runs() {
|
for run in buf.layout_runs() {
|
||||||
advance = advance.max(run.line_w * px_to_9);
|
advance = advance.max(run.line_w * px_to_9);
|
||||||
for g in run.glyphs.iter() {
|
for g in run.glyphs.iter() {
|
||||||
cells.push((g.start, g.end, g.x * px_to_9, (g.x + g.w) * px_to_9));
|
if g.start >= BASE_MARK.len_utf8() {
|
||||||
|
let m = BASE_MARK.len_utf8();
|
||||||
|
cells.push((g.start - m, g.end - m, g.x * px_to_9, (g.x + g.w) * px_to_9));
|
||||||
|
}
|
||||||
let face_index = fs.db_mut().face(g.font_id).map(|f| f.index).unwrap_or(0);
|
let face_index = fs.db_mut().face(g.font_id).map(|f| f.index).unwrap_or(0);
|
||||||
let Some(font) = fs.get_font(g.font_id, g.font_weight) else {
|
let Some(font) = fs.get_font(g.font_id, g.font_weight) else {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
Reference in new issue
Block a user