Skip to content

Commit

Permalink
Emoji support in tables (#276)
Browse files Browse the repository at this point in the history
* Fix rendering of cells with table decoration

* Add TableDemo

* Add tests and fix issue removing first column
  • Loading branch information
jperedadnr authored Jul 20, 2023
1 parent f36ed38 commit 078375e
Show file tree
Hide file tree
Showing 7 changed files with 375 additions and 23 deletions.
Binary file added .github/assets/TableDemo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
51 changes: 30 additions & 21 deletions rta/src/main/java/com/gluonhq/richtextarea/RichListCell.java
Original file line number Diff line number Diff line change
Expand Up @@ -158,27 +158,36 @@ protected void updateItem(Paragraph item, boolean empty) {
richTextAreaSkin.getViewModel().walkFragments((unit, decoration) -> {
if (decoration instanceof TextDecoration && !unit.isEmpty()) {
if (item.getDecoration().hasTableDecoration()) {
// TODO REFACTORING: deal with units inside table cells
String text = unit.getText();
AtomicInteger s = new AtomicInteger();
IntStream.iterate(text.indexOf(TextBuffer.ZERO_WIDTH_TABLE_SEPARATOR),
index -> index >= 0,
index -> text.indexOf(TextBuffer.ZERO_WIDTH_TABLE_SEPARATOR, index + 1))
.boxed()
.forEach(i -> {
String tableText = text.substring(s.getAndSet(i + 1), i + 1);
final Text textNode = buildText(tableText, (TextDecoration) decoration);
textNode.getProperties().put(TABLE_SEPARATOR, tp.get());
fragments.add(textNode);
positions.add(tp.addAndGet(tableText.length()));
});
if (s.get() < text.length()) {
String tableText = text.substring(s.get()).replace("\n", TextBuffer.ZERO_WIDTH_TEXT);
final Text textNode = buildText(tableText, (TextDecoration) decoration);
textNode.getProperties().put(TABLE_SEPARATOR, tp.getAndAdd(tableText.length()));
fragments.add(textNode);
if (text.substring(s.get()).contains("\n")) {
positions.add(tp.get());
if (unit instanceof TextUnit) {
String text = unit.getText();
AtomicInteger s = new AtomicInteger();
IntStream.iterate(text.indexOf(TextBuffer.ZERO_WIDTH_TABLE_SEPARATOR),
index -> index >= 0,
index -> text.indexOf(TextBuffer.ZERO_WIDTH_TABLE_SEPARATOR, index + 1))
.boxed()
.forEach(i -> {
String tableText = text.substring(s.getAndSet(i + 1), i + 1);
final Text textNode = buildText(tableText, (TextDecoration) decoration);
textNode.getProperties().put(TABLE_SEPARATOR, tp.get());
fragments.add(textNode);
positions.add(tp.addAndGet(tableText.length()));
});
if (s.get() < text.length()) {
String tableText = text.substring(s.get()).replace("\n", TextBuffer.ZERO_WIDTH_TEXT);
final Text textNode = buildText(tableText, (TextDecoration) decoration);
textNode.getProperties().put(TABLE_SEPARATOR, tp.getAndAdd(tableText.length()));
fragments.add(textNode);
if (text.substring(s.get()).contains("\n")) {
positions.add(tp.get());
}
}
} else {
final Node node = buildNode(unit, (TextDecoration) decoration);
node.getProperties().put(TABLE_SEPARATOR, tp.getAndIncrement());
fragments.add(node);
length.addAndGet(unit.length());
if (unit instanceof EmojiUnit) {
richTextAreaSkin.nonTextNodes.incrementAndGet();
}
}
} else {
Expand Down
10 changes: 8 additions & 2 deletions rta/src/main/java/com/gluonhq/richtextarea/model/Table.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
package com.gluonhq.richtextarea.model;

import com.gluonhq.richtextarea.Selection;
import com.gluonhq.richtextarea.Tools;

import java.util.List;
import java.util.logging.Level;
Expand Down Expand Up @@ -64,6 +65,11 @@ public class Table {
private final int start;
private final int rows;
private final int columns;

/**
* list of global indices of the table separator chars '\u200b', referred to the
* start of the document
*/
private final List<Integer> positions;
private final List<String> textCells;

Expand Down Expand Up @@ -199,7 +205,7 @@ public String removeColumnAndGetText(int caret) {
for (int i = rows - 1; i >= 0; i--) {
// remove text from current column, for each row
int posStart = currentCol == 0 && i == 0 ? 0 : positions.get(i * columns + currentCol - 1) - start;
int posEnd = positions.get(i * columns + currentCol) - start;
int posEnd = positions.get(i * columns + currentCol) - start + (currentCol == 0 && i == 0 ? 1 : 0);
newText = newText.substring(0, posStart) + newText.substring(posEnd);
}
return newText;
Expand Down Expand Up @@ -229,7 +235,7 @@ private int getCurrentCell(int caret) {
return currentRow * columns + currentCol;
}

private List<Integer> getTablePositions() {
List<Integer> getTablePositions() {
List<Integer> positions = IntStream.iterate(text.indexOf(TextBuffer.ZERO_WIDTH_TABLE_SEPARATOR),
index -> index >= 0,
index -> text.indexOf(TextBuffer.ZERO_WIDTH_TABLE_SEPARATOR, index + 1))
Expand Down
197 changes: 197 additions & 0 deletions rta/src/test/java/com/gluonhq/richtextarea/model/TableTests.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/*
* Copyright (c) 2023, Gluon
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL GLUON BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.gluonhq.richtextarea.model;

import com.gluonhq.richtextarea.Selection;
import com.gluonhq.richtextarea.viewmodel.RichTextAreaViewModel;
import javafx.scene.text.FontWeight;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

public class TableTests {

private static final TableDecoration tableDecoration = new TableDecoration(1, 3);
private static final Document FACE_MODEL = new Document(
"One\u200bText\u200bname!\u200bend\n",
List.of(new DecorationModel(0, 19,
TextDecoration.builder().presets().build(),
ParagraphDecoration.builder().tableDecoration(tableDecoration).build())),
0);

private static final Document FACE_MODEL_WITH_EMOJI = new Document(
"One \ud83d\ude00\u200bText\u200bname!\u200bend\n",
List.of(new DecorationModel(0, 22,
TextDecoration.builder().presets().build(),
ParagraphDecoration.builder().tableDecoration(tableDecoration).build())),
0);

@Test
@DisplayName("Table: table with text")
public void originalTextIntact() {
PieceTable pt = new PieceTable(FACE_MODEL);
Assertions.assertEquals(FACE_MODEL.getText(), pt.getText());
Table table = new Table(FACE_MODEL.getText(), 0, 1, 4);
Assertions.assertEquals(pt.getTextLength() - 1, table.getTableTextLength()); // 18
}

@Test
@DisplayName("Table: table with text and emoji")
public void originalTextIntactWithEmoji() {
PieceTable pt = new PieceTable(FACE_MODEL_WITH_EMOJI);
Assertions.assertEquals(FACE_MODEL_WITH_EMOJI.getText(), pt.getText());
Table table = new Table(FACE_MODEL_WITH_EMOJI.getText(), 0, 1, 4);
Assertions.assertEquals(pt.getTextLength(), table.getTableTextLength()); // 21
}

@Test
@DisplayName("Table: table with four columns")
public void tablePositions1x4() {
PieceTable pt = new PieceTable(FACE_MODEL);
Assertions.assertEquals(FACE_MODEL.getText(), pt.getText());
Table table = new Table(FACE_MODEL.getText(), 0, 1, 4);
List<Integer> tablePositions = table.getTablePositions();
Assertions.assertEquals(table.getRows() * table.getColumns(), tablePositions.size()); // 4
Assertions.assertEquals("[3, 8, 14, 18]", tablePositions.toString());
int col = 0;
for (int i = 0; i <= 18; i++) {
Assertions.assertEquals(0, table.getCurrentRow(i));
Assertions.assertEquals(col, table.getCurrentColumn(i));
if (i == tablePositions.get(col)) {
col++;
}
}
}

@Test
@DisplayName("Table: table with four columns and emoji")
public void tableEmojiPositions1x4() {
PieceTable pt = new PieceTable(FACE_MODEL_WITH_EMOJI);
Assertions.assertEquals(FACE_MODEL_WITH_EMOJI.getText(), pt.getText());
Table table = new Table(FACE_MODEL_WITH_EMOJI.getText(), 0, 1, 4);
List<Integer> tablePositions = table.getTablePositions();
Assertions.assertEquals(table.getRows() * table.getColumns(), tablePositions.size()); // 4
Assertions.assertEquals("[6, 11, 17, 21]", tablePositions.toString());
int col = 0;
for (int i = 0; i <= 21; i++) {
Assertions.assertEquals(0, table.getCurrentRow(i));
Assertions.assertEquals(col, table.getCurrentColumn(i));
if (i == tablePositions.get(col)) {
col++;
}
}
}

@Test
@DisplayName("Table: 2x2 table")
public void tablePositions2x2() {
PieceTable pt = new PieceTable(FACE_MODEL);
Assertions.assertEquals(FACE_MODEL.getText(), pt.getText());
Table table = new Table(FACE_MODEL.getText(), 0, 2, 2);
List<Integer> tablePositions = table.getTablePositions();
Assertions.assertEquals(table.getRows() * table.getColumns(), tablePositions.size()); // 4
Assertions.assertEquals("[3, 8, 14, 18]", tablePositions.toString());
for (int i = 0; i <= 18; i++) {
Assertions.assertEquals(i <= tablePositions.get(1) ? 0 : 1, table.getCurrentRow(i));
Assertions.assertEquals(i <= tablePositions.get(0) ||
(tablePositions.get(1) < i && i <= tablePositions.get(2)) ? 0 : 1, table.getCurrentColumn(i));
}
}

@Test
@DisplayName("Table: 2x2 table with emoji")
public void tableEmojiPositions2x2() {
PieceTable pt = new PieceTable(FACE_MODEL_WITH_EMOJI);
Assertions.assertEquals(FACE_MODEL_WITH_EMOJI.getText(), pt.getText());
Table table = new Table(FACE_MODEL_WITH_EMOJI.getText(), 0, 2, 2);
List<Integer> tablePositions = table.getTablePositions();
Assertions.assertEquals(table.getRows() * table.getColumns(), tablePositions.size()); // 4
Assertions.assertEquals("[6, 11, 17, 21]", tablePositions.toString());
for (int i = 0; i <= 21; i++) {
Assertions.assertEquals(i <= tablePositions.get(1) ? 0 : 1, table.getCurrentRow(i));
Assertions.assertEquals(i <= tablePositions.get(0) ||
(tablePositions.get(1) < i && i <= tablePositions.get(2)) ? 0 : 1, table.getCurrentColumn(i));
}
}

@Test
@DisplayName("Table: 2x2 table + column")
public void addColumnToTable2x2() {
PieceTable pt = new PieceTable(FACE_MODEL);
Assertions.assertEquals(FACE_MODEL.getText(), pt.getText());
Table table = new Table(FACE_MODEL.getText(), 0, 2, 2);
String newText = table.addColumnAndGetTableText(0, RichTextAreaViewModel.Direction.BACK);
Assertions.assertEquals("\u200bOne\u200bText\u200b\u200bname!\u200bend", newText);
newText = table.addColumnAndGetTableText(0, RichTextAreaViewModel.Direction.FORWARD);
Assertions.assertEquals("One\u200b\u200bText\u200bname!\u200b\u200bend", newText);
newText = table.addColumnAndGetTableText(4, RichTextAreaViewModel.Direction.FORWARD);
Assertions.assertEquals("One\u200bText\u200b\u200bname!\u200bend\u200b", newText);
}

@Test
@DisplayName("Table: 2x2 table with emoji + column")
public void addColumnToTableEmoji2x2() {
PieceTable pt = new PieceTable(FACE_MODEL_WITH_EMOJI);
Assertions.assertEquals(FACE_MODEL_WITH_EMOJI.getText(), pt.getText());
Table table = new Table(FACE_MODEL_WITH_EMOJI.getText(), 0, 2, 2);
String newText = table.addColumnAndGetTableText(0, RichTextAreaViewModel.Direction.BACK);
Assertions.assertEquals("\u200bOne \ud83d\ude00\u200bText\u200b\u200bname!\u200bend", newText);
newText = table.addColumnAndGetTableText(0, RichTextAreaViewModel.Direction.FORWARD);
Assertions.assertEquals("One \ud83d\ude00\u200b\u200bText\u200bname!\u200b\u200bend", newText);
newText = table.addColumnAndGetTableText(7, RichTextAreaViewModel.Direction.FORWARD);
Assertions.assertEquals("One \ud83d\ude00\u200bText\u200b\u200bname!\u200bend\u200b", newText);
}

@Test
@DisplayName("Table: 2x2 table - column")
public void removeColumnFromTable2x2() {
PieceTable pt = new PieceTable(FACE_MODEL);
Assertions.assertEquals(FACE_MODEL.getText(), pt.getText());
Table table = new Table(FACE_MODEL.getText(), 0, 2, 2);
String newText = table.removeColumnAndGetText(0);
Assertions.assertEquals("Text\u200bend", newText);
newText = table.removeColumnAndGetText(4);
Assertions.assertEquals("One\u200bname!", newText);
}

@Test
@DisplayName("Table: 2x2 table with emoji - column")
public void removeColumnFromTableEmoji2x2() {
PieceTable pt = new PieceTable(FACE_MODEL_WITH_EMOJI);
Assertions.assertEquals(FACE_MODEL_WITH_EMOJI.getText(), pt.getText());
Table table = new Table(FACE_MODEL_WITH_EMOJI.getText(), 0, 2, 2);
String newText = table.removeColumnAndGetText(0);
Assertions.assertEquals("Text\u200bend", newText);
newText = table.removeColumnAndGetText(7);
Assertions.assertEquals("One \ud83d\ude00\u200bname!", newText);
}

}
15 changes: 15 additions & 0 deletions samples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,21 @@ mvn javafx:run -Dmain.class=com.gluonhq.richtextarea.samples.EmojiPopupDemo
![rta_editor.png](../.github/assets/EmojiPopupDemo.png)
## TableDemo
The [TableDemo](/samples/src/main/java/com/gluonhq/richtextarea/samples/TableDemo.java) shows how to use the
RichTextArea control to render a table, embedded into the text, including text and emojis.
### Usage
To run this sample, using Java 17+, do as follows:
```
mvn javafx:run -Dmain.class=com.gluonhq.richtextarea.samples.TableDemo
```
![rta_editor.png](../.github/assets/TableDemo.png)
## FullFeaturedDemo
The [FullFeaturedDemo](/samples/src/main/java/com/gluonhq/richtextarea/samples/FullFeaturedDemo.java) shows a complete
Expand Down
Loading

0 comments on commit 078375e

Please sign in to comment.