I maintain a simple hashtable mapping fonts to a companion fontmetrics object. Somehow,
this hashtable maps all fonts to the same slot. The only rational explanation for this would be
if "font" objects have a custom hashcode and equals function that are faulty.
This behavior is brand new, and affects IOS only. The underlying code has been in use for
years.
Here's a test program
/**
* demo program for issue xxx, fontmetric cache failure
*/
package com.boardspace.dtest;
import com.codename1.ui.Component;
//
// drawing pdfs
//
//
import com.codename1.ui.Display;
import com.codename1.ui.Form;
import com.codename1.ui.Graphics;
import com.codename1.ui.Toolbar;
import com.codename1.ui.geom.Dimension;
import com.codename1.ui.plaf.UIManager;
import com.codename1.ui.util.Resources;
import java.util.ArrayList;
/*
Copyright 2006-2023 by Dave Dyer
This file is part of the Boardspace project.
Boardspace 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.
Boardspace 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 Boardspace.
If not, see https://www.gnu.org/licenses/.
*/
import java.util.Hashtable;
import com.codename1.ui.Font;
import com.codename1.ui.geom.Rectangle2D;
//There are these fonts now: https://www.codenameone.com/blog/good-looking-by-default-native-fonts-simulator-detection-more.html
class LruCache
{ int size=0;
static int hits = 0;
static int probes = 0;
String strs[];
int values[];
public String toString() { return "<wc "+hits*100/probes+">"; }
static int NOVALUE = - 0x023536215;
LruCache(int sz)
{ size = sz;
strs = new String[sz];
values = new int[sz];
}
synchronized int getValue(String key)
{ probes++;
for(int i=0;i<size; i++)
{ if(key==strs[i])
{ hits++;
int v = values[i];
while(i>0)
{
strs[i] = strs[i-i];
values[i] = values[i-1];
i--;
}
return v; }
}
return NOVALUE;
}
synchronized void storeValue(String key,int val)
{ for(int idx=size-2; idx>=0;idx--)
{ strs[idx+1] = strs[idx];
values[idx+1] = values[idx];
}
values[0] = val;
strs[0] = key;
}}
class FontMetrics {
private Font myFont;
static Hashtable <Font,FontMetrics>fmCache = new Hashtable<Font,FontMetrics>();
LruCache widthCache = new LruCache(3);
public FontMetrics(Font f)
{ myFont = f;
int oldsize = fmCache.size();
FontMetrics old = fmCache.get(f);
fmCache.put(f,this);
int newsize = fmCache.size();
Test.messages.add("add font "+f+" was "+old+" "+oldsize);
Test.messages.add("new "+this+newsize);
}
public static FontMetrics getFontMetrics(Font g)
{
FontMetrics m = fmCache.get(g);
if(m==null)
{
m = new FontMetrics(g);
}
if(m.myFont!=g)
{
Test.messages.add("Impossible mismatch fm "+m+" "+m.myFont+" "+g);
FontMetrics m2 = fmCache.get(m.myFont);
Test.messages.add("Other is "+m2);
m = new FontMetrics(g);
}
return m;
}
public int getMaxDescent() { return myFont.getDescent(); }
public static FontMetrics getFontMetrics(Graphics g)
{ return(getFontMetrics(g.getFont()));
}
public Rectangle2D getStringBounds(String str, Graphics context)
{
return(new Rectangle2D(0,0,stringWidth(str),getHeight()));
}
public Rectangle2D getStringBounds(String str, int from,int to,Graphics context)
{
return(new Rectangle2D(stringWidth(str.substring(0,from)),
0,
stringWidth(str.substring(from,to)),
getHeight()));
}
public Font getFont() { return(myFont); }
public int stringWidth(String str)
{ int w = widthCache.getValue(str);
// widthcache per font is only intended to optimize when
// the same string is queried multiple times while being prepped for display
if(w!=LruCache.NOVALUE) { return w; }
w = myFont.stringWidth(str);
widthCache.storeValue(str,w);
return(w);
}
public int getHeight()
{
int h = myFont.getHeight();
return h;
}
public int getDescent() { return(myFont.getDescent()); }
public int getAscent() { return(myFont.getAscent()); }
public int getMaxAscent()
{ // the standard spec is a little fuzzy, it says
// "ascent" is the height for most alphanumeric characters,
// but some characters may be taller. So we hope getAscent is
// good enough
return(myFont.getAscent());
}
public static Font getFont(Font f,int size)
{
Font fd = f.derive(size,0);
return(fd);
}
static int fontFaceCode(String spec)
{
if ("monospaced".equalsIgnoreCase(spec)) { return Font.FACE_MONOSPACE; }
if ("serif".equalsIgnoreCase(spec)) { return Font.FACE_PROPORTIONAL; }
return Font.FACE_SYSTEM;
}
public static Font getFont(String family,int size)
{
Font f = Font.createSystemFont(fontFaceCode(family),0,size);
return f;
}
}
class Test extends Component implements Runnable
{ Form form = null;
static ArrayList<String>messages = new ArrayList<String>();
static void Message(String m) { messages.add(m); }
public Component getCanvas() { return this; }
Test(Form f)
{ form = f;
setPreferredSize(new Dimension(f.getWidth(),f.getHeight()));
}
int loops = 0;
int phase = 0;
String got = "first with coords";
String got2 = "second with rectangle";
public void paint(Graphics gc)
{
int w = form.getWidth();
int h = form.getHeight();
int step = 40;
gc.setColor(0x806060);
gc.fillRect(0,0,w,h);
gc.setColor(0);
String phase = "before";
try {
for(int i=0;i<messages.size() && i*step<h;i++ )
{
gc.drawString(messages.get(i),20,(i+1)*step);
}
}
catch(Throwable err)
{
gc.drawString("error "+err+" in phase "+phase,100,400);
}
}
public void run() {
int sz = 10;
Font f = FontMetrics.getFont("Arial Unicode MS",sz);
for(int i=0;i<10;i++)
{
FontMetrics.getFontMetrics(f);
Font f1 = FontMetrics.getFont(f,sz++);
FontMetrics.getFontMetrics(f1);
}
while(true)
{ this.repaint();
try { Thread.sleep(1000); }
catch(InterruptedException e) {}
}
}
}
@SuppressWarnings("rawtypes")
public class Dtest{
private Form current;
@SuppressWarnings("unused")
private Resources theme;
public void init(Object context) {
theme = UIManager.initFirstTheme("/theme");
// Pro only feature, uncomment if you have a pro subscription
// Log.bindCrashProtection(true);
}
public Toolbar toolbar = null;
public void run()
{
}
public void start() {
if(current != null){
current.show();
}
Form hi = new Form();
hi.setToolbar(new Toolbar());
Test can = new Test(hi);
hi.addComponent(can);
can.setVisible(true);
new Thread(can).start();
hi.show();
}
public void stop() {
current = Display.getInstance().getCurrent();
}
public void destroy() {
}
}
I maintain a simple hashtable mapping fonts to a companion fontmetrics object. Somehow,
this hashtable maps all fonts to the same slot. The only rational explanation for this would be
if "font" objects have a custom hashcode and equals function that are faulty.
This behavior is brand new, and affects IOS only. The underlying code has been in use for
years.
Here's a test program